From a8b2957b7469535d695e3fd3743c00a8c0597b08 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:24:43 +0100 Subject: [PATCH 001/154] Fix preview on crop/rotate (#7266) Preview on crop and rotate shows just the original file you uploaded, if you switch to a new one it doesn't update. --- .../components/tools/crop/CropSettings.tsx | 29 ++--- .../tools/rotate/RotateSettings.test.tsx | 66 ++++++++++ .../tools/rotate/RotateSettings.tsx | 25 ++-- .../tools/shared/useViewScopedFiles.test.ts | 119 ++++++++++++++++++ .../hooks/tools/shared/useViewScopedFiles.ts | 27 +++- 5 files changed, 230 insertions(+), 36 deletions(-) create mode 100644 frontend/editor/src/core/components/tools/rotate/RotateSettings.test.tsx create mode 100644 frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.test.ts diff --git a/frontend/editor/src/core/components/tools/crop/CropSettings.tsx b/frontend/editor/src/core/components/tools/crop/CropSettings.tsx index 710391dd6c..82516059b0 100644 --- a/frontend/editor/src/core/components/tools/crop/CropSettings.tsx +++ b/frontend/editor/src/core/components/tools/crop/CropSettings.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Stack, Text, @@ -12,7 +12,10 @@ import { ActionIcon } from "@app/ui/ActionIcon"; import { useTranslation } from "react-i18next"; import RestartAltIcon from "@mui/icons-material/RestartAlt"; import { CropParametersHook } from "@app/hooks/tools/crop/useCropParameters"; -import { useAllFiles } from "@app/contexts/FileContext"; +import { + useViewScopedFiles, + useViewScopedFileStubs, +} from "@app/hooks/tools/shared/useViewScopedFiles"; import CropAreaSelector from "@app/components/tools/crop/CropAreaSelector"; import CropCoordinateInputs from "@app/components/tools/crop/CropCoordinateInputs"; import { DEFAULT_CROP_AREA } from "@app/constants/cropConstants"; @@ -34,32 +37,20 @@ const CONTAINER_SIZE = 250; // Fit within actual pane width const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => { const { t } = useTranslation(); - const { files, fileStubs } = useAllFiles(); + // Preview and measure the document the crop will actually apply to, so both + // follow the viewer when the user switches files with the tool open. + const [selectedStub = null] = useViewScopedFileStubs(); + const [selectedFile = null] = useViewScopedFiles(); - // Get the first file for preview - const selectedStub = useMemo(() => { - return fileStubs.length > 0 ? fileStubs[0] : null; - }, [fileStubs]); - - // Get the first file for PDF processing - const selectedFile = useMemo(() => { - return files.length > 0 ? files[0] : null; - }, [files]); - - // Get thumbnail for the selected file - const [thumbnail, setThumbnail] = useState(null); const [pdfBounds, setPdfBounds] = useState(null); useEffect(() => { const loadPDFDimensions = async () => { if (!selectedStub || !selectedFile) { setPdfBounds(null); - setThumbnail(null); return; } - setThumbnail(selectedStub.thumbnailUrl || null); - try { // Get PDF dimensions from the actual file const arrayBuffer = await selectedFile.arrayBuffer(); @@ -223,7 +214,7 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => { > ({ useViewer: () => viewer })); +vi.mock("@app/contexts/NavigationContext", () => ({ + useNavigationState: () => ({ workbench: "viewer" }), +})); +vi.mock("@app/contexts/FileContext", () => ({ useAllFiles: () => files })); +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (_key: string, fallback?: string) => fallback ?? _key, + }), +})); + +import RotateSettings from "@app/components/tools/rotate/RotateSettings"; + +const parameters = { + parameters: { angle: 0 }, + rotateClockwise: vi.fn(), + rotateAnticlockwise: vi.fn(), +} as never; + +const renderAt = (activeFileIndex: number) => { + viewer.activeFileIndex = activeFileIndex; + return render( + + + , + ); +}; + +describe("RotateSettings preview", () => { + it("previews the viewer's active file, not the first loaded one", () => { + renderAt(1); + + expect(screen.getByAltText("Preview of beta.pdf")).toBeInTheDocument(); + expect(screen.queryByAltText("Preview of alpha.pdf")).toBeNull(); + }); + + it("follows the viewer when the active file changes", () => { + const { rerender } = renderAt(0); + expect(screen.getByAltText("Preview of alpha.pdf")).toBeInTheDocument(); + + viewer.activeFileIndex = 1; + rerender( + + + , + ); + + expect(screen.getByAltText("Preview of beta.pdf")).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/core/components/tools/rotate/RotateSettings.tsx b/frontend/editor/src/core/components/tools/rotate/RotateSettings.tsx index ba66790298..950c78ea99 100644 --- a/frontend/editor/src/core/components/tools/rotate/RotateSettings.tsx +++ b/frontend/editor/src/core/components/tools/rotate/RotateSettings.tsx @@ -1,11 +1,11 @@ -import { useMemo, useState, useEffect } from "react"; +import { useEffect } from "react"; import { Stack, Text, Box, Group, Center } from "@mantine/core"; import { ActionIcon } from "@app/ui/ActionIcon"; import { useTranslation } from "react-i18next"; import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateRightIcon from "@mui/icons-material/RotateRight"; import { RotateParametersHook } from "@app/hooks/tools/rotate/useRotateParameters"; -import { useAllFiles } from "@app/contexts/FileContext"; +import { useViewScopedFileStubs } from "@app/hooks/tools/shared/useViewScopedFiles"; import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail"; interface RotateSettingsProps { @@ -18,19 +18,9 @@ const RotateSettings = ({ disabled = false, }: RotateSettingsProps) => { const { t } = useTranslation(); - const { fileStubs } = useAllFiles(); - - // Get the first file for preview - const selectedStub = useMemo(() => { - return fileStubs.length > 0 ? fileStubs[0] : null; - }, [fileStubs]); - - // Get thumbnail for the selected file - const [thumbnail, setThumbnail] = useState(null); - - useEffect(() => { - setThumbnail(selectedStub?.thumbnailUrl || null); - }, [selectedStub]); + // Preview the document the rotation will actually apply to, so it follows + // the viewer when the user switches files with the tool open. + const [previewStub = null] = useViewScopedFileStubs(); // Calculate current angle display const currentAngle = parameters.parameters.angle; @@ -86,7 +76,10 @@ const RotateSettings = ({ justifyContent: "center", }} > - + diff --git a/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.test.ts b/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.test.ts new file mode 100644 index 0000000000..974782c09e --- /dev/null +++ b/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderHook } from "@testing-library/react"; + +const mockViewer = { activeFileIndex: 0 }; +const mockNavigation = { workbench: "viewer" }; +const mockFiles: { files: unknown[]; fileStubs: unknown[] } = { + files: [], + fileStubs: [], +}; + +vi.mock("@app/contexts/ViewerContext", () => ({ + useViewer: () => mockViewer, +})); + +vi.mock("@app/contexts/NavigationContext", () => ({ + useNavigationState: () => mockNavigation, +})); + +vi.mock("@app/contexts/FileContext", () => ({ + useAllFiles: () => mockFiles, +})); + +import { + useViewScopedFiles, + useViewScopedFileStubs, +} from "@app/hooks/tools/shared/useViewScopedFiles"; + +const file = (id: string) => ({ fileId: id, name: `${id}.pdf` }); +const stub = (id: string) => ({ + id, + name: `${id}.pdf`, + thumbnailUrl: `${id}.png`, +}); + +function setState(opts: { + workbench?: string; + activeFileIndex?: number; + files?: unknown[]; + fileStubs?: unknown[]; +}) { + mockNavigation.workbench = opts.workbench ?? "viewer"; + mockViewer.activeFileIndex = opts.activeFileIndex ?? 0; + mockFiles.files = opts.files ?? []; + mockFiles.fileStubs = opts.fileStubs ?? []; +} + +describe("useViewScopedFileStubs", () => { + it("follows the viewer's active file rather than the first loaded one", () => { + setState({ + files: [file("a"), file("b"), file("c")], + fileStubs: [stub("a"), stub("b"), stub("c")], + activeFileIndex: 2, + }); + + const { result } = renderHook(() => useViewScopedFileStubs()); + + expect(result.current.map((s) => s.id)).toEqual(["c"]); + }); + + it("re-resolves when the viewer switches files", () => { + setState({ + files: [file("a"), file("b")], + fileStubs: [stub("a"), stub("b")], + activeFileIndex: 0, + }); + + const { result, rerender } = renderHook(() => useViewScopedFileStubs()); + expect(result.current.map((s) => s.id)).toEqual(["a"]); + + setState({ + files: [file("a"), file("b")], + fileStubs: [stub("a"), stub("b")], + activeFileIndex: 1, + }); + rerender(); + + expect(result.current.map((s) => s.id)).toEqual(["b"]); + }); + + it("matches by id, not position, when the two lists are out of step", () => { + // A stub exists before its bytes load, so the stub list can be longer than + // (and offset from) the file list. + setState({ + files: [file("b")], + fileStubs: [stub("a"), stub("b")], + activeFileIndex: 0, + }); + + const { result } = renderHook(() => useViewScopedFileStubs()); + + expect(result.current.map((s) => s.id)).toEqual(["b"]); + }); + + it("returns every stub outside the viewer, mirroring useViewScopedFiles", () => { + setState({ + workbench: "fileEditor", + files: [file("a"), file("b")], + fileStubs: [stub("a"), stub("b")], + }); + + const stubs = renderHook(() => useViewScopedFileStubs()).result.current; + const files = renderHook(() => useViewScopedFiles()).result.current; + + expect(stubs.map((s) => s.id)).toEqual(["a", "b"]); + expect(stubs).toHaveLength(files.length); + }); + + it("drops stubs with no loaded file instead of returning undefined holes", () => { + setState({ + workbench: "fileEditor", + files: [file("a")], + fileStubs: [stub("a"), stub("orphan")], + }); + + const { result } = renderHook(() => useViewScopedFileStubs()); + + expect(result.current.map((s) => s.id)).toEqual(["a"]); + }); +}); diff --git a/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.ts b/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.ts index 070e854700..cec039481f 100644 --- a/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.ts +++ b/frontend/editor/src/core/hooks/tools/shared/useViewScopedFiles.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { useAllFiles } from "@app/contexts/FileContext"; import { useViewer } from "@app/contexts/ViewerContext"; import { useNavigationState } from "@app/contexts/NavigationContext"; -import { StirlingFile } from "@app/types/fileContext"; +import { StirlingFile, StirlingFileStub } from "@app/types/fileContext"; export function useViewScopedFiles(ignoreViewerScope = false): StirlingFile[] { const { activeFileIndex } = useViewer(); @@ -18,3 +18,28 @@ export function useViewScopedFiles(ignoreViewerScope = false): StirlingFile[] { return allFiles; }, [workbench, allFiles, activeFileIndex, ignoreViewerScope]); } + +/** + * The stubs for {@link useViewScopedFiles} — the same scoping rule, for UI that + * needs stub-only metadata (thumbnails, page counts) rather than file bytes. + * A tool's preview must read from here, not from the raw file list: indexing + * that list pins the preview to whichever file was added first, so it keeps + * showing the original document after the viewer switches files. + * + * Resolved by id rather than by position, because the file and stub lists are + * filtered independently — a stub exists before its bytes finish loading, so + * the two arrays can hold different indices for the same document. + */ +export function useViewScopedFileStubs( + ignoreViewerScope = false, +): StirlingFileStub[] { + const scopedFiles = useViewScopedFiles(ignoreViewerScope); + const { fileStubs } = useAllFiles(); + + return useMemo(() => { + const stubsById = new Map(fileStubs.map((stub) => [stub.id, stub])); + return scopedFiles + .map((file) => stubsById.get(file.fileId)) + .filter((stub): stub is StirlingFileStub => stub != null); + }, [scopedFiles, fileStubs]); +} From cb45fb159ab6b9c97701606afdc368af34d8fed0 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:02:18 +0200 Subject: [PATCH 002/154] fix(search): Fix PDF viewer search showing 0 of 0 results (#7228) # Description of Changes Fixes that search always showing 0 of 0 result even when there are multiple hits in the search. ### New image ### Old image On the same PDF. Changes: * Refactored the effect in `SearchInterface` to fetch the latest search state from `viewerContext` on each interval, preventing stale closure values and ensuring up-to-date search results and active index are used. * Updated the "previous" and "next" result buttons to be disabled whenever there are zero search results, rather than relying on the current index, making the UI more robust and intuitive. --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../components/viewer/SearchInterface.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/editor/src/core/components/viewer/SearchInterface.tsx b/frontend/editor/src/core/components/viewer/SearchInterface.tsx index 2c93aff17c..a32c64c33a 100644 --- a/frontend/editor/src/core/components/viewer/SearchInterface.tsx +++ b/frontend/editor/src/core/components/viewer/SearchInterface.tsx @@ -13,12 +13,15 @@ interface SearchInterfaceProps { export function SearchInterface({ visible, onClose }: SearchInterfaceProps) { const { t } = useTranslation(); const viewerContext = React.useContext(ViewerContext); + const viewerContextRef = useRef(viewerContext); + + useEffect(() => { + viewerContextRef.current = viewerContext; + }, [viewerContext]); + const inputRef = useRef(null); const searchTimeoutRef = useRef(null); - const searchState = viewerContext?.getSearchState(); - const searchResults = searchState?.results; - const searchActiveIndex = searchState?.activeIndex; const searchActions = viewerContext?.searchActions; const [searchQuery, setSearchQuery] = useState(""); const [jumpToValue, setJumpToValue] = useState(""); @@ -89,7 +92,11 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) { if (!visible) return; const checkSearchState = () => { - // Use ViewerContext state instead of window APIs + // Fetch fresh search state from ViewerContext ref to avoid closure stale values + const searchState = viewerContextRef.current?.getSearchState(); + const searchResults = searchState?.results; + const searchActiveIndex = searchState?.activeIndex; + if (searchResults && searchResults.length > 0) { const activeIndex = searchActiveIndex || 1; @@ -115,7 +122,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) { const interval = setInterval(checkSearchState, 200); return () => clearInterval(interval); - }, [visible, searchResults, searchActiveIndex, searchQuery]); + }, [visible, searchQuery]); const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { @@ -279,7 +286,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) { variant="tertiary" size="sm" onClick={handlePrevious} - disabled={!resultInfo || resultInfo.currentIndex <= 1} + disabled={!resultInfo || resultInfo.totalResults === 0} aria-label={t("viewer.search.previous", "Previous result")} > @@ -288,9 +295,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) { variant="tertiary" size="sm" onClick={handleNext} - disabled={ - !resultInfo || resultInfo.currentIndex >= resultInfo.totalResults - } + disabled={!resultInfo || resultInfo.totalResults === 0} aria-label={t("viewer.search.next", "Next result")} > From 120678260af2334fd39eeb9136184911d537d2e9 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:06:31 +0200 Subject: [PATCH 003/154] docs(contributing): update CONTRIBUTING.md to include desktop application startup command (#7211) # Description of Changes May spare some headaches down the line. Changes: - Added desktop dev command to the contributing guide --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79eb43f645..9092d5cd72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ This project uses [Task](https://taskfile.dev/) as a unified command runner. Aft 1. Install the `task` CLI: https://taskfile.dev/installation/ 2. Run `task install` to install all dependencies -3. Run `task dev` to start backend + frontend +3. Run `task dev` to start backend + frontend or `task desktop:dev` to start the desktop application 4. Run `task check` before submitting a PR Run `task --list` to see all available commands. From 921bdac4b78d303bbe48837eedeab75078272c7b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:40:41 +0100 Subject: [PATCH 004/154] Desktop installer fixes (#7174) # Description of Changes * Adds a `windows-11-arm` CI/release leg (NSIS, Microsoft JDK 25, updater keys); JPDFium natives deliberately excluded (`jpdfiumPlatforms=none`) until published, so don't ship ARM64 installers to users yet * Defaults `WEBKIT_DISABLE_DMABUF_RENDERER=1` on Linux (crash switching tools on NVIDIA) * Strips the bundled libwayland from AppImages (blank window on Fedora Wayland) * Blocks off-app webview navigation + window drop guard + close failsafe (drag-drop bricks the app) * 120s startup grace before the backend is declared unhealthy, restart success only announced after a real health check ("Backend stopped unexpectedly" spam and likely the OAuth port churn) * Verified: green `windows-arm64` build (234 MB NSIS artifact) and green Linux run with libwayland confirmed stripped * JPDFium fixes for multi threading issues --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .github/workflows/multiOSReleases.yml | 120 ++++++++++++++++-- .github/workflows/tauri-build.yml | 86 +++++++++++-- .taskfiles/desktop.yml | 1 + app/common/build.gradle | 18 ++- build.gradle | 2 +- frontend/editor/src-tauri/src/lib.rs | 82 +++++++++++- .../src/desktop/components/AppProviders.tsx | 14 ++ .../src/desktop/hooks/useExitWarning.ts | 11 +- .../desktop/services/tauriBackendService.ts | 40 +++++- 9 files changed, 335 insertions(+), 39 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 75cecb7fb5..8303cf102a 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -12,13 +12,14 @@ on: - "true" - "false" platform: - description: "Platform to build (windows, macos, linux, or all)" + description: "Platform to build (windows, windows-arm64, macos, linux, or all)" required: true default: "all" type: choice options: - all - windows + - windows-arm64 - macos - linux sign: @@ -87,24 +88,35 @@ jobs: - name: Determine build matrix id: set-matrix run: | + # windows-arm64: NSIS only (WiX MSI has no arm64 support in Tauri) and no + # JPDFium natives yet - flip to windows-arm64 once JPDFium ships them. + WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}' + WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}' + MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}' + LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}' + ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then case "${{ github.event.inputs.platform }}" in "windows") - echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT + ;; + "windows-arm64") + echo "matrix={\"include\":[$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT ;; "macos") - echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$MACOS]}" >> $GITHUB_OUTPUT ;; "linux") - echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$LINUX]}" >> $GITHUB_OUTPUT ;; *) - echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT ;; esac else # For push/release events, build all platforms - echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT fi build-jars: @@ -236,11 +248,12 @@ jobs: if: matrix.platform == 'macos-15' run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does. - name: Set up JDK 25 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: "25" - distribution: "temurin" + distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - name: Setup Gradle uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 @@ -271,7 +284,7 @@ jobs: # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker id: digicert-setup - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1 env: SM_API_KEY: ${{ secrets.SM_API_KEY }} @@ -281,7 +294,7 @@ jobs: SM_HOST: ${{ secrets.SM_HOST }} - name: Setup DigiCert KeyLocker Certificate - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} shell: pwsh run: | Write-Host "Setting up DigiCert KeyLocker environment..." @@ -316,7 +329,7 @@ jobs: # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - name: Import Windows Code Signing Certificate - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} env: WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} @@ -384,7 +397,7 @@ jobs: # Without this, signCommand failures are opaque (Tauri captures but drops # smctl's stderr) - running these loudly surfaces auth/env/keypair issues. - name: Preflight smctl - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} shell: pwsh env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -415,7 +428,7 @@ jobs: # smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD # from env (set by prior DigiCert setup step). No --config-file needed. - name: Configure Windows code signing - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} shell: bash env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -486,6 +499,54 @@ jobs: args: ${{ matrix.args }} updaterJsonKeepUniversal: true + # Bundled libwayland conflicts with the host's on some distros (Fedora + # Wayland: EGL_BAD_PARAMETER, blank window - #6878). Repack without it, + # then regenerate the updater .sig (repack invalidates the original) and + # GPG-sign again when release signing is on. + - name: Strip bundled Wayland libs from AppImage + if: matrix.platform == 'ubuntu-22.04' + continue-on-error: true + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }} + SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} + APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} + run: | + set -euo pipefail + AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1) + if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi + chmod +x "$AI" + WORK=$(mktemp -d) + (cd "$WORK" && "$AI" --appimage-extract >/dev/null) + if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then + echo "No bundled libwayland - nothing to strip" + rm -rf "$WORK" + exit 0 + fi + rm -f "$WORK/squashfs-root/usr/lib/"libwayland-* + curl -fsSL -o "$WORK/appimagetool" \ + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + # Pinned checksum: never execute an unverified downloaded binary. On + # mismatch (upstream rebuilt continuous) the step aborts and the + # original AppImage ships unchanged - update the pin deliberately. + echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c - + chmod +x "$WORK/appimagetool" + SIGN_ARGS=() + if [ "$GPG_SIGN" = "1" ] && [ -n "${SIGN_KEY:-}" ]; then + SIGN_ARGS=(--sign --sign-key "$SIGN_KEY") + fi + "$WORK/appimagetool" --appimage-extract-and-run "${SIGN_ARGS[@]}" "$WORK/squashfs-root" "$AI.new" + # Updater payload signature must match the repacked bytes. The CLI + # reads the key/password from env - never pass secrets as argv. + if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then + (cd frontend && npx tauri signer sign "$AI.new") + mv "$AI.new.sig" "$AI.sig" + fi + mv "$AI.new" "$AI" + rm -rf "$WORK" + echo "Stripped bundled libwayland from $(basename "$AI")" + - name: Clear release GPG key from runner keyring (Linux) if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') env: @@ -502,10 +563,31 @@ jobs: # artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw # cargo output unsigned, so checking it produces false negatives. - name: Verify Windows Code Signature - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} timeout-minutes: 15 shell: pwsh run: | + # arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the + # inner exe before packing and the setup exe after, so verifying the setup + # exe is the arm64 equivalent of the MSI + inner-exe check below. + if ("${{ matrix.platform }}" -eq "windows-11-arm") { + $setupExes = Get-ChildItem -Path "./frontend/editor/src-tauri/target" -Filter "*-setup.exe" -Recurse -File + if ($setupExes.Count -eq 0) { + Write-Host "[ERROR] No NSIS installer found under target/" + exit 1 + } + foreach ($exe in $setupExes) { + $sig = Get-AuthenticodeSignature -FilePath $exe.FullName + Write-Host "NSIS installer: $($exe.Name) Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] NSIS installer is not signed" + exit 1 + } + } + Write-Host "[SUCCESS] NSIS installer is properly signed" + exit 0 + } + $allSigned = $true # Check MSI installer (outer wrapper - what users download) @@ -569,7 +651,7 @@ jobs: # but drops stderr when the command exits non-zero, making failures opaque. # The real errors live in smctl's log files - surface them here for debugging. - name: Dump smctl logs on failure - if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }} + if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }} shell: pwsh run: | $logDir = "$env:USERPROFILE\.signingmanager\logs" @@ -605,6 +687,11 @@ jobs: if [ "${{ matrix.platform }}" = "windows-latest" ]; then find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \; find . -name "*.msi.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi.sig" \; + elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then + # arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri). + # The setup exe is also its own updater payload (-> sibling .sig). + find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \; + find . -name "*-setup.exe.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe.sig" \; elif [ "${{ matrix.platform }}" = "macos-15" ]; then # DMG = manual install; .app.tar.gz (+ .sig) = updater payload. # Raw .app is intentionally not shipped (hundreds of MB of uncompressed input). @@ -719,6 +806,10 @@ jobs: 'bundles': ['Stirling-PDF-windows-x86_64.msi'], 'targets': ['windows-x86_64-msi', 'windows-x86_64'], }, + { + 'bundles': ['Stirling-PDF-windows-arm64-setup.exe'], + 'targets': ['windows-aarch64-nsis', 'windows-aarch64'], + }, { 'bundles': ['Stirling-PDF-macos-universal.app.tar.gz'], 'targets': ['darwin-x86_64', 'darwin-aarch64'], @@ -817,6 +908,7 @@ jobs: files: | ./artifacts/**/*.jar ./artifacts/**/*.msi + ./artifacts/**/*-setup.exe ./artifacts/**/*.dmg ./artifacts/**/*.app.tar.gz ./artifacts/**/*.deb diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index e504826d35..415e8be4e5 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -12,7 +12,7 @@ on: workflow_call: inputs: platform: - description: "Platform to build (windows, macos, linux, windows-macos, or all)." + description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)." required: false type: string default: "all" @@ -29,13 +29,14 @@ on: workflow_dispatch: inputs: platform: - description: "Platform to build (windows, macos, linux, windows-macos, or all)" + description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)" required: true default: "all" type: choice options: - all - windows + - windows-arm64 - macos - linux - windows-macos @@ -73,15 +74,19 @@ jobs: PLATFORM: ${{ inputs.platform }} run: | WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}' + # ARM64: NSIS only (WiX MSI has no arm64 support in Tauri) and no JPDFium + # natives yet - flip jpdfium_platforms to windows-arm64 once JPDFium ships it. + WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}' MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}' LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}' case "$PLATFORM" in - windows) ENTRIES=("$WINDOWS") ;; + windows) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64") ;; + windows-arm64) ENTRIES=("$WINDOWS_ARM64") ;; macos) ENTRIES=("$MACOS") ;; linux) ENTRIES=("$LINUX") ;; windows-macos) ENTRIES=("$WINDOWS" "$MACOS") ;; - *) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;; + *) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64" "$MACOS" "$LINUX") ;; esac # Drop macOS entries when Apple certificate secret is unavailable @@ -167,11 +172,12 @@ jobs: if: matrix.platform == 'macos-15' run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does. - name: Set up JDK 25 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: "25" - distribution: "temurin" + distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - name: Setup Gradle uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 @@ -203,7 +209,7 @@ jobs: # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker id: digicert-setup - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1 env: SM_API_KEY: ${{ secrets.SM_API_KEY }} @@ -213,7 +219,7 @@ jobs: SM_HOST: ${{ secrets.SM_HOST }} - name: Setup DigiCert KeyLocker Certificate - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: pwsh run: | Write-Host "Setting up DigiCert KeyLocker environment..." @@ -248,7 +254,7 @@ jobs: # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - name: Import Windows Code Signing Certificate - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }} env: WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} @@ -323,7 +329,7 @@ jobs: ls -la /usr/bin/hd* || echo "No hd* tools found" - name: Preflight smctl - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: pwsh env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -336,7 +342,7 @@ jobs: if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" } - name: Configure Windows code signing - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: bash env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -452,6 +458,37 @@ jobs: tauriScript: npx tauri args: --bundles appimage + # Bundled libwayland conflicts with the host's on some distros (Fedora + # Wayland: EGL_BAD_PARAMETER, blank window - #6878). The AppImage + # ecosystem excludelist agrees these libs must come from the system. + - name: Strip bundled Wayland libs from AppImage + if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal + continue-on-error: true + run: | + set -euo pipefail + AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1) + if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi + chmod +x "$AI" + WORK=$(mktemp -d) + (cd "$WORK" && "$AI" --appimage-extract >/dev/null) + if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then + echo "No bundled libwayland - nothing to strip" + rm -rf "$WORK" + exit 0 + fi + rm -f "$WORK/squashfs-root/usr/lib/"libwayland-* + curl -fsSL -o "$WORK/appimagetool" \ + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + # Pinned checksum: never execute an unverified downloaded binary. On + # mismatch (upstream rebuilt continuous) the step aborts and the + # original AppImage ships unchanged - update the pin deliberately. + echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c - + chmod +x "$WORK/appimagetool" + "$WORK/appimagetool" --appimage-extract-and-run "$WORK/squashfs-root" "$AI.new" + mv "$AI.new" "$AI" + rm -rf "$WORK" + echo "Stripped bundled libwayland from $(basename "$AI")" + - name: Clear release GPG key from runner keyring (Linux) if: always() && inputs.sign && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main' env: @@ -490,6 +527,9 @@ jobs: # Only ship the MSI installer. The loose exe and WiX toolset exes # are not the user-facing installer - the MSI contains the signed inner exe. find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \; + elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then + # arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri). + find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \; elif [ "${{ matrix.platform }}" = "macos-15" ]; then find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \; else @@ -501,9 +541,28 @@ jobs: # Verify the MSI AND the inner exe extracted from it are signed. # The inner exe is what gets installed on users' machines and what AV scans. - name: Verify Windows Code Signature - if: inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' + if: inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' shell: pwsh run: | + # arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the + # inner exe before packing and the setup exe after, so verifying the setup + # exe is the arm64 equivalent of the MSI + inner-exe check below. + if ("${{ matrix.platform }}" -eq "windows-11-arm") { + $exePath = "./dist/Stirling-PDF-${{ matrix.name }}-setup.exe" + if (-not (Test-Path $exePath)) { + Write-Host "[ERROR] NSIS installer not found at $exePath" + exit 1 + } + $sig = Get-AuthenticodeSignature -FilePath $exePath + Write-Host "NSIS installer: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] NSIS installer is not signed" + exit 1 + } + Write-Host "[SUCCESS] NSIS installer is properly signed" + exit 0 + } + $allSigned = $true $msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi" @@ -549,7 +608,7 @@ jobs: Write-Host "[SUCCESS] MSI and inner exe are properly signed" - name: Dump smctl logs on failure - if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }} + if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }} shell: pwsh run: | $logDir = "$env:USERPROFILE\.signingmanager\logs" @@ -576,7 +635,7 @@ jobs: cd ./frontend/editor/src-tauri/target # Check for expected artifacts based on platform - if [ "${{ matrix.platform }}" = "windows-latest" ]; then + if [ "${{ matrix.platform }}" = "windows-latest" ] || [ "${{ matrix.platform }}" = "windows-11-arm" ]; then echo "Checking for Windows artifacts..." find . -name "*.exe" -o -name "*.msi" | head -5 if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then @@ -655,6 +714,7 @@ jobs: // Map of expected artifact names to display info const artifactMap = { 'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' }, + 'Stirling-PDF-windows-arm64': { icon: '🪟', platform: 'Windows ARM64', files: '-setup.exe (NSIS)' }, 'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' }, 'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' } }; diff --git a/.taskfiles/desktop.yml b/.taskfiles/desktop.yml index c80196305b..27f8dafde0 100644 --- a/.taskfiles/desktop.yml +++ b/.taskfiles/desktop.yml @@ -22,6 +22,7 @@ vars: linux-amd64) echo "linux-x64";; linux-arm64) echo "linux-arm64";; windows-amd64) echo "windows-x64";; + windows-arm64) echo "none";; # no JPDFium windows-arm64 natives published yet *) echo "all";; esac fi diff --git a/app/common/build.gradle b/app/common/build.gradle index 71ccbf236f..73be441940 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -36,18 +36,24 @@ dependencies { api "com.stirling:jpdfium:${jpdfiumVersion}" - // -PjpdfiumPlatforms=all| + // -PjpdfiumPlatforms=all|none| + // 'none' skips natives entirely (windows-arm64 builds, until JPDFium ships that platform). def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim() def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64'] - def jpdfiumPlatforms = jpdfiumPlatformsProp == 'all' - ? jpdfiumAllPlatforms - : jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it } + def jpdfiumPlatforms + if (jpdfiumPlatformsProp == 'all') { + jpdfiumPlatforms = jpdfiumAllPlatforms + } else if (jpdfiumPlatformsProp == 'none') { + jpdfiumPlatforms = [] + } else { + jpdfiumPlatforms = jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it } + } def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) } if (jpdfiumInvalid) { throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " + - "Valid: ${jpdfiumAllPlatforms.join(', ')} or 'all'.") + "Valid: ${jpdfiumAllPlatforms.join(', ')}, 'all' or 'none'.") } - logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}") + logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms ? jpdfiumPlatforms.join(', ') : 'none'}") jpdfiumPlatforms.each { platform -> runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}" } diff --git a/build.gradle b/build.gradle index 38fab663f6..2af5522a73 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ ext { bucket4jVersion = "8.19.0" archunitVersion = "1.4.2" batikVersion = "1.19" - jpdfiumVersion = "1.0.2" + jpdfiumVersion = "1.0.4" jwtVersion = "0.13.0" awsSdkVersion = "2.44.12" jschVersion = "0.2.23" diff --git a/frontend/editor/src-tauri/src/lib.rs b/frontend/editor/src-tauri/src/lib.rs index 8855604624..16810649fd 100644 --- a/frontend/editor/src-tauri/src/lib.rs +++ b/frontend/editor/src-tauri/src/lib.rs @@ -71,9 +71,43 @@ fn parse_launch_files(args: &[String]) -> Vec { .collect() } +// URLs the webview is allowed to show: the bundled app and dev server only. +// Anything else (file://, https://...) must never replace the app UI. +fn is_app_url(url: &tauri::Url) -> bool { + match url.scheme() { + "tauri" | "about" | "blob" | "data" => true, + "http" | "https" => matches!( + url.host_str(), + Some("tauri.localhost") | Some("localhost") | Some("127.0.0.1") + ), + _ => false, + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + // WebKitGTK's DMA-BUF renderer crashes the web process on NVIDIA and some + // Wayland stacks (blank window, app dying on tool switch). Opt out unless overridden. + #[cfg(target_os = "linux")] + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + tauri::Builder::default() + .plugin( + // Dropping a file outside a dropzone makes WebKit navigate the webview to + // that file, killing the app UI and its close handler (window becomes + // unclosable). Block every off-app navigation at the Rust layer. + tauri::plugin::Builder::::new("navigation-guard") + .on_navigation(|_webview, url| { + let allowed = is_app_url(url); + if !allowed { + add_log(format!("🚫 Blocked webview navigation to: {}", url)); + } + allowed + }) + .build() + ) .plugin( tauri_plugin_log::Builder::new() .level(log::LevelFilter::Info) @@ -209,10 +243,20 @@ pub fn run() { // Use Tauri's built-in cleanup app_handle.cleanup_before_exit(); } - RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => { + RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, label, .. } => { add_log("🔄 Window close requested (will cleanup on actual exit)...".to_string()); // Don't cleanup here - let JavaScript handler prevent close if needed // Backend cleanup happens in ExitRequested when window actually closes + // + // Failsafe: if the webview somehow left the app (JS close handler gone, + // window would stay open forever), destroy the window directly. + if let Some(window) = app_handle.get_webview_window(&label) { + let off_app = window.url().map(|u| !is_app_url(&u)).unwrap_or(false); + if off_app { + add_log(format!("🚨 Webview '{}' is off-app, destroying window directly", label)); + let _ = window.destroy(); + } + } } RunEvent::WindowEvent { event: WindowEvent::DragDrop(drag_drop_event), label, .. } => { use tauri::DragDropEvent; @@ -267,3 +311,39 @@ pub fn run() { } }); } + +#[cfg(test)] +mod tests { + use super::is_app_url; + + fn allows(raw: &str) -> bool { + is_app_url(&tauri::Url::parse(raw).expect("valid url")) + } + + #[test] + fn allows_bundled_app_and_dev_server() { + assert!(allows("tauri://localhost/index.html")); + assert!(allows("http://tauri.localhost/")); + assert!(allows("http://localhost:5173/")); + assert!(allows("http://127.0.0.1:8080/api")); + assert!(allows("about:blank")); + assert!(allows("blob:http://localhost:5173/abc")); + assert!(allows("data:text/html,hi")); + } + + #[test] + fn blocks_dropped_files() { + // The #6872 lockup: webview navigating to a dropped PDF. + assert!(!allows("file:///C:/Users/me/report.pdf")); + assert!(!allows("file:///home/me/report.pdf")); + } + + #[test] + fn blocks_remote_origins() { + assert!(!allows("https://example.com/")); + assert!(!allows("http://evil.test/")); + // Look-alike hosts must not slip past the allowlist. + assert!(!allows("https://localhost.evil.test/")); + assert!(!allows("https://nottauri.localhost.evil.test/")); + } +} diff --git a/frontend/editor/src/desktop/components/AppProviders.tsx b/frontend/editor/src/desktop/components/AppProviders.tsx index 69c258b4f6..5b00c1f4f5 100644 --- a/frontend/editor/src/desktop/components/AppProviders.tsx +++ b/frontend/editor/src/desktop/components/AppProviders.tsx @@ -62,6 +62,20 @@ export function AppProviders({ children }: { children: ReactNode }) { const [appKey, setAppKey] = useState(0); const hasLoadedInitialMode = useRef(false); + // Files dropped outside a dropzone must never navigate the webview to the + // file (Linux WebKit renders the PDF fullscreen and orphans the app UI). + // Dropzone-level handlers run before these window-level listeners, so + // in-app drag & drop is unaffected. + useEffect(() => { + const preventNavigation = (e: DragEvent) => e.preventDefault(); + window.addEventListener("dragover", preventNavigation); + window.addEventListener("drop", preventNavigation); + return () => { + window.removeEventListener("dragover", preventNavigation); + window.removeEventListener("drop", preventNavigation); + }; + }, []); + // Load connection mode on mount and subscribe to future changes useEffect(() => { void connectionModeService.getCurrentMode().then((mode) => { diff --git a/frontend/editor/src/desktop/hooks/useExitWarning.ts b/frontend/editor/src/desktop/hooks/useExitWarning.ts index 6f19125d17..1e6b4423fc 100644 --- a/frontend/editor/src/desktop/hooks/useExitWarning.ts +++ b/frontend/editor/src/desktop/hooks/useExitWarning.ts @@ -21,8 +21,6 @@ export function useExitWarning() { const handleCloseRequested = async (event: { preventDefault: () => void; }) => { - event.preventDefault(); - if (isClosingRef.current) { return; } @@ -30,6 +28,15 @@ export function useExitWarning() { const allStubs = selectorsRef.current.getStirlingFileStubs(); const dirtyStubs = allStubs.filter((stub) => stub.isDirty); + // Nothing unsaved: don't preventDefault, so the window closes natively + // without depending on the JS dialog/destroy round-trip below. + if (dirtyStubs.length === 0) { + isClosingRef.current = true; + return; + } + + event.preventDefault(); + if (dirtyStubs.length > 0) { const fileList = dirtyStubs.map((f) => `• ${f.name}`).join("\n"); const saveLabel = t("confirmCloseSave", "Save and close"); diff --git a/frontend/editor/src/desktop/services/tauriBackendService.ts b/frontend/editor/src/desktop/services/tauriBackendService.ts index dd1af3d96b..31ed0119c5 100644 --- a/frontend/editor/src/desktop/services/tauriBackendService.ts +++ b/frontend/editor/src/desktop/services/tauriBackendService.ts @@ -18,6 +18,11 @@ export class TauriBackendService { private isRecovering = false; private restartAttempts = 0; private static readonly MAX_RESTART_ATTEMPTS = 3; + /** Failed health checks before the first-ever success are "starting", not "unhealthy" - + * a slow first boot must not trigger the restart/notification loop. */ + private hasBeenHealthy = false; + private startupGraceUntil = 0; + private static readonly STARTUP_GRACE_MS = 120_000; static getInstance(): TauriBackendService { if (!TauriBackendService.instance) { @@ -115,9 +120,18 @@ export class TauriBackendService { // Reset started flag so startBackend() will run again this.backendStarted = false; this.startPromise = null; + // Fresh grace window: the restarted backend needs boot time before failed + // health checks may count as unhealthy again. + this.hasBeenHealthy = false; this.setStatus("starting"); try { await this.startBackend(); + // startBackend resolves once the port is known, not once Spring is up - + // only declare success after a real health check passes. + const healthy = await this.waitUntilHealthy(60_000); + if (!healthy) { + throw new Error("Backend did not become healthy after restart"); + } this.restartAttempts = 0; // Reset on successful restart this.isRecovering = false; console.log("[TauriBackendService] Backend restarted successfully."); @@ -156,6 +170,7 @@ export class TauriBackendService { } this.backendStarted = true; // Mark as active for health checks + this.startupGraceUntil = Date.now() + TauriBackendService.STARTUP_GRACE_MS; this.setStatus("starting"); this.beginHealthMonitoring(); @@ -176,6 +191,7 @@ export class TauriBackendService { return this.startPromise; } + this.startupGraceUntil = Date.now() + TauriBackendService.STARTUP_GRACE_MS; this.setStatus("starting"); this.startPromise = invoke("start_backend", { backendUrl }) @@ -260,7 +276,7 @@ export class TauriBackendService { console.warn( `[TauriBackendService] Health check failed: ${response.status}`, ); - this.setStatus("unhealthy"); + this.setStatus(this.isInStartupGrace() ? "starting" : "unhealthy"); return false; } @@ -270,15 +286,22 @@ export class TauriBackendService { `[TauriBackendService] dependenciesReady=${dependenciesReady}`, ); + if (dependenciesReady) { + this.hasBeenHealthy = true; + } this.setStatus(dependenciesReady ? "healthy" : "starting"); return dependenciesReady; } catch (error) { console.error("[TauriBackendService] Health check error:", error); - this.setStatus("unhealthy"); + this.setStatus(this.isInStartupGrace() ? "starting" : "unhealthy"); return false; } } + private isInStartupGrace(): boolean { + return !this.hasBeenHealthy && Date.now() < this.startupGraceUntil; + } + private async waitForHealthy(): Promise { while (true) { const isHealthy = await this.checkBackendHealth(); @@ -289,6 +312,17 @@ export class TauriBackendService { } } + private async waitUntilHealthy(timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await this.checkBackendHealth()) { + return true; + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + return false; + } + /** * Reset backend state (used when switching from external to local backend) */ @@ -298,6 +332,8 @@ export class TauriBackendService { this.isLocalBackend = false; this.isRecovering = false; this.restartAttempts = 0; + this.hasBeenHealthy = false; + this.startupGraceUntil = 0; if (this.recoveryTimer) { clearTimeout(this.recoveryTimer); this.recoveryTimer = null; From 030f9f541eb4c3c533586221a1fa51f32dbbd5fe Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:08:06 +0100 Subject: [PATCH 005/154] feat(editor): adopt TanStack Query, convert three read-only fetches (#7264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes ## The problem The editor has no query client. ~295 `apiClient` call sites, each mount refetching what the last one just got, and three module-level caches reimplementing dedupe, retry and invalidation by hand — each shaped differently. The Processor (`frontend/editor/src/portal`) has run on TanStack Query since #7135. The editor never got it. ## End state The editor has a query client, and the three read-only fetch sites that convert safely now use it. `@tanstack/react-query` is already a dependency — no new package. **Foundation** | File | | |---|---| | `core/query/queryClient.ts` | `baseQueryOptions` + client factory. The portal now builds its client from the same options. `networkMode: "always"` — `navigator.onLine` describes internet reachability, which says nothing about a bundled backend on 127.0.0.1 or a self-hosted server on the LAN. | | `core/query/keys.ts` | `["editor", resource, ...params]` | | `core/query/staleTime.ts` + `desktop/query/staleTime.ts` | Config staleTime: `Infinity` on web, 5 min on desktop | | `core/api/config.ts`, `core/api/users.ts` | Fetch functions, mirroring `portal/api/*` | | `core/tests/utils/TestQueryProvider.tsx` | | | `desktop/components/DesktopQueryCacheReset.tsx` | | `QueryClientProvider` mounts at the top of `core/components/AppProviders.tsx`. That diff looks large but is one wrapper plus the reindent underneath it. **Converted.** All three keep their existing return shape, so no consumer changes. | | Before | |---|---| | `useFooterInfo` | Fetched twice — Footer and admin legal section | | `useGroupEnabled` | Refetched on every mount | | `UserSelector` | Refetched the whole roster on each of two mount sites, and again whenever `t` or `user` changed identity | **Desktop needs more than the provider.** `operationRouter` resolves the same relative path to the local bundled backend, a self-hosted server, or the SaaS backend. Query caches by key, not by resolved URL, so a cached entry can outlive the backend that filled it. `group-enabled` routes this way, so this PR introduces the hazard and carries the fix: `DesktopQueryCacheReset` calls `resetQueries()` when the connection mode changes or the self-hosted server goes up or down, and `CONFIG_STALE_TIME` is finite on desktop as a backstop. **Behaviour changes** - All three sites now retry once on failure (client default). None retried before, so a failing request sits in `loading` for one extra attempt plus backoff. - `staleTime: Infinity` on web means admin edits to legal links no longer appear on remount within a session. Saving those already prompts a restart, so this is accepted rather than incidental. - Desktop `useGroupEnabled` shows the *translated* offline reason on first render. The old code showed raw English for one render. - `UserSelector` drops three `console.log`s that were dumping user records to the console. ## Decisions **1. The foundation doesn't ship alone.** A provider nothing consumes gives a reviewer nothing to react to and rots if the follow-up stalls, so it lands with the cheapest safe conversions. **2. Hooks keep their existing return shape.** The alternative is switching to `{ data, isPending, error }` and updating consumers now. Cost of my choice: we carry a `loading`-shaped façade indefinitely, and consumers don't get `isFetching`/`refetch` without a second pass. Taken because it's what keeps each later migration a one-file diff. **3. Shared defaults, separate instances.** The editor and the Processor mount as *sibling* routes, not nested — they never coexist in one tree. Both clients now come from the same `baseQueryOptions`, so behaviour can't drift. A single shared instance would only buy cache surviving navigation between the two products, which is worth little while they share no keys, and it breaks the contract three portal tests rely on (`createPortalQueryClient()` returning a fresh client per test). That belongs in the collapse PR. Consequence meanwhile: the desktop reset covers the editor client only — harmless, since the portal isn't in desktop builds. **4. The desktop reset is wholesale.** A mode switch already remounts the SaaS provider tree, so there's nothing to preserve, and an allowlist of "mode-sensitive" keys would be a trap every new query has to remember to join. ## Coming next Ordered by consumers per line changed. | PR | Scope | |---|---| | 2 | `AppConfigContext` + `useEndpointConfig` — ~80 consumers, deletes ~200 lines of hand-rolled cache, retry and dedupe | | 3 | `useAdminSettings` (20 consumers) and the config sections | | 4 | Polling loops → `refetchInterval` | | 5 | Finish the Processor's remaining files, collapse to one client | | 6 | Tool execution — mutation state only, narrowly scoped | Not in scope, deliberately: `usePdfLibLinks` (its cache is a refcounted ArrayBuffer lifetime manager), thumbnail hooks, watched-folder IndexedDB reads, the desktop health monitors. Unifying `endpointAvailabilityService` / `saasAppConfigService` with the query cache would mean handing `operationRouter` a query client — its own PR if a second reason appears. ## Testing `task frontend:check` green: 1666 tests across 191 files, typecheck on all five flavours, eslint `--max-warnings=0`, dpdm, prettier. New tests cover request de-duplication, per-group key isolation, the desktop offline short-circuit, and the cache reset. The reset test was verified to fail against the `clear()` implementation it replaced. `UserSelector` has no test beyond its existing stories. One existing test needed a wrapper: `Login.test.tsx` renders `` in isolation, and `AuthLayout` → `Footer` → `useFooterInfo` now needs a client. The real `/login` route is already inside `AppProviders`, so this is test isolation, not a runtime gap. Rollback is a clean revert — nothing persists outside the React tree. --- frontend/editor/src/core/api/config.ts | 32 +++++ frontend/editor/src/core/api/users.ts | 8 ++ .../src/core/components/AppProviders.tsx | 133 ++++++++--------- .../core/components/shared/UserSelector.tsx | 106 ++++++-------- .../src/core/hooks/useFooterInfo.test.tsx | 56 ++++++++ .../editor/src/core/hooks/useFooterInfo.ts | 64 +++------ .../src/core/hooks/useGroupEnabled.test.tsx | 71 ++++++++++ .../editor/src/core/hooks/useGroupEnabled.ts | 46 ++---- frontend/editor/src/core/query/keys.ts | 6 + frontend/editor/src/core/query/queryClient.ts | 15 ++ frontend/editor/src/core/query/staleTime.ts | 2 + .../core/tests/utils/TestQueryProvider.tsx | 13 ++ .../src/desktop/components/AppProviders.tsx | 4 + .../DesktopQueryCacheReset.test.tsx | 134 ++++++++++++++++++ .../components/DesktopQueryCacheReset.tsx | 39 +++++ .../desktop/hooks/useGroupEnabled.test.tsx | 64 +++++++++ .../src/desktop/hooks/useGroupEnabled.ts | 70 +++++---- .../editor/src/desktop/query/staleTime.ts | 5 + frontend/editor/src/portal/PortalApp.tsx | 2 - frontend/editor/src/portal/queryClient.ts | 28 +--- .../src/proprietary/routes/Login.test.tsx | 12 +- 21 files changed, 638 insertions(+), 272 deletions(-) create mode 100644 frontend/editor/src/core/api/config.ts create mode 100644 frontend/editor/src/core/api/users.ts create mode 100644 frontend/editor/src/core/hooks/useFooterInfo.test.tsx create mode 100644 frontend/editor/src/core/hooks/useGroupEnabled.test.tsx create mode 100644 frontend/editor/src/core/query/keys.ts create mode 100644 frontend/editor/src/core/query/queryClient.ts create mode 100644 frontend/editor/src/core/query/staleTime.ts create mode 100644 frontend/editor/src/core/tests/utils/TestQueryProvider.tsx create mode 100644 frontend/editor/src/desktop/components/DesktopQueryCacheReset.test.tsx create mode 100644 frontend/editor/src/desktop/components/DesktopQueryCacheReset.tsx create mode 100644 frontend/editor/src/desktop/hooks/useGroupEnabled.test.tsx create mode 100644 frontend/editor/src/desktop/query/staleTime.ts diff --git a/frontend/editor/src/core/api/config.ts b/frontend/editor/src/core/api/config.ts new file mode 100644 index 0000000000..61f74b2330 --- /dev/null +++ b/frontend/editor/src/core/api/config.ts @@ -0,0 +1,32 @@ +import apiClient from "@app/services/apiClient"; + +export interface FooterInfo { + analyticsEnabled?: boolean; + termsAndConditions?: string; + privacyPolicy?: string; + accessibilityStatement?: string; + cookiePolicy?: string; + impressum?: string; +} + +/** Public — no authentication required. */ +export async function fetchFooterInfo(): Promise { + try { + const response = await apiClient.get( + "/api/v1/ui-data/footer-info", + { suppressErrorToast: true }, + ); + return response.data; + } catch (error) { + // Toasts are suppressed here, so the failure would otherwise be silent. + console.error("[api/config] footer-info failed:", error); + throw error; + } +} + +export async function fetchGroupEnabled(group: string): Promise { + const response = await apiClient.get( + `/api/v1/config/group-enabled?group=${encodeURIComponent(group)}`, + ); + return response.data; +} diff --git a/frontend/editor/src/core/api/users.ts b/frontend/editor/src/core/api/users.ts new file mode 100644 index 0000000000..851757d8db --- /dev/null +++ b/frontend/editor/src/core/api/users.ts @@ -0,0 +1,8 @@ +import apiClient from "@app/services/apiClient"; +import { UserSummary } from "@app/types/signingSession"; + +export async function fetchUsers(): Promise { + const response = await apiClient.get("/api/v1/user/users"); + // A proxy can answer 200 with an HTML login page; callers assume an array. + return Array.isArray(response.data) ? response.data : []; +} diff --git a/frontend/editor/src/core/components/AppProviders.tsx b/frontend/editor/src/core/components/AppProviders.tsx index f050ae7f9b..01b2e42a82 100644 --- a/frontend/editor/src/core/components/AppProviders.tsx +++ b/frontend/editor/src/core/components/AppProviders.tsx @@ -1,4 +1,6 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode, useEffect, useState } from "react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { createAppQueryClient } from "@app/query/queryClient"; import { ThemeProvider } from "@app/components/shared/ThemeProvider"; import { FileContextProvider } from "@app/contexts/FileContext"; import { NavigationProvider } from "@app/contexts/NavigationContext"; @@ -119,70 +121,73 @@ export function AppProviders({ appConfigRetryOptions, appConfigProviderProps, }: AppProvidersProps) { + const [queryClient] = useState(createAppQueryClient); return ( - - - - - - - - - - {/* Auto-popup on startup when a newer Stirling-PDF release is available. - No-ops inside Tauri — the desktop popup handles that flow. */} - - + + + + + - - - - - - - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* Auto-popup on startup when a newer Stirling-PDF release is available. + No-ops inside Tauri — the desktop popup handles that flow. */} + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/editor/src/core/components/shared/UserSelector.tsx b/frontend/editor/src/core/components/shared/UserSelector.tsx index 57d2a0dd76..5c25728e4a 100644 --- a/frontend/editor/src/core/components/shared/UserSelector.tsx +++ b/frontend/editor/src/core/components/shared/UserSelector.tsx @@ -1,12 +1,13 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useQuery } from "@tanstack/react-query"; import { MultiSelect, Loader, Text, Stack } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { useNavigate } from "react-router-dom"; import { alert } from "@app/components/toast"; -import { UserSummary } from "@app/types/signingSession"; -import apiClient from "@app/services/apiClient"; +import { fetchUsers } from "@app/api/users"; import { useAuth } from "@app/auth/UseSession"; +import { qk } from "@app/query/keys"; import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; interface UserSelectorProps { @@ -30,71 +31,51 @@ const UserSelector = ({ const { t } = useTranslation(); const { user } = useAuth(); const navigate = useNavigate(); - const [selectData, setSelectData] = useState([]); - const [loading, setLoading] = useState(true); const [stringValue, setStringValue] = useState([]); + const { + data: users, + isPending: loading, + error, + } = useQuery({ queryKey: qk.users(), queryFn: fetchUsers }); + useEffect(() => { - const fetchUsers = async () => { - try { - const response = await apiClient.get("/api/v1/user/users"); - console.log("Users API response:", response.data); - const fetchedUsers = response.data || []; + if (!error) return; + alert({ + alertType: "error", + title: t("common.error"), + body: t("certSign.collab.userSelector.loadError", "Failed to load users"), + }); + }, [error, t]); - // Process selectData inside useEffect - group by team - const usersByTeam: Record = {}; - const currentUserId = user?.id ? parseInt(user.id, 10) : null; + const selectData = useMemo(() => { + const usersByTeam: Record = {}; + const currentUserId = user?.id ? parseInt(user.id, 10) : null; - fetchedUsers - .filter((u: UserSummary) => u && u.userId && u.username) - .filter((u: UserSummary) => u.userId !== currentUserId) // Exclude current user - .filter((u: UserSummary) => u.teamName?.toLowerCase() !== "internal") // Exclude internal users - .forEach((user: UserSummary) => { - const teamName = - user.teamName || - t("certSign.collab.userSelector.noTeam", "No Team"); - if (!usersByTeam[teamName]) { - usersByTeam[teamName] = []; - } - const displayName = user.displayName || user.username || "Unknown"; - const username = user.username || "unknown"; - const label = - displayName !== username - ? `${displayName} (@${username})` - : displayName; - usersByTeam[teamName].push({ - value: String(user.userId), - label, - }); - }); + (users ?? []) + .filter((u) => u && u.userId && u.username) + .filter((u) => u.userId !== currentUserId) + .filter((u) => u.teamName?.toLowerCase() !== "internal") + .forEach((u) => { + const teamName = + u.teamName || t("certSign.collab.userSelector.noTeam", "No Team"); + if (!usersByTeam[teamName]) { + usersByTeam[teamName] = []; + } + const displayName = u.displayName || u.username || "Unknown"; + const username = u.username || "unknown"; + const label = + displayName !== username + ? `${displayName} (@${username})` + : displayName; + usersByTeam[teamName].push({ value: String(u.userId), label }); + }); - // Convert to Mantine's grouped format - const processed: GroupedData[] = Object.entries(usersByTeam).map( - ([teamName, items]) => ({ - group: teamName, - items: items.sort((a, b) => a.label.localeCompare(b.label)), - }), - ); - - console.log("Processed selectData:", processed); - setSelectData(processed); - } catch (error) { - console.error("Failed to load users:", error); - alert({ - alertType: "error", - title: t("common.error"), - body: t( - "certSign.collab.userSelector.loadError", - "Failed to load users", - ), - }); - } finally { - setLoading(false); - } - }; - - fetchUsers(); - }, [t, user]); + return Object.entries(usersByTeam).map(([teamName, items]) => ({ + group: teamName, + items: items.sort((a, b) => a.label.localeCompare(b.label)), + })); + }, [users, user, t]); // Process stringValue when value prop changes useEffect(() => { @@ -102,7 +83,6 @@ const UserSelector = ({ const result = safeValue .map((id) => (id != null ? id.toString() : "")) .filter(Boolean); - console.log("stringValue for MultiSelect:", result); setStringValue(result); }, [value]); diff --git a/frontend/editor/src/core/hooks/useFooterInfo.test.tsx b/frontend/editor/src/core/hooks/useFooterInfo.test.tsx new file mode 100644 index 0000000000..e269d8c0f7 --- /dev/null +++ b/frontend/editor/src/core/hooks/useFooterInfo.test.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider"; +import { useFooterInfo } from "@app/hooks/useFooterInfo"; +import { fetchFooterInfo } from "@app/api/config"; + +vi.mock("@app/api/config", () => ({ fetchFooterInfo: vi.fn() })); + +const mockFetch = vi.mocked(fetchFooterInfo); + +describe("useFooterInfo", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns the server's footer config", async () => { + mockFetch.mockResolvedValue({ + analyticsEnabled: true, + privacyPolicy: "/privacy", + }); + + const { result } = renderHook(() => useFooterInfo(), { + wrapper: TestQueryProvider, + }); + + expect(result.current.loading).toBe(true); + await waitFor(() => expect(result.current.loading).toBe(false)); + expect(result.current.footerInfo).toEqual({ + analyticsEnabled: true, + privacyPolicy: "/privacy", + }); + }); + + it("falls back to analytics-off rather than null when the fetch fails", async () => { + mockFetch.mockRejectedValue(new Error("offline")); + + const { result } = renderHook(() => useFooterInfo(), { + wrapper: TestQueryProvider, + }); + + await waitFor(() => expect(result.current.error).toBeTruthy()); + expect(result.current.footerInfo).toEqual({ analyticsEnabled: false }); + }); + + it("shares one request between the footer and the legal section", async () => { + mockFetch.mockResolvedValue({ analyticsEnabled: false }); + + const { result } = renderHook( + () => ({ footer: useFooterInfo(), legal: useFooterInfo() }), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => expect(result.current.footer.loading).toBe(false)); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/editor/src/core/hooks/useFooterInfo.ts b/frontend/editor/src/core/hooks/useFooterInfo.ts index 4e0ddca2eb..021c02e0d5 100644 --- a/frontend/editor/src/core/hooks/useFooterInfo.ts +++ b/frontend/editor/src/core/hooks/useFooterInfo.ts @@ -1,50 +1,24 @@ -import { useState, useEffect } from "react"; -import apiClient from "@app/services/apiClient"; +import { useQuery } from "@tanstack/react-query"; +import { fetchFooterInfo, type FooterInfo } from "@app/api/config"; +import { qk } from "@app/query/keys"; +import { CONFIG_STALE_TIME } from "@app/query/staleTime"; -export interface FooterInfo { - analyticsEnabled?: boolean; - termsAndConditions?: string; - privacyPolicy?: string; - accessibilityStatement?: string; - cookiePolicy?: string; - impressum?: string; -} +export type { FooterInfo }; -/** - * Hook to fetch public footer configuration data. - * This endpoint is always accessible without authentication. - */ +const FALLBACK: FooterInfo = { analyticsEnabled: false }; + +/** Public footer config, shared by Footer and the admin legal section. */ export function useFooterInfo() { - const [footerInfo, setFooterInfo] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const { data, isPending, error } = useQuery({ + queryKey: qk.footerInfo(), + queryFn: fetchFooterInfo, + staleTime: CONFIG_STALE_TIME, + }); - useEffect(() => { - const fetchFooterInfo = async () => { - try { - setLoading(true); - const response = await apiClient.get( - "/api/v1/ui-data/footer-info", - { - suppressErrorToast: true, - } as any, - ); - setFooterInfo(response.data); - setError(null); - } catch (err) { - console.error("[useFooterInfo] Failed to fetch footer info:", err); - setError(err as Error); - // Set defaults on error - setFooterInfo({ - analyticsEnabled: false, - }); - } finally { - setLoading(false); - } - }; - - fetchFooterInfo(); - }, []); - - return { footerInfo, loading, error }; + return { + // Callers render legal links off this, so a failure must still yield an object. + footerInfo: data ?? (error ? FALLBACK : null), + loading: isPending, + error: (error as Error | null) ?? null, + }; } diff --git a/frontend/editor/src/core/hooks/useGroupEnabled.test.tsx b/frontend/editor/src/core/hooks/useGroupEnabled.test.tsx new file mode 100644 index 0000000000..647bcaa5b5 --- /dev/null +++ b/frontend/editor/src/core/hooks/useGroupEnabled.test.tsx @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider"; +import { useGroupEnabled } from "@app/hooks/useGroupEnabled"; +import { fetchGroupEnabled } from "@app/api/config"; + +vi.mock("@app/api/config", () => ({ fetchGroupEnabled: vi.fn() })); + +const mockFetch = vi.mocked(fetchGroupEnabled); + +describe("useGroupEnabled", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("reports null while loading, then the server's answer", async () => { + mockFetch.mockResolvedValue(true); + + const { result } = renderHook(() => useGroupEnabled("ImageMagick"), { + wrapper: TestQueryProvider, + }); + + expect(result.current.enabled).toBeNull(); + await waitFor(() => expect(result.current.enabled).toBe(true)); + expect(mockFetch).toHaveBeenCalledWith("ImageMagick"); + }); + + it("reads a failed check as disabled", async () => { + mockFetch.mockRejectedValue(new Error("boom")); + + const { result } = renderHook(() => useGroupEnabled("ImageMagick"), { + wrapper: TestQueryProvider, + }); + + await waitFor(() => expect(result.current.enabled).toBe(false)); + }); + + it("serves a second consumer of the same group from cache", async () => { + mockFetch.mockResolvedValue(true); + + const { result } = renderHook( + () => ({ + a: useGroupEnabled("ImageMagick"), + b: useGroupEnabled("ImageMagick"), + }), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => expect(result.current.a.enabled).toBe(true)); + expect(result.current.b.enabled).toBe(true); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("keeps distinct groups on distinct keys", async () => { + mockFetch.mockImplementation((group: string) => + Promise.resolve(group === "ImageMagick"), + ); + + const { result } = renderHook( + () => ({ + magick: useGroupEnabled("ImageMagick"), + calibre: useGroupEnabled("Calibre"), + }), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => expect(result.current.magick.enabled).toBe(true)); + await waitFor(() => expect(result.current.calibre.enabled).toBe(false)); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); +}); diff --git a/frontend/editor/src/core/hooks/useGroupEnabled.ts b/frontend/editor/src/core/hooks/useGroupEnabled.ts index df35b551a8..361b8be1af 100644 --- a/frontend/editor/src/core/hooks/useGroupEnabled.ts +++ b/frontend/editor/src/core/hooks/useGroupEnabled.ts @@ -1,41 +1,21 @@ -import { useState, useEffect, useRef } from "react"; -import apiClient from "@app/services/apiClient"; +import { useQuery } from "@tanstack/react-query"; +import { fetchGroupEnabled } from "@app/api/config"; +import { qk } from "@app/query/keys"; +import { CONFIG_STALE_TIME } from "@app/query/staleTime"; import type { GroupEnabledResult } from "@app/types/groupEnabled"; export type { GroupEnabledResult }; -/** - * Checks whether a named feature group is enabled on the backend. - * Returns { enabled: null } while loading, then true/false with an optional reason. - */ +/** Null while loading; a failed check reads as disabled. */ export function useGroupEnabled(group: string): GroupEnabledResult { - const [result, setResult] = useState({ - enabled: null, - unavailableReason: null, + const { data, isPending } = useQuery({ + queryKey: qk.groupEnabled(group), + queryFn: () => fetchGroupEnabled(group), + staleTime: CONFIG_STALE_TIME, }); - const isMountedRef = useRef(true); - useEffect(() => { - isMountedRef.current = true; - return () => { - isMountedRef.current = false; - }; - }, []); - - useEffect(() => { - apiClient - .get( - `/api/v1/config/group-enabled?group=${encodeURIComponent(group)}`, - ) - .then((res) => { - if (isMountedRef.current) - setResult({ enabled: res.data, unavailableReason: null }); - }) - .catch(() => { - if (isMountedRef.current) - setResult({ enabled: false, unavailableReason: null }); - }); - }, [group]); - - return result; + return { + enabled: isPending ? null : (data ?? false), + unavailableReason: null, + }; } diff --git a/frontend/editor/src/core/query/keys.ts b/frontend/editor/src/core/query/keys.ts new file mode 100644 index 0000000000..84a0206c8f --- /dev/null +++ b/frontend/editor/src/core/query/keys.ts @@ -0,0 +1,6 @@ +/** Editor query keys: ["editor", , ...params]. */ +export const qk = { + footerInfo: () => ["editor", "footerInfo"] as const, + groupEnabled: (group: string) => ["editor", "groupEnabled", group] as const, + users: () => ["editor", "users"] as const, +} as const; diff --git a/frontend/editor/src/core/query/queryClient.ts b/frontend/editor/src/core/query/queryClient.ts new file mode 100644 index 0000000000..989ebff8c5 --- /dev/null +++ b/frontend/editor/src/core/query/queryClient.ts @@ -0,0 +1,15 @@ +import { QueryClient, type DefaultOptions } from "@tanstack/react-query"; + +// networkMode "always": navigator.onLine tracks the internet, not a backend on +// 127.0.0.1 or the LAN. On the default, losing Wi-Fi strands every query. +export const baseQueryOptions: DefaultOptions["queries"] = { + staleTime: 30_000, + gcTime: 5 * 60_000, + retry: 1, + networkMode: "always", + refetchOnWindowFocus: false, +}; + +export function createAppQueryClient(): QueryClient { + return new QueryClient({ defaultOptions: { queries: baseQueryOptions } }); +} diff --git a/frontend/editor/src/core/query/staleTime.ts b/frontend/editor/src/core/query/staleTime.ts new file mode 100644 index 0000000000..50537f66c6 --- /dev/null +++ b/frontend/editor/src/core/query/staleTime.ts @@ -0,0 +1,2 @@ +/** Server config, fixed for the session. Nothing auth-scoped — no login/logout invalidation exists. */ +export const CONFIG_STALE_TIME = Infinity; diff --git a/frontend/editor/src/core/tests/utils/TestQueryProvider.tsx b/frontend/editor/src/core/tests/utils/TestQueryProvider.tsx new file mode 100644 index 0000000000..6e8f8aee5d --- /dev/null +++ b/frontend/editor/src/core/tests/utils/TestQueryProvider.tsx @@ -0,0 +1,13 @@ +import { useState, type ReactNode } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +/** Fresh client per test: retries off so failures surface immediately. */ +export function TestQueryProvider({ children }: { children: ReactNode }) { + const [client] = useState( + () => + new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: Infinity } }, + }), + ); + return {children}; +} diff --git a/frontend/editor/src/desktop/components/AppProviders.tsx b/frontend/editor/src/desktop/components/AppProviders.tsx index 5b00c1f4f5..d7405ef83d 100644 --- a/frontend/editor/src/desktop/components/AppProviders.tsx +++ b/frontend/editor/src/desktop/components/AppProviders.tsx @@ -1,6 +1,7 @@ import { ReactNode, useEffect, useRef, useState } from "react"; import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders"; import { DesktopConfigSync } from "@app/components/DesktopConfigSync"; +import { DesktopQueryCacheReset } from "@app/components/DesktopQueryCacheReset"; import { DesktopBannerInitializer } from "@app/components/DesktopBannerInitializer"; import { SaveShortcutListener } from "@app/components/SaveShortcutListener"; import { DesktopOnboardingModal } from "@app/components/DesktopOnboardingModal"; @@ -325,6 +326,8 @@ export function AppProviders({ children }: { children: ReactNode }) { autoFetch: false, }} > + {/* Also here: the auth check below switches mode pre-authChecked. */} +
{updatePopupModal} @@ -350,6 +353,7 @@ export function AppProviders({ children }: { children: ReactNode }) { window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT)), }} > + diff --git a/frontend/editor/src/desktop/components/DesktopQueryCacheReset.test.tsx b/frontend/editor/src/desktop/components/DesktopQueryCacheReset.test.tsx new file mode 100644 index 0000000000..d32cf34210 --- /dev/null +++ b/frontend/editor/src/desktop/components/DesktopQueryCacheReset.test.tsx @@ -0,0 +1,134 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import { + QueryClient, + QueryClientProvider, + useQuery, +} from "@tanstack/react-query"; +import { DesktopQueryCacheReset } from "@app/components/DesktopQueryCacheReset"; + +type Listener = (config: { mode: string }) => void; +type ServerListener = (state: { status: string }) => void; + +const modeListeners = new Set(); +const serverListeners = new Set(); + +vi.mock("@app/services/connectionModeService", () => ({ + connectionModeService: { + subscribeToModeChanges: (listener: Listener) => { + modeListeners.add(listener); + return () => modeListeners.delete(listener); + }, + }, +})); + +vi.mock("@app/services/selfHostedServerMonitor", () => ({ + selfHostedServerMonitor: { + subscribe: (listener: ServerListener) => { + serverListeners.add(listener); + listener({ status: "online" }); // matches the real replay-on-attach + return () => serverListeners.delete(listener); + }, + }, +})); + +function renderWithConsumer(queryFn: () => Promise) { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: Infinity } }, + }); + const seen: (string | undefined)[] = []; + + function Consumer() { + const { data } = useQuery({ queryKey: ["editor", "probe"], queryFn }); + seen.push(data); + return {data ?? "pending"}; + } + + render( + + + + , + ); + return seen; +} + +describe("DesktopQueryCacheReset", () => { + beforeEach(() => { + modeListeners.clear(); + serverListeners.clear(); + }); + + it("refetches a mounted query when the connection mode changes", async () => { + const queryFn = vi + .fn<() => Promise>() + .mockResolvedValueOnce("local-backend") + .mockResolvedValueOnce("self-hosted-backend"); + + const seen = renderWithConsumer(queryFn); + await waitFor(() => expect(seen).toContain("local-backend")); + + modeListeners.forEach((l) => l({ mode: "selfhosted" })); + + // Must refetch, not just evict — this is what clear() got wrong. + await waitFor(() => expect(seen).toContain("self-hosted-backend")); + expect(queryFn).toHaveBeenCalledTimes(2); + }); + + it("refetches when the self-hosted server flips online/offline", async () => { + const queryFn = vi + .fn<() => Promise>() + .mockResolvedValueOnce("server") + .mockResolvedValueOnce("local-fallback"); + + const seen = renderWithConsumer(queryFn); + await waitFor(() => expect(seen).toContain("server")); + + serverListeners.forEach((l) => l({ status: "offline" })); + + await waitFor(() => expect(seen).toContain("local-fallback")); + }); + + it("ignores the replayed state the monitor emits on subscribe", async () => { + const queryFn = vi.fn<() => Promise>().mockResolvedValue("once"); + + renderWithConsumer(queryFn); + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1)); + + expect(queryFn).toHaveBeenCalledTimes(1); + }); + + it("resets on recovery, not on the checking state in between", async () => { + const queryFn = vi.fn<() => Promise>().mockResolvedValue("x"); + + renderWithConsumer(queryFn); + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1)); + + const emit = (status: string) => + serverListeners.forEach((l) => l({ status })); + + emit("offline"); + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)); + + // A monitor restart while still down must not read as recovery — otherwise + // the reset lands here and the real online transition is skipped. + emit("checking"); + expect(queryFn).toHaveBeenCalledTimes(2); + + emit("online"); + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(3)); + }); + + it("does not reset when the monitor stops while offline", async () => { + const queryFn = vi.fn<() => Promise>().mockResolvedValue("cached"); + + renderWithConsumer(queryFn); + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1)); + + serverListeners.forEach((l) => l({ status: "offline" })); + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)); + + serverListeners.forEach((l) => l({ status: "idle" })); + expect(queryFn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/frontend/editor/src/desktop/components/DesktopQueryCacheReset.tsx b/frontend/editor/src/desktop/components/DesktopQueryCacheReset.tsx new file mode 100644 index 0000000000..d6b1fb35e6 --- /dev/null +++ b/frontend/editor/src/desktop/components/DesktopQueryCacheReset.tsx @@ -0,0 +1,39 @@ +import { useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { connectionModeService } from "@app/services/connectionModeService"; +import { selfHostedServerMonitor } from "@app/services/selfHostedServerMonitor"; + +/** + * Drops cached responses when the backend behind them changes. operationRouter + * resolves the same path to the bundled backend, a self-hosted server or SaaS, + * so a mode switch or the server going up/down invalidates every key. + */ +export function DesktopQueryCacheReset() { + const queryClient = useQueryClient(); + + useEffect(() => { + // resetQueries, not clear(): clear() evicts without notifying mounted + // observers, so a panel keeps rendering the old backend's answer. + const reset = () => void queryClient.resetQueries(); + const unsubscribeMode = connectionModeService.subscribeToModeChanges(reset); + + // idle/checking say nothing about reachability — treating them as "not + // offline" would reset on offline→checking and then skip the real recovery. + let wasOffline: boolean | null = null; + const unsubscribeServer = selfHostedServerMonitor.subscribe( + ({ status }) => { + if (status !== "online" && status !== "offline") return; + const isOffline = status === "offline"; + if (wasOffline !== null && wasOffline !== isOffline) reset(); + wasOffline = isOffline; + }, + ); + + return () => { + unsubscribeMode(); + unsubscribeServer(); + }; + }, [queryClient]); + + return null; +} diff --git a/frontend/editor/src/desktop/hooks/useGroupEnabled.test.tsx b/frontend/editor/src/desktop/hooks/useGroupEnabled.test.tsx new file mode 100644 index 0000000000..70fbc9f207 --- /dev/null +++ b/frontend/editor/src/desktop/hooks/useGroupEnabled.test.tsx @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor, act } from "@testing-library/react"; +import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider"; +import { useGroupEnabled } from "@app/hooks/useGroupEnabled"; +import { fetchGroupEnabled } from "@app/api/config"; + +vi.mock("@app/api/config", () => ({ fetchGroupEnabled: vi.fn() })); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (_k: string, fallback: string) => fallback }), +})); + +let status = "online"; +const listeners = new Set<() => void>(); + +vi.mock("@app/services/selfHostedServerMonitor", () => ({ + selfHostedServerMonitor: { + getSnapshot: () => ({ status }), + subscribe: (listener: () => void) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + }, +})); + +function setStatus(next: string) { + status = next; + act(() => listeners.forEach((l) => l())); +} + +const mockFetch = vi.mocked(fetchGroupEnabled); + +describe("desktop useGroupEnabled", () => { + beforeEach(() => { + vi.clearAllMocks(); + status = "online"; + }); + + it("skips the request entirely when the server is offline", async () => { + status = "offline"; + + const { result } = renderHook(() => useGroupEnabled("ImageMagick"), { + wrapper: TestQueryProvider, + }); + + expect(result.current.enabled).toBe(false); + expect(result.current.unavailableReason).toBeTruthy(); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("reflects the server going offline after a successful check", async () => { + mockFetch.mockResolvedValue(true); + + const { result } = renderHook(() => useGroupEnabled("ImageMagick"), { + wrapper: TestQueryProvider, + }); + await waitFor(() => expect(result.current.enabled).toBe(true)); + + setStatus("offline"); + + expect(result.current.enabled).toBe(false); + expect(result.current.unavailableReason).toBeTruthy(); + }); +}); diff --git a/frontend/editor/src/desktop/hooks/useGroupEnabled.ts b/frontend/editor/src/desktop/hooks/useGroupEnabled.ts index ec6b2523ae..a58cd2a255 100644 --- a/frontend/editor/src/desktop/hooks/useGroupEnabled.ts +++ b/frontend/editor/src/desktop/hooks/useGroupEnabled.ts @@ -1,50 +1,46 @@ -import { useState, useEffect } from "react"; +import { useSyncExternalStore } from "react"; +import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import apiClient from "@app/services/apiClient"; +import { fetchGroupEnabled } from "@app/api/config"; import { selfHostedServerMonitor } from "@app/services/selfHostedServerMonitor"; +import { qk } from "@app/query/keys"; +import { CONFIG_STALE_TIME } from "@app/query/staleTime"; import type { GroupEnabledResult } from "@app/types/groupEnabled"; const OFFLINE_REASON_FALLBACK = "Requires your Stirling-PDF server (currently offline)"; -/** - * Desktop override: skips the network request entirely when the self-hosted - * server is confirmed offline, returning a reason string matching the tool panel. - */ +// A boolean, not the monitor's state object — that is reassigned every poll. +const subscribeToMonitor = (onChange: () => void) => + selfHostedServerMonitor.subscribe(onChange); +const getIsOffline = () => + selfHostedServerMonitor.getSnapshot().status === "offline"; + +/** Desktop override: skips the request when the self-hosted server is offline. */ export function useGroupEnabled(group: string): GroupEnabledResult { const { t } = useTranslation(); - // Initialise synchronously so the first render already reflects offline state — - // avoids a flash where the option appears enabled before the effect runs. - // Use OFFLINE_REASON_FALLBACK directly so unavailableReason is non-null from - // the very first render when offline (t() is not available in useState initialiser). - const [result, setResult] = useState(() => { - const { status } = selfHostedServerMonitor.getSnapshot(); - if (status === "offline") { - return { enabled: false, unavailableReason: OFFLINE_REASON_FALLBACK }; - } - return { enabled: null, unavailableReason: null }; + const isOffline = useSyncExternalStore(subscribeToMonitor, getIsOffline); + + const { data, isPending } = useQuery({ + queryKey: qk.groupEnabled(group), + queryFn: () => fetchGroupEnabled(group), + staleTime: CONFIG_STALE_TIME, + enabled: !isOffline, }); - useEffect(() => { - const { status } = selfHostedServerMonitor.getSnapshot(); - if (status === "offline") { - setResult({ - enabled: false, - unavailableReason: t( - "toolPanel.fullscreen.selfHostedOffline", - OFFLINE_REASON_FALLBACK, - ), - }); - return; - } + // Before the query: a disabled query stays isPending, which would read as loading forever. + if (isOffline) { + return { + enabled: false, + unavailableReason: t( + "toolPanel.fullscreen.selfHostedOffline", + OFFLINE_REASON_FALLBACK, + ), + }; + } - apiClient - .get( - `/api/v1/config/group-enabled?group=${encodeURIComponent(group)}`, - ) - .then((res) => setResult({ enabled: res.data, unavailableReason: null })) - .catch(() => setResult({ enabled: false, unavailableReason: null })); - }, [group, t]); - - return result; + return { + enabled: isPending ? null : (data ?? false), + unavailableReason: null, + }; } diff --git a/frontend/editor/src/desktop/query/staleTime.ts b/frontend/editor/src/desktop/query/staleTime.ts new file mode 100644 index 0000000000..7650bfefd6 --- /dev/null +++ b/frontend/editor/src/desktop/query/staleTime.ts @@ -0,0 +1,5 @@ +export * from "@core/query/staleTime"; + +// Desktop keys don't pin a backend (see DesktopQueryCacheReset), so expire as a +// backstop. 5 min matches endpointAvailabilityService and saasAppConfigService. +export const CONFIG_STALE_TIME = 5 * 60_000; diff --git a/frontend/editor/src/portal/PortalApp.tsx b/frontend/editor/src/portal/PortalApp.tsx index 27fc5652cb..3122016436 100644 --- a/frontend/editor/src/portal/PortalApp.tsx +++ b/frontend/editor/src/portal/PortalApp.tsx @@ -30,8 +30,6 @@ function ThemedSuiProvider({ children }: { children: ReactNode }) { * self-hosted mounts the account-link layer, SaaS does not. */ export function PortalApp() { - // One client for the portal's lifetime. Sits above the router so its cache - // survives view navigation. Cheap and inert when no query hooks are mounted. const [queryClient] = useState(createPortalQueryClient); return ( diff --git a/frontend/editor/src/portal/queryClient.ts b/frontend/editor/src/portal/queryClient.ts index 85bd449b61..5c0404f17d 100644 --- a/frontend/editor/src/portal/queryClient.ts +++ b/frontend/editor/src/portal/queryClient.ts @@ -1,35 +1,15 @@ import { QueryClient } from "@tanstack/react-query"; +import { baseQueryOptions } from "@app/query/queryClient"; -/** - * The portal's TanStack Query client, mounted once at the portal root - * (PortalApp) so its cache lives above the router — data survives navigating - * away and back. staleTime 30s: a return visit within 30s serves cache with no - * network call, then revalidates in the background. Focus refetch is off — admin - * screens don't need polling. - */ let current: QueryClient | null = null; +/** Own instance, shared defaults — the portal and editor are sibling routes. */ export function createPortalQueryClient(): QueryClient { - current = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 30_000, - gcTime: 5 * 60_000, - retry: 1, - refetchOnWindowFocus: false, - }, - }, - }); + current = new QueryClient({ defaultOptions: { queries: baseQueryOptions } }); return current; } -/** - * The client created by {@link createPortalQueryClient}, or null if none has - * been mounted yet. Lets a non-hook module (the SaaS usersBackend's resolveTeam) - * read/populate the shared cache via ensureQueryData when the portal is mounted, - * while still working — via a direct fetch — when it isn't (e.g. a unit test - * that exercises the adapter without the provider). - */ +/** Null until the portal mounts, so resolveTeam can fall back to a direct fetch. */ export function tryGetPortalQueryClient(): QueryClient | null { return current; } diff --git a/frontend/editor/src/proprietary/routes/Login.test.tsx b/frontend/editor/src/proprietary/routes/Login.test.tsx index 7a232d0fde..31e74e533b 100644 --- a/frontend/editor/src/proprietary/routes/Login.test.tsx +++ b/frontend/editor/src/proprietary/routes/Login.test.tsx @@ -7,6 +7,7 @@ import Login from "@app/routes/Login"; import { useAuth } from "@app/auth/UseSession"; import { springAuth } from "@app/auth/spring/springAuthClient"; import { PreferencesProvider } from "@app/contexts/PreferencesContext"; +import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider"; import apiClient from "@app/services/apiClient"; import { configureSpringAuth } from "@app/auth/config"; import type { AxiosInstance } from "axios"; @@ -92,11 +93,14 @@ vi.mock("react-router-dom", async () => { }; }); -// Test wrapper with MantineProvider +// AuthLayout renders
, which reads useFooterInfo. In the real router +// /login sits inside AppProviders, which supplies the client. const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - + + + {children} + + ); describe("Login", () => { From 4d8a86ad286268453f10e1602d3c7fea2f38de4f Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:40:18 +0100 Subject: [PATCH 006/154] Fix the two nightly frontend failures (a11y contrast + Firefox Copy menu test) (#7286) Fixes the two nightly frontend jobs that started failing after #7163 (new design, part one). Two unrelated causes, one small fix each. ## Accessibility scan (`--c-primary-hover`) The full a11y scan flagged a colour-contrast violation on the portal pipelines ToolPicker story. #7163 moved the light canvas from `--p-gray-50` (#f9fafb) to the slightly darker `--p-paper` (#f5f4f1), but the accent text colour stayed put. That token doubles as the label colour for quiet and tertiary buttons, so the pairing slipped from 4.64:1 to 4.41:1 purely from the background change. Darkening the custom-theme mix from 85% to 80% primary puts it back at 4.89:1. It is also the hover fill for primary buttons, where a marginally deeper blue is if anything more correct. Only the nightly caught this because PR runs scan just the stories whose files changed, and #7163 did not touch that story file. ## Cross-browser Playwright (right-click Copy menu) Failed in Firefox only. The feature itself is fine in every browser. The test hit-tested a word using a fixed fraction of the page box, and the page is auto-fit to the viewer, so the rendered text scales with the viewport. The Firefox and WebKit projects run at 1280x720, where the page renders about 375px wide and the first line of text is only a few pixels tall. #7163 shrank the viewer area slightly (the rails now float with a gutter), which shrank the auto-fit page just enough to tip that fraction to landing below the glyphs. Nothing was selected, so no menu appeared. Pinning 1920x1080 for that one test makes the glyphs comfortably larger than the click tolerance everywhere, rather than re-tuning a fraction that was only ever a couple of pixels from failing. A Firefox skip was considered and rejected: the sibling clipboard test is already Chromium-only and its comment states the Copy menu is covered cross-browser by this test, so skipping would leave the menu with no Firefox coverage at all. ## Verification Test pinned run is green across chromium, firefox and webkit. Contrast checked with the theme linter's contrast report. --- .../stubbed/viewer-text-selection.spec.ts | 46 +++++++++++-------- frontend/editor/src/core/theme/colors.css | 3 +- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/frontend/editor/src/core/tests/stubbed/viewer-text-selection.spec.ts b/frontend/editor/src/core/tests/stubbed/viewer-text-selection.spec.ts index d9a08ae9e7..55dad6e2a4 100644 --- a/frontend/editor/src/core/tests/stubbed/viewer-text-selection.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/viewer-text-selection.spec.ts @@ -120,27 +120,35 @@ test("Ctrl+C copies selected text to the clipboard", async ({ expect(clipboardText.trim().length).toBeGreaterThan(0); }); -test("right-click on a word auto-selects it and reveals the Copy menu", async ({ - page, -}) => { - test.setTimeout(60_000); - const firstPage = await loadSampleAndOpenViewer(page); - const box = await firstPage.boundingBox(); - if (!box) throw new Error("no box"); +// Pinned wide: the page is auto-fit, so at the 1280x720 firefox/webkit projects +// the text renders too small to hit-test a word reliably. +test.describe("right-click selection", () => { + test.use({ viewport: { width: 1920, height: 1080 } }); - // Right-click on a word in the top paragraph "Test document for word documents". - await page.mouse.click(box.x + box.width * 0.21, box.y + box.height * 0.105, { - button: "right", + test("right-click on a word auto-selects it and reveals the Copy menu", async ({ + page, + }) => { + test.setTimeout(60_000); + const firstPage = await loadSampleAndOpenViewer(page); + const box = await firstPage.boundingBox(); + if (!box) throw new Error("no box"); + + // Right-click a word in the top paragraph "Test document for word documents". + await page.mouse.click( + box.x + box.width * 0.21, + box.y + box.height * 0.105, + { button: "right" }, + ); + await page.waitForTimeout(400); + + const selectionRects = firstPage.locator( + ".pdf-selection-layer > div:first-child > div", + ); + await expect(selectionRects.first()).toBeAttached({ timeout: 5_000 }); + + const copyButton = page.getByRole("button", { name: "Copy" }).first(); + await expect(copyButton).toBeVisible({ timeout: 5_000 }); }); - await page.waitForTimeout(400); - - const selectionRects = firstPage.locator( - ".pdf-selection-layer > div:first-child > div", - ); - await expect(selectionRects.first()).toBeAttached({ timeout: 5_000 }); - - const copyButton = page.getByRole("button", { name: "Copy" }).first(); - await expect(copyButton).toBeVisible({ timeout: 5_000 }); }); test("right-click on the page does not surface the browser context menu", async ({ diff --git a/frontend/editor/src/core/theme/colors.css b/frontend/editor/src/core/theme/colors.css index c7b0ccb08f..e79bc64c49 100644 --- a/frontend/editor/src/core/theme/colors.css +++ b/frontend/editor/src/core/theme/colors.css @@ -161,7 +161,8 @@ html[data-app-theme="midnight"] { /* ── Base accent — fixed default (blue buttons); [data-accent="default"] blocks at the end keep surfaces neutral. LIGHT base here, DARK base next. ── */ html[data-app-theme="custom"] { --c-primary: var(--p-blue-500); - --c-primary-hover: color-mix(in srgb, var(--c-primary) 85%, var(--p-black)); + /* Also the accent text colour (--_text), so 80% keeps 4.5:1 on the canvas. */ + --c-primary-hover: color-mix(in srgb, var(--c-primary) 80%, var(--p-black)); --c-primary-subtle: color-mix(in srgb, var(--c-primary) 14%, transparent); --c-text-on-primary: var(--p-white); --c-accent-fg: var(--c-primary); From d7c130fca9ad04f912caa4c9b552229cac0b6b50 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:03:12 +0100 Subject: [PATCH 007/154] Serve SPA shell for deep frontend routes (#7145) # Description of Changes stops the /new urls crashing page on f5 --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> --- .../web/ReactRoutingController.java | 89 +++++++++++++++++++ .../web/ReactRoutingControllerTest.java | 89 +++++++++++++++++++ frontend/editor/scripts/lint/theme-lint.mjs | 6 +- 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 1e05ec17b2..7689d109fe 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -5,17 +5,26 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.Set; import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.JavaScriptUtils; @@ -32,6 +41,33 @@ public class ReactRoutingController { private static final Pattern BASE_HREF_PATTERN = Pattern.compile(""); + // First path segments owned by the backend or static assets, never SPA routes. + // Mirrors the exclusion regexes on forwardRootPaths/forwardNestedPaths below. + private static final Set NON_SPA_FIRST_SEGMENTS = + Set.of( + "api", + "static", + "pipeline", + "pdfjs", + "pdfjs-legacy", + "pdfium", + "vendor", + "fonts", + "images", + "css", + "js", + "assets", + "locales", + "modern-logo", + "classic-logo", + "Login", + "og_images", + "samples"); + + // After the annotated controllers (order 0), before the resource chain + // (LOWEST_PRECEDENCE - 1). + private static final int SPA_FALLBACK_ORDER = Ordered.LOWEST_PRECEDENCE - 2; + @Value("${server.servlet.context-path:/}") private String contextPath; @@ -256,6 +292,59 @@ public class ReactRoutingController { return serveIndexHtml(request); } + // The regex mappings above only cover 1- and 2-segment paths (Spring path variables cannot + // span '/'), so deep SPA links like /processor/pipelines/new 404d on direct navigation. + // + // Registered as its own mapping rather than exposed as a bare RouterFunction @Bean: + // Spring's own RouterFunctionMapping is ordered -1, ahead of the annotated controllers at + // order 0, so a plain bean would shadow every dot-free backend route the denylist below + // does not name (/v1/api-docs, /error, /actuator, ...). LOWEST_PRECEDENCE - 2 puts it after + // the controllers and before the resource chain (LOWEST_PRECEDENCE - 1), which is the only + // position where a catch-all fallback is safe. + @Bean + public RouterFunctionMapping spaDeepLinkFallbackMapping() { + RouterFunction fallback = + RouterFunctions.route( + request -> { + HttpServletRequest servletRequest = request.servletRequest(); + return "GET".equals(servletRequest.getMethod()) + && isSpaFallbackRoute( + stripContextPath( + servletRequest.getContextPath(), + servletRequest.getRequestURI())); + }, + request -> + ServerResponse.ok() + .cacheControl(CacheControl.noCache().mustRevalidate()) + .contentType(MediaType.TEXT_HTML) + .body(serveIndexHtml(request.servletRequest()).getBody())); + RouterFunctionMapping mapping = new RouterFunctionMapping(fallback); + mapping.setOrder(SPA_FALLBACK_ORDER); + mapping.setMessageConverters( + List.of(new StringHttpMessageConverter(StandardCharsets.UTF_8))); + return mapping; + } + + // Dot-free paths only, so requests for real files still fall through to the resource + // handlers. This is a denylist, so it is only safe because the mapping above runs after + // the annotated controllers - see spaDeepLinkFallbackMapping. + static boolean isSpaFallbackRoute(String path) { + if (path == null || path.isEmpty() || "/".equals(path) || path.indexOf('.') >= 0) { + return false; + } + String[] segments = (path.startsWith("/") ? path.substring(1) : path).split("/"); + return segments.length > 0 + && !segments[0].isEmpty() + && !NON_SPA_FIRST_SEGMENTS.contains(segments[0]); + } + + private static String stripContextPath(String contextPath, String uri) { + if (contextPath != null && !contextPath.isBlank() && uri.startsWith(contextPath)) { + return uri.substring(contextPath.length()); + } + return uri; + } + private String buildFallbackHtml() { String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/"; diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java index 2df3fb5587..41909e3e0a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java @@ -4,12 +4,24 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.servlet.function.EntityResponse; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; +import org.springframework.web.util.ServletRequestPathUtils; import jakarta.servlet.http.HttpServletRequest; @@ -175,6 +187,83 @@ class ReactRoutingControllerTest { assertNotNull(response.getBody()); } + // --- deep-link SPA fallback (router function) --- + + @Test + void isSpaFallbackRoute_acceptsDeepSpaPaths() { + assertTrue(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/new")); + assertTrue(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/123/runs/456")); + assertTrue(ReactRoutingController.isSpaFallbackRoute("/workflow/sign/some-token")); + assertTrue(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/new/")); + // "pipelines" must not be swallowed by the "pipeline" exclusion + assertTrue(ReactRoutingController.isSpaFallbackRoute("/pipelines")); + } + + @Test + void isSpaFallbackRoute_rejectsBackendStaticAndFilePaths() { + assertFalse(ReactRoutingController.isSpaFallbackRoute("/api/v1/some/endpoint")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/pipeline")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/pipeline/anything")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/assets/deep/path")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/file.js")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/branding/sub/logo.png")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("")); + assertFalse(ReactRoutingController.isSpaFallbackRoute(null)); + } + + @Test + void spaDeepLinkFallback_servesIndexForDeepRoute() throws Exception { + controller.init(); + RouterFunction router = routerOf(controller.spaDeepLinkFallbackMapping()); + + ServerRequest deepRequest = serverRequest("GET", "/processor/pipelines/new"); + Optional> handler = router.route(deepRequest); + assertTrue(handler.isPresent()); + + ServerResponse response = handler.get().handle(deepRequest); + assertEquals(HttpStatus.OK, response.statusCode()); + assertInstanceOf(EntityResponse.class, response); + Object body = ((EntityResponse) response).entity(); + assertTrue(body.toString().contains("Stirling PDF")); + } + + @Test + void spaDeepLinkFallback_ignoresApiFilesAndNonGet() { + controller.init(); + RouterFunction router = routerOf(controller.spaDeepLinkFallbackMapping()); + + assertTrue(router.route(serverRequest("GET", "/api/v1/policies/run")).isEmpty()); + assertTrue(router.route(serverRequest("GET", "/branding/sub/logo.png")).isEmpty()); + assertTrue(router.route(serverRequest("POST", "/processor/pipelines/new")).isEmpty()); + } + + @Test + void spaDeepLinkFallback_runsAfterControllersAndBeforeResources() { + controller.init(); + int order = controller.spaDeepLinkFallbackMapping().getOrder(); + + // A catch-all denylist is only safe below every annotated controller; Spring's own + // RouterFunctionMapping sits at -1, which would shadow /v1/api-docs, /error and friends. + assertTrue(order > 0, "SPA fallback must run after annotated controllers"); + assertTrue( + order < Ordered.LOWEST_PRECEDENCE - 1, + "SPA fallback must run before the static-resource chain"); + } + + private static RouterFunction routerOf(RouterFunctionMapping mapping) { + @SuppressWarnings("unchecked") + RouterFunction router = + (RouterFunction) mapping.getRouterFunction(); + return router; + } + + private static ServerRequest serverRequest(String method, String uri) { + MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, uri); + ServletRequestPathUtils.parseAndCache(servletRequest); + return ServerRequest.create(servletRequest, List.of(new StringHttpMessageConverter())); + } + // --- context path handling --- @Test diff --git a/frontend/editor/scripts/lint/theme-lint.mjs b/frontend/editor/scripts/lint/theme-lint.mjs index 0ae53a3ce9..42f6153119 100644 --- a/frontend/editor/scripts/lint/theme-lint.mjs +++ b/frontend/editor/scripts/lint/theme-lint.mjs @@ -314,6 +314,10 @@ function check() { const violations = []; const primitiveValues = new Map(); const lineOf = (text, index) => text.slice(0, index).split("\n").length; + // path.relative emits backslashes on Windows; normalize so the PRIMITIVES + // comparison below matches and printed paths stay POSIX-style. + const posixRel = (name) => + relative(process.cwd(), join(THEME, name)).replaceAll("\\", "/"); // Fail if a theme .css exists that isn't registered above (readdir is only // compared here — never used to build a path passed to readFileSync). @@ -321,7 +325,7 @@ function check() { for (const name of readdirSync(THEME)) { if (name.endsWith(".css") && !known.has(name)) { violations.push({ - file: relative(process.cwd(), join(THEME, name)), + file: posixRel(name), line: 1, msg: `unregistered theme CSS — add "${name}" to THEME_FILES in theme-lint.mjs`, }); From 62e5e28039d9e3cad1e101daf175c0774dd8aa33 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:10:28 +0100 Subject: [PATCH 008/154] Avoid renderer OOM when adding large PDFs to the workbench (#7175) # Description of Changes - "Out of Memory" tab crash on merge with big PDFs (Discord report) is a WebView2 renderer OOM, not system RAM; adding files did 2 full parses + a parallel full read per file - IndexedDB now stores files as Blobs (by reference, no JS-side copy); old ArrayBuffer records stay readable - Both thumbnail variants now come from one PDFium parse instead of two - PDFs over 100MB never get a full client-side parse: 2MB linearized-prefix attempt, placeholder icon otherwise - Follow-ups if placeholders aren't enough: backend-rendered thumbnails, PDFium ranged reads in a worker --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> --- .../fileEditor/FileEditorThumbnail.tsx | 4 + .../src/core/components/shared/FileCard.tsx | 10 +- .../src/core/contexts/file/fileActions.ts | 19 +- .../editor/src/core/services/fileAnalyzer.ts | 81 ++++++-- .../editor/src/core/services/fileStorage.ts | 9 +- .../src/core/utils/thumbnailUtils.test.ts | 53 ++++++ .../editor/src/core/utils/thumbnailUtils.ts | 180 +++++++++++++++++- 7 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 frontend/editor/src/core/utils/thumbnailUtils.test.ts diff --git a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx index e183e45d6e..ccf07e9dea 100644 --- a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -45,6 +45,7 @@ import { VersionHistoryModal } from "@app/components/filesPage/VersionHistoryMod import { useAppConfig } from "@app/contexts/AppConfigContext"; import { useFileThumbnail } from "@app/hooks/useFileThumbnail"; import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail"; +import { LARGE_PDF_PARSE_LIMIT } from "@app/utils/thumbnailUtils"; import { truncateCenter } from "@app/utils/textUtils"; import { FileEditorStatusDot } from "@app/components/fileEditor/FileEditorStatusDot"; @@ -553,6 +554,9 @@ const FileEditorThumbnail = ({ isLoading={ !isEncrypted && !displayThumbnail && + // No thumbnail is ever produced at/above the parse limit, so + // without this the spinner has no terminal state. + file.size < LARGE_PDF_PARSE_LIMIT && (isThumbGenerating || file.type?.startsWith("application/pdf") || file.type?.startsWith("image/")) diff --git a/frontend/editor/src/core/components/shared/FileCard.tsx b/frontend/editor/src/core/components/shared/FileCard.tsx index e43dbeeb1c..495a76a53b 100644 --- a/frontend/editor/src/core/components/shared/FileCard.tsx +++ b/frontend/editor/src/core/components/shared/FileCard.tsx @@ -11,6 +11,7 @@ import { StirlingFileStub } from "@app/types/fileContext"; import { getFileSize, getFileDate } from "@app/utils/fileUtils"; import { useFileThumbnail } from "@app/hooks/useFileThumbnail"; import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail"; +import { LARGE_PDF_PARSE_LIMIT } from "@app/utils/thumbnailUtils"; interface FileCardProps { file: File; @@ -44,7 +45,14 @@ const FileCard = ({ const [isHovered, setIsHovered] = useState(false); const isPdf = file.type === "application/pdf"; - const isHydrating = isPdf && !isEncrypted && !thumb && !isGenerating; + // Files at/above the parse limit never get a thumbnail, so without the size + // check their spinner has no terminal state and runs forever. + const isHydrating = + isPdf && + file.size < LARGE_PDF_PARSE_LIMIT && + !isEncrypted && + !thumb && + !isGenerating; return ( { + const tailStart = Math.max(0, file.size - ENCRYPT_PROBE_BYTES); + if (bufferHasEncryptMarker(await file.slice(tailStart).arrayBuffer())) { + return true; + } + if (tailStart === 0) return false; + return bufferHasEncryptMarker( + await file.slice(0, ENCRYPT_PROBE_BYTES).arrayBuffer(), + ); +} + export class FileAnalyzer { private static readonly SIZE_THRESHOLDS = { SMALL: 10 * 1024 * 1024, // 10MB @@ -80,32 +96,57 @@ export class FileAnalyzer { /** * Cheap encryption-only probe for the upload-time detection path. * - * Looks for a /Encrypt entry in the last 8KB of the file (where the PDF - * trailer lives). If absent, the file is definitely not encrypted and we - * can skip a full pdf.js parse. If present, falls back to pdf.js so we can - * distinguish user-password (blocks open) from owner-password-only (opens - * fine) — only the former should prompt. + * Looks for a /Encrypt entry in a bounded window at either end of the file + * (where PDF trailers live). If absent, the file is definitely not encrypted + * and we can skip a full pdf.js parse. If present, falls back to pdf.js so we + * can distinguish user-password (blocks open) from owner-password-only (opens + * fine); only the former should prompt. + * + * Runs inside the addFiles mutex, so it must always settle: an unbounded + * parse here stalls every later upload as well as this one. */ static async isPDFUserPasswordProtected(file: File): Promise { - const arrayBuffer = await file.arrayBuffer(); - if (!hasEncryptMarker(arrayBuffer)) return false; + if (!(await hasEncryptMarker(file))) return false; - let pdf: PDFDocumentProxy | undefined; - try { - pdf = await pdfWorkerManager.createDocument(arrayBuffer, { - stopAtErrors: false, - verbosity: 0, + // Too big to hand pdf.js: that full-buffer parse is the renderer OOM the + // large-file path exists to avoid, so trust the marker instead of it. + if (file.size >= LARGE_PDF_PARSE_LIMIT) return true; + + const arrayBuffer = await file.arrayBuffer(); + let timedOut = false; + let timer: ReturnType | undefined; + const opening = pdfWorkerManager + .createDocument(arrayBuffer, { stopAtErrors: false, verbosity: 0 }) + .then((pdf) => { + // Arrived after the timeout won the race - nothing else frees it. + if (timedOut) pdfWorkerManager.destroyDocument(pdf); + return pdf; }); + + try { + const pdf = await Promise.race([ + opening, + new Promise((_, reject) => { + timer = setTimeout(() => { + timedOut = true; + reject(new Error(PROBE_TIMEOUT)); + }, PROBE_TIMEOUT_MS); + }), + ]); + pdfWorkerManager.destroyDocument(pdf); // pdf.js opened it — owner-password-only case, no prompt needed. return false; } catch (error) { const errorMessage = error instanceof Error ? error.message.toLowerCase() : ""; + // Unconfirmed either way; the /Encrypt marker is the better guess, and a + // spurious prompt beats a card that never stops spinning. + if (errorMessage === PROBE_TIMEOUT) return true; return ( errorMessage.includes("password") || errorMessage.includes("encrypted") ); } finally { - if (pdf) pdfWorkerManager.destroyDocument(pdf); + clearTimeout(timer); } } diff --git a/frontend/editor/src/core/services/fileStorage.ts b/frontend/editor/src/core/services/fileStorage.ts index 4cdecca9f1..4f82af3a8f 100644 --- a/frontend/editor/src/core/services/fileStorage.ts +++ b/frontend/editor/src/core/services/fileStorage.ts @@ -23,7 +23,9 @@ import { const THUMBNAIL_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days export interface StoredStirlingFileRecord extends BaseFileMetadata { - data: ArrayBuffer; + // Blob since the large-file OOM fix (stored by reference, no JS-side copy); + // ArrayBuffer records predate it and are still readable. + data: ArrayBuffer | Blob; fileId: FileId; // Matches runtime StirlingFile.fileId exactly quickKey: string; // Matches runtime StirlingFile.quickKey exactly thumbnail?: string; @@ -118,7 +120,6 @@ class FileStorageService { stub: StirlingFileStub, ): Promise { const db = await this.getDatabase(); - const arrayBuffer = await stirlingFile.arrayBuffer(); const record: StoredStirlingFileRecord = { id: stirlingFile.fileId, @@ -129,7 +130,9 @@ class FileStorageService { size: stirlingFile.size, lastModified: stirlingFile.lastModified, createdAt: stub.createdAt, - data: arrayBuffer, + // Store the File (a Blob) itself: IndexedDB persists it by reference and + // streams to disk, so multi-GB files never materialize in JS memory. + data: stirlingFile, thumbnail: stub.thumbnailUrl, thumbnailStoredAt: stub.thumbnailUrl ? Date.now() : undefined, isLeaf: stub.isLeaf ?? true, diff --git a/frontend/editor/src/core/utils/thumbnailUtils.test.ts b/frontend/editor/src/core/utils/thumbnailUtils.test.ts new file mode 100644 index 0000000000..c47bd05bdf --- /dev/null +++ b/frontend/editor/src/core/utils/thumbnailUtils.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { containsEncryptMarker } from "@app/utils/thumbnailUtils"; + +/** + * The only signal that a PDF too large to fully parse is password-protected. + * Tested over raw bytes because jsdom's Blob does not return its own contents + * from arrayBuffer(), so the slicing wrapper cannot be exercised here. + */ + +/** PDF-shaped bytes with `trailer` written at the very end. */ +function bytesEndingWith(trailer: string, size: number): Uint8Array { + const bytes = new Uint8Array(size).fill(0x20); // padding + const tail = new TextEncoder().encode(trailer); + bytes.set(tail, size - tail.length); + return bytes; +} + +describe("containsEncryptMarker — trailer probe for large PDFs", () => { + it("detects /Encrypt in a trailer dictionary", () => { + const bytes = bytesEndingWith( + "trailer\n<< /Size 9 /Root 1 0 R /Encrypt 8 0 R >>\nstartxref\n1234\n%%EOF\n", + 4096, + ); + expect(containsEncryptMarker(bytes)).toBe(true); + }); + + it("leaves an unencrypted document alone", () => { + const bytes = bytesEndingWith( + "trailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n1234\n%%EOF\n", + 4096, + ); + expect(containsEncryptMarker(bytes)).toBe(false); + }); + + it("does not match longer keys that merely start the same way", () => { + const bytes = bytesEndingWith("<< /EncryptionAware true >>\n%%EOF\n", 512); + expect(containsEncryptMarker(bytes)).toBe(false); + }); + + it("survives binary bytes around the marker", () => { + // Under UTF-8 these become replacement characters and consume the marker. + const prefix = new Uint8Array([0xff, 0xfe, 0x80, 0x00, 0x9d]); + const marker = new TextEncoder().encode(" /Encrypt 8 0 R >>"); + const bytes = new Uint8Array(prefix.length + marker.length); + bytes.set(prefix); + bytes.set(marker, prefix.length); + expect(containsEncryptMarker(bytes)).toBe(true); + }); + + it("reports nothing for an empty read", () => { + expect(containsEncryptMarker(new Uint8Array(0))).toBe(false); + }); +}); diff --git a/frontend/editor/src/core/utils/thumbnailUtils.ts b/frontend/editor/src/core/utils/thumbnailUtils.ts index 838992e38b..b356b26db9 100644 --- a/frontend/editor/src/core/utils/thumbnailUtils.ts +++ b/frontend/editor/src/core/utils/thumbnailUtils.ts @@ -31,6 +31,37 @@ export function calculateScaleFromFileSize(fileSize: number): number { /** PDFium error code 4 = password required (encrypted PDF). */ const PDFIUM_ERR_PASSWORD = 4; +/** PDFs at or above this size never get a full-buffer client-side parse + * (renderer OOM) - only the linearized-prefix attempt below. */ +export const LARGE_PDF_PARSE_LIMIT = 100 * 1024 * 1024; + +/** Linearized PDFs keep page 1 + hint tables in the first bytes, so a small + * prefix is often enough to render a thumbnail without reading the file. */ +const LINEARIZED_PREFIX_BYTES = 2 * 1024 * 1024; + +/** Window at each end of the file searched for an /Encrypt entry. */ +const ENCRYPT_PROBE_BYTES = 64 * 1024; + +/** Decoded latin1 because the input is binary - UTF-8 replacement characters + * can swallow the marker. \b excludes longer keys like /Encryption. */ +export function containsEncryptMarker(bytes: Uint8Array): boolean { + return /\/Encrypt\b/.test(new TextDecoder("latin1").decode(bytes)); +} + +/** /Encrypt is referenced from a trailer, never from the page data the prefix + * parse sees. Linearized PDFs (most large ones) keep their first-page trailer at + * the head and the main one at the tail, so both windows have to be probed. + * Heuristic: a false positive offers unlock on a file that did not need it, a + * false negative leaves it unopenable. */ +async function looksEncryptedFromTrailer(file: File): Promise { + const tailStart = Math.max(0, file.size - ENCRYPT_PROBE_BYTES); + const tail = await file.slice(tailStart).arrayBuffer(); + if (containsEncryptMarker(new Uint8Array(tail))) return true; + if (tailStart === 0) return false; + const head = await file.slice(0, ENCRYPT_PROBE_BYTES).arrayBuffer(); + return containsEncryptMarker(new Uint8Array(head)); +} + interface PdfiumRenderResult { thumbnail: string; pageCount: number; @@ -112,6 +143,72 @@ async function renderPdfThumbnailPdfium( } } +/** + * Render both thumbnail variants (upright + rotation-baked) from a single + * document open - halves the parse and memory cost of the add-files path. + */ +async function renderPdfThumbnailPairPdfium( + data: ArrayBuffer, + scale: number, + collectAllPagesMetadata: boolean, +): Promise<{ unrotated: PdfiumRenderResult; rotated: PdfiumRenderResult }> { + const m = await getPdfiumModule(); + let docPtr: number; + try { + docPtr = await openRawDocumentSafe(data); + } catch (error) { + if ( + error instanceof Error && + new RegExp(`error ${PDFIUM_ERR_PASSWORD}`).test(error.message) + ) { + const encrypted: PdfiumRenderResult = { + thumbnail: "", + pageCount: 1, + pageRotations: [], + pageDimensions: [], + isEncrypted: true, + }; + return { unrotated: encrypted, rotated: { ...encrypted } }; + } + throw error; + } + + try { + const pageCount = m.FPDF_GetPageCount(docPtr); + const unrotatedThumb = await renderPdfiumPageDataUrl(docPtr, 0, scale, { + applyRotation: false, + }); + const rotatedThumb = await renderPdfiumPageDataUrl(docPtr, 0, scale, { + applyRotation: true, + }); + if (!unrotatedThumb || !rotatedThumb) { + throw new Error("PDFium: failed to render page 0"); + } + + const firstMeta = await readPdfiumPageMetadata(docPtr, 0); + const pageRotations: number[] = [firstMeta?.rotation ?? 0]; + const pageDimensions: Array<{ width: number; height: number }> = [ + { width: firstMeta?.width ?? 0, height: firstMeta?.height ?? 0 }, + ]; + if (collectAllPagesMetadata) { + for (let i = 1; i < pageCount; i++) { + const meta = await readPdfiumPageMetadata(docPtr, i); + if (!meta) continue; + pageRotations[i] = meta.rotation; + pageDimensions[i] = { width: meta.width, height: meta.height }; + } + } + + const base = { pageCount, pageRotations, pageDimensions }; + return { + unrotated: { thumbnail: unrotatedThumb, ...base }, + rotated: { thumbnail: rotatedThumb, ...base }, + }; + } finally { + await closeRawDocument(docPtr); + } +} + async function generatePDFThumbnail( arrayBuffer: ArrayBuffer, scale: number, @@ -133,7 +230,7 @@ async function generatePDFThumbnail( */ export async function generateThumbnailForFile(file: File): Promise { // Very large PDFs skip thumbnail generation — SVG icon shown in UI instead - if (file.size >= 100 * 1024 * 1024) { + if (file.size >= LARGE_PDF_PARSE_LIMIT) { return ""; } @@ -152,8 +249,7 @@ export async function generateThumbnailForFile(file: File): Promise { const scale = calculateScaleFromFileSize(file.size); // Only read first 2MB for thumbnail generation to save memory - const chunkSize = 2 * 1024 * 1024; // 2MB - const chunk = file.slice(0, Math.min(chunkSize, file.size)); + const chunk = file.slice(0, Math.min(LINEARIZED_PREFIX_BYTES, file.size)); const arrayBuffer = await chunk.arrayBuffer(); try { @@ -193,6 +289,36 @@ export async function generateThumbnailWithMetadata( const scale = calculateScaleFromFileSize(file.size); + // Never full-parse huge PDFs client-side - the renderer process OOMs long + // before system RAM runs out. The prefix succeeds for linearized PDFs. + if (file.size >= LARGE_PDF_PARSE_LIMIT) { + // Probe inside the try: an unreadable file must still resolve, or the + // caller leaves the card with no metadata and a spinner that never stops. + try { + if (await looksEncryptedFromTrailer(file)) { + return { thumbnail: "", pageCount: 1, isEncrypted: true }; + } + const chunk = await file.slice(0, LINEARIZED_PREFIX_BYTES).arrayBuffer(); + const result = await renderPdfThumbnailPdfium( + chunk, + scale, + applyRotation, + false, + ); + if (result.isEncrypted) { + return { thumbnail: "", pageCount: 1, isEncrypted: true }; + } + return { + thumbnail: result.thumbnail, + pageCount: result.pageCount, + pageRotations: result.pageRotations, + pageDimensions: result.pageDimensions, + }; + } catch { + return { thumbnail: "", pageCount: 0 }; + } + } + try { const arrayBuffer = await file.arrayBuffer(); // Always read per-page rotation: PageEditor renders thumbnails upright and @@ -222,3 +348,51 @@ export async function generateThumbnailWithMetadata( return { thumbnail: "", pageCount: 1 }; } } + +/** + * Both thumbnail variants + page metadata from ONE full parse instead of two. + * Large PDFs only get the linearized-prefix attempt; if that fails, both + * variants are empty placeholders and page metadata is omitted. + */ +export async function generateThumbnailPairWithMetadata(file: File): Promise<{ + unrotated: ThumbnailWithMetadata; + rotated: ThumbnailWithMetadata; +}> { + const scale = calculateScaleFromFileSize(file.size); + const isLarge = file.size >= LARGE_PDF_PARSE_LIMIT; + try { + // Probe inside the try: an unreadable file must still resolve, or the + // caller leaves the card with no metadata and a spinner that never stops. + if (isLarge && (await looksEncryptedFromTrailer(file))) { + const encrypted: ThumbnailWithMetadata = { + thumbnail: "", + pageCount: 1, + isEncrypted: true, + }; + return { unrotated: encrypted, rotated: { ...encrypted } }; + } + const buffer = isLarge + ? await file.slice(0, LINEARIZED_PREFIX_BYTES).arrayBuffer() + : await file.arrayBuffer(); + const pair = await renderPdfThumbnailPairPdfium(buffer, scale, !isLarge); + + const toPublic = (r: PdfiumRenderResult): ThumbnailWithMetadata => + r.isEncrypted + ? { thumbnail: "", pageCount: 1, isEncrypted: true } + : { + thumbnail: r.thumbnail, + pageCount: r.pageCount, + pageRotations: r.pageRotations, + pageDimensions: r.pageDimensions, + }; + return { + unrotated: toPublic(pair.unrotated), + rotated: toPublic(pair.rotated), + }; + } catch { + return { + unrotated: { thumbnail: "", pageCount: 0 }, + rotated: { thumbnail: "", pageCount: 0 }, + }; + } +} From 8f5344ec7da36ff153432652ddc6c9c1c4c7d4c2 Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 5 Aug 2026 16:25:44 +0200 Subject: [PATCH 009/154] build(tool-models): separate API model generation from Swagger setup (#7300) # Description of Changes - Added internal `_generate` tasks for frontend and engine tool-model generation. - Kept the public `tool-models` tasks responsible for dependency installation and Swagger generation. - Separated environment preparation from the actual model generation commands. - This allows higher-level tasks to prepare the OpenAPI specification once and invoke both generators without repeating the backend setup. --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .taskfiles/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.taskfiles/backend.yml b/.taskfiles/backend.yml index fbabd783a3..63773f61fc 100644 --- a/.taskfiles/backend.yml +++ b/.taskfiles/backend.yml @@ -146,6 +146,7 @@ tasks: swagger: desc: "Generate OpenAPI docs" + run: once cmds: - cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc" platforms: [windows] From d0d197f09f84a5609aa8191dc08187f4c873b12d Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:23:50 +0100 Subject: [PATCH 010/154] Procurement: draft Enterprise Agreement + signature, legal pages & consent, quote/agreement split (#7021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates the enterprise procurement and legal work into one PR off `main`. Supersedes #7020 (closed; every commit from it is contained here). Sits on top of PAYG prepaid bundles (#7032) and the `--color-*` → `--c-*` portal token rename. ## Why Enterprise procurement was a mock. The stage screens read from a fake state machine, the "agreement" was prose hardcoded in a component, and nothing a buyer did was recorded anywhere. To actually sell to an enterprise we need three things it didn't have: a real document they can read and sign, a record that proves they signed that exact version, and a licence that flips when they pay. ## What **The agreement is a real versioned document** - Registry at `resources/legal/manifest.json` + `legal///*.md`. Publishing a new version is a markdown file and a manifest bump, no code change. `@`-prefixed parts are generated sections. - `AgreementAssembler` builds MSA (Part A) + generated Order Form (Part B) + DPA (Part C) as one document. Only the Order Form varies per deal. - `AgreementPdfRenderer` goes through our own pipeline (commonmark → `FileToPdf`/WeasyPrint), so we dogfood it. - Immutable signature record pinning document id and version, a SHA-256 of the exact rendered markdown, the variable snapshot, typed signatory details, timestamp and IP. **Legal document pages and consent logging** - `GET /api/v1/legal/{docId}` serves any registry document; a viewer modal renders it with a draft badge. The SLA exhibit is viewable for the first time. - `legal_consent` + `POST /api/v1/legal/consent`. EULA clickwrap is recorded once: at trial start, or at the quote step only if there was no trial. **Quote and Agreement are separate steps** The quote step is a plain itemised review (figures, renewal, PO) with download and "Accept quote". Accepting advances to the agreement and does not charge Stripe. Signing the agreement is still the commitment point. **One quote number** We no longer mint our own reference. The Stripe quote number is the identifier everywhere, so the UI and the memo can't disagree. `quote_number` is nullable until Stripe assigns it at finalisation (`20260808000000`). **Payment takes the deal live** `invoice.paid` on the stripe-webhook moves the deal to live and the UI reflects it. Nothing watched for payment before, so a paid customer sat in "payment" forever. Needs `invoice.paid` enabled on the webhook endpoint in the Stripe dashboard. **Security** Any signup could self-issue a $0 enterprise licence, from three things compounding: leader-on-signup, no entitlement gate, and no ACV floor. So: `startTrial` now has a stage guard (it was replacing committed licences), the offline `.lic` is gated on entitlement, the ACV floor is enforced before the quote persists, and the air-gap check reads the quote's deployment rather than the deal's. Invitee emails are redacted in logs. Dev and Storybook were hitting real Stripe; both now route through `resolveDemoResponse`. **Removed the dead procurement island** The original stage-by-stage page survived the rebuild with no route and no consumer, so it was invisible to review but still cost a reader's time. 16 unreferenced files, 182 lines of superseded API, 53 orphaned en-US keys, and `Procurement.css` from 1665 to 968 lines. Nothing deleted had a live consumer. ## Screenshots Home, deal underway (hero card footer): Quote builder, step 1: Agreement, ready to sign: Payment and live: ## How to test **Storybook** covers every state without a backend: ```bash cd frontend && npm run storybook ``` Then `Portal/Procurement/*`: | Story | What to look at | | --- | --- | | `DealStatusHero` — Trial / Quote / Agreement / Payment / Live | One hero per stage: progress band, one-line status, stage CTA | | `QuoteBuilder` — Default | 4 steps. Users + volume drive the price; Governance and PDF size are multipliers; step 4 is the itemised review | | `ProcurementAgreement` — Default / Signing | Header actions, always-visible scrollbar on the paper, one-line signature row | | `ProcurementStages` — Payment / Live / License | "View & pay invoice" opens Stripe directly; licence key and `.lic` download | | `Views/Home` — Subscribed In Procurement | The hero in real page context | Note: `ProcurementAgreement` renders "Could not load the agreement" in Storybook because it fetches the document from the backend. The chrome is accurate, the paper body needs the app. **Full flow** needs SaaS running and a linked team: 1. Home → **Explore enterprise** → trial setup (deployment + seats). EULA is recorded here. 2. **Build your quote** → 4 steps → Generate. Buyer details are required first. 3. Review the itemised quote → **Accept quote**. Confirm Stripe was *not* charged. 4. Agreement → tick, fill signatory, **Sign agreement**. Check `procurement_signature` for the version and content hash. 5. **View & pay invoice** → pay in Stripe test mode → deal should move to live on the `invoice.paid` webhook. Worth reviewing specifically: the licence cannot be issued without entitlement (step 3 before payment), and `startTrial` on an already-committed deal is rejected rather than overwriting. ## Verification - `:saas compileJava` + `spotlessJavaCheck` - `task frontend:check:all` green end to end: 9 typecheck variants, eslint at zero warnings, `theme-lint`, `lint:css`, prettier, build, **1656 tests across 188 files** - 7 deno tests on the `invoice.paid` handler, covering all four shapes Stripe uses for the subscription reference ## Open, not addressed here - **The commercial model contradicts itself in three places.** The Order Form says annual-in-advance, the MSA §2.3/§3.2 implies otherwise, the quote engine computes `tcv = annualNet × termYears` flat, and Stripe only invoices one year. Needs a decision before this is customer-facing. - The 25 MB data-processing increments vs the ×1.4/×2.4 size multiplier, deferred pending Matt. - All legal text is **draft**. It renders with a draft badge and is not presented as executed; counsel's read is still a publish gate. - `{{subprocessor_url}}` / `{{eula_url}}` awaiting marketing's final links. - `frontend-a11y` is red on pre-existing portal contrast debt, deferred by decision. ## Schema notes Two migrations land on the SaaS side (`v3`), both applied by that repo's PR CI: - `20260808000000` drops the NOT NULL on `procurement_quote.quote_number`, which is required rather than cosmetic — the number now comes from Stripe at finalisation, so a draft holds NULL, and `ddl-auto` cannot drop an existing NOT NULL itself. - `20260809000000` adds `procurement_deal.last_paid_invoice_id`, nullable. Nothing here needs a migration in this repo: Flyway is not on the classpath, so the Java side only ever adds via `ddl-auto`, and Postgres migrations run ahead of the app deploy. --- app/saas/build.gradle | 5 + .../software/saas/config/SaasJpaConfig.java | 6 +- .../software/saas/legal/LegalConsent.java | 62 + .../saas/legal/LegalConsentRepository.java | 5 + .../saas/legal/LegalConsentService.java | 47 + .../software/saas/legal/LegalController.java | 133 ++ .../saas/legal/LegalDocumentMeta.java | 27 + .../saas/legal/LegalDocumentRegistry.java | 151 ++ .../saas/payg/bundle/PrepaidBundle.java | 15 +- .../api/ProcurementController.java | 247 ++- .../ProcurementConfigurationProperties.java | 13 + .../procurement/legal/AgreementAssembler.java | 278 ++++ .../legal/AgreementPdfRenderer.java | 71 + .../procurement/legal/AgreementSigning.java | 12 + .../procurement/legal/AssembledAgreement.java | 16 + .../model/ProcurementAgreementSignature.java | 86 + .../procurement/model/ProcurementDeal.java | 33 + .../procurement/model/ProcurementQuote.java | 6 +- .../pricing/ProcurementPricingService.java | 49 +- ...ocurementAgreementSignatureRepository.java | 29 + .../service/ProcurementService.java | 390 ++++- .../legal/enterprise-agreement/0.9.1/dpa.md | 53 + .../legal/enterprise-agreement/0.9.1/msa.md | 119 ++ .../main/resources/legal/eula/1.0.0/eula.md | 75 + .../src/main/resources/legal/manifest.json | 38 + .../src/main/resources/legal/sla/1.0.0/sla.md | 37 + .../subprocessors/1.0.0/subprocessors.md | 19 + .../ProcurementTrialRestartPolicyTest.java | 44 + frontend/.storybook/a11y-baseline.json | 22 +- .../public/locales/en-US/translation.toml | 241 ++- frontend/editor/src/portal/ViewRouter.tsx | 2 - .../editor/src/portal/api/externalUrl.test.ts | 51 + frontend/editor/src/portal/api/externalUrl.ts | 25 + frontend/editor/src/portal/api/procurement.ts | 369 ++--- .../portal/components/EditorStatusCard.css | 99 +- .../components/EditorStatusCard.stories.tsx | 30 +- .../portal/components/EditorStatusCard.tsx | 179 +-- .../portal/components/HomeHero.stories.tsx | 21 +- .../editor/src/portal/components/HomeHero.tsx | 72 +- .../src/portal/components/SetupChecklist.css | 122 -- .../components/SetupChecklist.stories.tsx | 66 - .../src/portal/components/SetupChecklist.tsx | 150 -- .../src/portal/components/WelcomeBanner.css | 103 -- .../components/WelcomeBanner.stories.tsx | 41 - .../src/portal/components/WelcomeBanner.tsx | 97 -- .../billing/BundleCheckoutModal.tsx | 4 +- .../components/billing/EnterpriseUpsell.tsx | 13 +- .../components/billing/FreePlanView.tsx | 1 - .../components/billing/LinkAccountPrompt.tsx | 6 +- .../components/billing/PrepayModalHeader.tsx | 100 +- .../components/billing/SpendLimitCard.tsx | 2 +- .../billing/StripeCheckoutModal.tsx | 1 - .../src/portal/components/billing/billing.css | 53 - .../editor/src/portal/components/icons.tsx | 29 + .../procurement/ActionModal.stories.tsx | 51 - .../components/procurement/ActionModal.tsx | 166 -- .../procurement/DealJourney.stories.tsx | 30 - .../components/procurement/DealJourney.tsx | 99 -- .../procurement/DealStatusHero.stories.tsx | 6 +- .../components/procurement/DealStatusHero.tsx | 311 ++-- .../components/procurement/DocRow.stories.tsx | 62 - .../portal/components/procurement/DocRow.tsx | 89 -- .../procurement/DocumentLedger.stories.tsx | 30 - .../components/procurement/DocumentLedger.tsx | 160 -- .../procurement/LockedState.stories.tsx | 18 - .../components/procurement/LockedState.tsx | 36 - .../ProcurementAgreement.stories.tsx | 14 +- .../procurement/ProcurementAgreement.tsx | 369 +++-- .../procurement/ProcurementBanner.stories.tsx | 79 - .../procurement/ProcurementBanner.tsx | 65 +- .../procurement/ProcurementExtras.stories.tsx | 7 +- .../procurement/ProcurementExtras.tsx | 542 +++++-- .../procurement/ProcurementFlow.stories.tsx | 10 +- .../procurement/ProcurementFlow.tsx | 110 +- .../procurement/ProcurementHome.stories.tsx | 18 - .../procurement/ProcurementHome.tsx | 22 - .../procurement/ProcurementModal.stories.tsx | 4 +- .../procurement/ProcurementModal.tsx | 115 +- .../procurement/ProcurementStages.tsx | 102 +- .../components/procurement/QuoteBuilder.tsx | 377 ++++- .../procurement/StageStepper.stories.tsx | 20 - .../components/procurement/StageStepper.tsx | 52 - .../portal/components/procurement/format.ts | 32 +- .../procurement/useProcurement.test.tsx | 137 ++ .../components/procurement/useProcurement.ts | 165 +- .../portal/components/shared/FlowModal.css | 52 + .../components/shared/FlowModal.test.tsx | 94 ++ .../portal/components/shared/FlowModal.tsx | 52 + .../components/shared/StepModalHeader.css | 92 ++ .../components/shared/StepModalHeader.tsx | 127 ++ .../src/portal/contexts/UIContext.test.tsx | 47 + .../editor/src/portal/contexts/UIContext.tsx | 16 + .../src/portal/contexts/ViewContext.tsx | 2 - .../src/portal/hooks/useOnboardingProgress.ts | 63 - .../editor/src/portal/mocks/handlers/index.ts | 3 - .../src/portal/mocks/handlers/procurement.ts | 92 -- .../portal/mocks/handlers/procurementSaas.ts | 122 +- .../editor/src/portal/mocks/procurement.ts | 307 ---- .../portal/mocks/procurementMachine.test.ts | 86 - .../src/portal/mocks/procurementMachine.ts | 114 -- frontend/editor/src/portal/queries/keys.ts | 3 + frontend/editor/src/portal/queries/users.ts | 5 +- .../editor/src/portal/views/Home.stories.tsx | 1 + frontend/editor/src/portal/views/Home.tsx | 2 +- .../editor/src/portal/views/Procurement.css | 1383 ++++++----------- .../editor/src/portal/views/Procurement.tsx | 15 - frontend/eslint.config.mjs | 7 +- 107 files changed, 5347 insertions(+), 4601 deletions(-) create mode 100644 app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java create mode 100644 app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java create mode 100644 app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java create mode 100644 app/saas/src/main/java/stirling/software/saas/legal/LegalController.java create mode 100644 app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java create mode 100644 app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java create mode 100644 app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java create mode 100644 app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java create mode 100644 app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java create mode 100644 app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java create mode 100644 app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java create mode 100644 app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java create mode 100644 app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md create mode 100644 app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md create mode 100644 app/saas/src/main/resources/legal/eula/1.0.0/eula.md create mode 100644 app/saas/src/main/resources/legal/manifest.json create mode 100644 app/saas/src/main/resources/legal/sla/1.0.0/sla.md create mode 100644 app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md create mode 100644 app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java create mode 100644 frontend/editor/src/portal/api/externalUrl.test.ts create mode 100644 frontend/editor/src/portal/api/externalUrl.ts delete mode 100644 frontend/editor/src/portal/components/SetupChecklist.css delete mode 100644 frontend/editor/src/portal/components/SetupChecklist.stories.tsx delete mode 100644 frontend/editor/src/portal/components/SetupChecklist.tsx delete mode 100644 frontend/editor/src/portal/components/WelcomeBanner.css delete mode 100644 frontend/editor/src/portal/components/WelcomeBanner.stories.tsx delete mode 100644 frontend/editor/src/portal/components/WelcomeBanner.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/ActionModal.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/ActionModal.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/DealJourney.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/DealJourney.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/DocRow.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/DocRow.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/DocumentLedger.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/DocumentLedger.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/LockedState.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/LockedState.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/ProcurementBanner.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/ProcurementHome.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/ProcurementHome.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/StageStepper.stories.tsx delete mode 100644 frontend/editor/src/portal/components/procurement/StageStepper.tsx create mode 100644 frontend/editor/src/portal/components/procurement/useProcurement.test.tsx create mode 100644 frontend/editor/src/portal/components/shared/FlowModal.css create mode 100644 frontend/editor/src/portal/components/shared/FlowModal.test.tsx create mode 100644 frontend/editor/src/portal/components/shared/FlowModal.tsx create mode 100644 frontend/editor/src/portal/components/shared/StepModalHeader.css create mode 100644 frontend/editor/src/portal/components/shared/StepModalHeader.tsx create mode 100644 frontend/editor/src/portal/contexts/UIContext.test.tsx delete mode 100644 frontend/editor/src/portal/hooks/useOnboardingProgress.ts delete mode 100644 frontend/editor/src/portal/mocks/handlers/procurement.ts delete mode 100644 frontend/editor/src/portal/mocks/procurement.ts delete mode 100644 frontend/editor/src/portal/mocks/procurementMachine.test.ts delete mode 100644 frontend/editor/src/portal/mocks/procurementMachine.ts delete mode 100644 frontend/editor/src/portal/views/Procurement.tsx diff --git a/app/saas/build.gradle b/app/saas/build.gradle index 495f583a74..a954e6d05a 100644 --- a/app/saas/build.gradle +++ b/app/saas/build.gradle @@ -6,6 +6,11 @@ dependencies { implementation project(':common') implementation project(':proprietary') + // Markdown -> HTML for rendering versioned legal documents (agreement) to PDF via the + // shared FileToPdf/WeasyPrint path in :common. Same library the core Markdown-to-PDF tool uses. + implementation "org.commonmark:commonmark:$commonmarkVersion" + implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion" + api 'org.springframework.boot:spring-boot-starter-security' api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java b/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java index cd991cc44e..612c6e3171 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java @@ -20,7 +20,8 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; "stirling.software.saas.ai.repository", "stirling.software.saas.payg.repository", "stirling.software.saas.payg.bundle", - "stirling.software.saas.procurement.repository" + "stirling.software.saas.procurement.repository", + "stirling.software.saas.legal" }) @EntityScan({ "stirling.software.saas.accountlink", @@ -28,6 +29,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; "stirling.software.saas.billing.model", "stirling.software.saas.ai.model", "stirling.software.saas.payg", - "stirling.software.saas.procurement.model" + "stirling.software.saas.procurement.model", + "stirling.software.saas.legal" }) public class SaasJpaConfig {} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java new file mode 100644 index 0000000000..5698133fe8 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java @@ -0,0 +1,62 @@ +package stirling.software.saas.legal; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * An append-only record that a user accepted a versioned legal document at a particular moment in + * the product. Distinct from a signed agreement (which is a negotiated, signature-bearing artifact, + * see {@code ProcurementAgreementSignature}); this captures the lighter clickwrap consents — the + * EULA accepted at trial start and at quote generation — with the exact document version, so what + * was agreed is auditable even after the document versions up. + */ +@Entity +@Table(name = "legal_consent") +@NoArgsConstructor +@Getter +@Setter +public class LegalConsent implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "consent_id") + private Long consentId; + + @Column(name = "team_id") + private Long teamId; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "document_id", nullable = false, length = 64) + private String documentId; + + @Column(name = "document_version", nullable = false, length = 32) + private String documentVersion; + + // Where in the product the consent was given: "trial", "quote", etc. + @Column(name = "context", nullable = false, length = 32) + private String context; + + @Column(name = "signer_ip", length = 64) + private String signerIp; + + @CreationTimestamp + @Column(name = "consented_at", nullable = false, updatable = false) + private LocalDateTime consentedAt; +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java new file mode 100644 index 0000000000..9d79640cd0 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java @@ -0,0 +1,5 @@ +package stirling.software.saas.legal; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LegalConsentRepository extends JpaRepository {} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java new file mode 100644 index 0000000000..c6377f35b7 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java @@ -0,0 +1,47 @@ +package stirling.software.saas.legal; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** Records clickwrap consents to versioned legal documents (see {@link LegalConsent}). */ +@Slf4j +@Service +@Profile("saas") +@RequiredArgsConstructor +public class LegalConsentService { + + private final LegalDocumentRegistry registry; + private final LegalConsentRepository consents; + + /** + * Record that the given user accepted the current version of {@code documentId} in {@code + * context} (e.g. "trial", "quote"). No-op for an unknown document. Best-effort: callers treat a + * failure as non-fatal so it never blocks the flow the consent accompanies. + */ + @Transactional + public void record(Long teamId, Long userId, String documentId, String context, String ip) { + LegalDocumentMeta meta = registry.meta(documentId).orElse(null); + if (meta == null) { + log.warn("[legal] consent for unknown document '{}' ignored", documentId); + return; + } + LegalConsent consent = new LegalConsent(); + consent.setTeamId(teamId); + consent.setUserId(userId); + consent.setDocumentId(meta.id()); + consent.setDocumentVersion(meta.version()); + consent.setContext(context); + consent.setSignerIp(ip); + consents.save(consent); + log.info( + "[legal] consent recorded team={} doc={} v{} context={}", + teamId, + meta.id(), + meta.version(), + context); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalController.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalController.java new file mode 100644 index 0000000000..308b3e99bd --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalController.java @@ -0,0 +1,133 @@ +package stirling.software.saas.legal; + +import java.util.Optional; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Hidden; + +import jakarta.servlet.http.HttpServletRequest; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.TeamMembership; +import stirling.software.proprietary.security.database.repository.UserRepository; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.security.repository.TeamMembershipRepository; +import stirling.software.saas.util.AuthenticationUtils; + +/** + * Serves the versioned legal documents (EULA, SLA exhibit, subprocessors) for in-product viewing, + * and records the lighter clickwrap consents. The enterprise agreement itself is served + signed + * through the procurement controller, since it needs a quote to fill its Order Form. + */ +@Slf4j +@Hidden +@RestController +@RequestMapping("/api/v1/legal") +@Profile("saas") +@RequiredArgsConstructor +public class LegalController { + + private final LegalDocumentRegistry registry; + private final LegalConsentService consents; + private final TeamMembershipRepository memberRepo; + private final UserRepository userRepository; + + /** A legal document rendered for viewing: registry metadata + the static markdown body. */ + public record LegalDocumentResponse( + String docId, + String version, + String versionLabel, + String displayName, + String effectiveDate, + String status, + String markdown) {} + + public record ConsentRequest(String documentId, String context) {} + + /** Fetch a legal document's current version as markdown. 404 for an unknown document. */ + @GetMapping("/{docId}") + @PreAuthorize("isAuthenticated()") + public ResponseEntity document(@PathVariable String docId) { + return registry.meta(docId) + .>map( + meta -> + ResponseEntity.ok( + new LegalDocumentResponse( + meta.id(), + meta.version(), + meta.versionLabel(), + meta.displayName(), + meta.effectiveDate(), + meta.status(), + registry.staticMarkdown(docId)))) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * Record a clickwrap consent (e.g. the EULA accepted at trial start or quote generation). + * Best-effort — a teamless caller still returns 200 so the accompanying flow is never blocked. + */ + @PostMapping("/consent") + @PreAuthorize("isAuthenticated()") + public ResponseEntity consent( + @RequestBody ConsentRequest request, Authentication auth, HttpServletRequest http) { + if (request == null || request.documentId() == null || request.context() == null) { + return ResponseEntity.badRequest().build(); + } + Optional membership = primaryMembership(auth); + Long teamId = membership.map(m -> m.getTeam().getId()).orElse(null); + Long userId = membership.map(m -> m.getUser().getId()).orElse(null); + // Best-effort for real: consent is audit metadata, not an authorisation gate, so a failed + // write must not fail the trial start or quote generation this call accompanies. Previously + // that only held because the caller happened to swallow the 500. + try { + consents.record( + teamId, userId, request.documentId(), request.context(), clientIp(http)); + } catch (RuntimeException e) { + log.warn( + "[legal] consent not recorded doc={} context={}: {}", + request.documentId(), + request.context(), + e.getMessage()); + } + return ResponseEntity.ok().build(); + } + + private Optional primaryMembership(Authentication auth) { + User user; + try { + user = AuthenticationUtils.getCurrentUser(auth, userRepository); + } catch (SecurityException e) { + return Optional.empty(); + } + return memberRepo.findPrimaryMembership(user.getId()).stream().findFirst(); + } + + /** + * Best guess at the caller's address, for the audit record. + * + *

Informational only, and must stay that way: the first {@code X-Forwarded-For} hop is set + * by the client, so a stored address is trivially spoofable and is not evidence of where a + * consent or signature came from. Treat it as a hint when reconstructing events, never as + * proof. + */ + private static String clientIp(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-For"); + if (forwarded != null && !forwarded.isBlank()) { + return forwarded.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java new file mode 100644 index 0000000000..9f35e42053 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java @@ -0,0 +1,27 @@ +package stirling.software.saas.legal; + +import java.util.List; + +/** + * One legal document's registry entry, as declared in {@code legal/manifest.json}. Immutable + * snapshot loaded at startup by {@link LegalDocumentRegistry}. + * + *

{@code parts} lists the pieces, in render order, that make up the document. A plain entry + * (e.g. {@code "msa.md"}) is a static markdown file under {@code legal///}; an entry + * prefixed with {@code "@"} (e.g. {@code "@order-form"}) is a dynamic section that a document + * assembler generates at render time. + */ +public record LegalDocumentMeta( + String id, + String label, + String displayName, + String version, + String effectiveDate, + String status, + List parts) { + + /** Fully-qualified version label shown to users and stored on signatures, e.g. "SEA v0.9.1". */ + public String versionLabel() { + return label + " v" + version; + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java new file mode 100644 index 0000000000..ffbe030863 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java @@ -0,0 +1,151 @@ +package stirling.software.saas.legal; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +/** + * Loads the versioned legal-document registry from {@code legal/manifest.json} on startup and + * serves document metadata + rendered markdown from the classpath. + * + *

Publishing a new version of any document is a content-only change: drop the markdown under + * {@code legal///} and bump that document's {@code version} in the manifest — no + * code change. Signatures pin the exact {@code {id, version, contentHash}} they were signed against + * (see the procurement agreement flow), so historical documents stay reproducible. + * + *

Token slots of the form {{name}} in the markdown are filled at render time. This + * registry fills the document-level common tokens ({@code version}, {@code version_date}, {@code + * subprocessor_url}, {@code eula_url}); callers that need per-quote tokens (the enterprise + * agreement's Order Form) fill the rest. + */ +@Slf4j +@Service +public class LegalDocumentRegistry { + + private static final String MANIFEST = "legal/manifest.json"; + private static final Pattern TOKEN = Pattern.compile("\\{\\{\\s*([a-zA-Z0-9_]+)\\s*}}"); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final Map documents = new LinkedHashMap<>(); + private String subprocessorUrl = ""; + private String eulaUrl = ""; + + @PostConstruct + void load() throws IOException { + JsonNode root; + try (InputStream in = new ClassPathResource(MANIFEST).getInputStream()) { + root = objectMapper.readTree(in); + } + subprocessorUrl = root.path("subprocessorUrl").asText(""); + eulaUrl = root.path("eulaUrl").asText(""); + JsonNode docs = root.path("documents"); + docs.fieldNames() + .forEachRemaining( + id -> { + JsonNode d = docs.get(id); + List parts = + objectMapper.convertValue( + d.path("parts"), + objectMapper + .getTypeFactory() + .constructCollectionType( + List.class, String.class)); + documents.put( + id, + new LegalDocumentMeta( + id, + d.path("label").asText(id), + d.path("displayName").asText(id), + d.path("version").asText("0"), + d.path("effectiveDate").asText(""), + d.path("status").asText("draft"), + parts == null ? List.of() : parts)); + }); + log.info("[legal] loaded {} document(s) from {}", documents.size(), MANIFEST); + } + + public Optional meta(String docId) { + return Optional.ofNullable(documents.get(docId)); + } + + public String subprocessorUrl() { + return subprocessorUrl; + } + + public String eulaUrl() { + return eulaUrl; + } + + /** Document-level tokens available to every document (before any per-quote tokens). */ + public Map commonTokens(LegalDocumentMeta meta) { + Map t = new LinkedHashMap<>(); + t.put("version", meta.version()); + t.put("version_date", meta.effectiveDate()); + t.put("subprocessor_url", subprocessorUrl); + t.put("eula_url", eulaUrl); + return t; + } + + /** Read one static markdown part of a document from the classpath. */ + public String readPart(LegalDocumentMeta meta, String partFile) { + String path = "legal/" + meta.id() + "/" + meta.version() + "/" + partFile; + try (InputStream in = new ClassPathResource(path).getInputStream()) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Missing legal document part: " + path, e); + } + } + + /** + * The concatenated static parts of a document (dynamic {@code @}-parts skipped), with only the + * common tokens filled. Use for fully-static documents (EULA, SLA, subprocessors). + */ + public String staticMarkdown(String docId) { + LegalDocumentMeta meta = + meta(docId) + .orElseThrow( + () -> new IllegalArgumentException("Unknown document: " + docId)); + Map tokens = commonTokens(meta); + StringBuilder sb = new StringBuilder(); + for (String part : meta.parts()) { + if (part.startsWith("@")) continue; // dynamic section — not part of the static body + if (sb.length() > 0) sb.append("\n\n"); + sb.append(fill(readPart(meta, part), tokens)); + } + return sb.toString(); + } + + /** Replace {@code {{token}}} slots; unknown tokens are left intact so gaps are visible. */ + public static String fill(String markdown, Map tokens) { + Matcher m = TOKEN.matcher(markdown); + StringBuilder out = new StringBuilder(); + while (m.find()) { + String key = m.group(1); + String value = tokens.get(key); + m.appendReplacement( + out, + value == null + ? Matcher.quoteReplacement(m.group(0)) + : Matcher.quoteReplacement(value)); + } + m.appendTail(out); + return out.toString(); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/payg/bundle/PrepaidBundle.java b/app/saas/src/main/java/stirling/software/saas/payg/bundle/PrepaidBundle.java index e2dff403a7..f864bf4d98 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/bundle/PrepaidBundle.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/bundle/PrepaidBundle.java @@ -34,17 +34,22 @@ import lombok.Setter; @Entity @Table( name = "payg_prepaid_bundle", - // Declared here for ddl-auto (fresh schemas) and to document intent. The authoritative creator - // in production is the Supabase CLI migration 20260720000000_payg_prepaid_bundle, which builds + // Declared here for ddl-auto (fresh schemas) and to document intent. The authoritative + // creator + // in production is the Supabase CLI migration 20260720000000_payg_prepaid_bundle, which + // builds // the partial forms (WHERE units_remaining > 0 / WHERE stripe_ref IS NOT NULL). Flyway was // retired for SaaS (#7100), so there is no migration twin — names match the CLI migration. indexes = { - // Hot-path FIFO draw lookup — findDrawableForUpdate runs a locked read on every billable - // charge past the free grant; without it that degrades to a locked scan as the table grows. + // Hot-path FIFO draw lookup — findDrawableForUpdate runs a locked read on every + // billable + // charge past the free grant; without it that degrades to a locked scan as the table + // grows. @Index( name = "idx_payg_prepaid_bundle_team_expiry", columnList = "team_id, expires_at"), - // One pool per Stripe payment — the idempotency guard so a redelivered invoice.paid can't + // One pool per Stripe payment — the idempotency guard so a redelivered invoice.paid + // can't // credit the same purchase twice. @Index( name = "uq_payg_prepaid_bundle_stripe_ref", diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java b/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java index d67a0fddbf..1c495b3afd 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; + import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.enumeration.TeamRole; @@ -30,6 +32,8 @@ import stirling.software.proprietary.security.database.repository.UserRepository import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.repository.TeamMembershipRepository; import stirling.software.saas.procurement.config.ProcurementConfigurationProperties; +import stirling.software.saas.procurement.legal.AgreementSigning; +import stirling.software.saas.procurement.model.ProcurementAgreementSignature; import stirling.software.saas.procurement.model.ProcurementDeal; import stirling.software.saas.procurement.model.ProcurementQuote; import stirling.software.saas.procurement.model.QuoteDetails; @@ -178,7 +182,17 @@ public class ProcurementController { String taxId) {} /** Trial setup captured before the trial starts: deployment target + seat count. */ - public record StartTrialRequest(String deployment, int users) {} + /** + * Setup step 2 collects the buying entity; all of it is optional so an older client still + * starts. + */ + public record StartTrialRequest( + String deployment, + int users, + String businessName, + String contactName, + String contactEmail, + String inviteEmails) {} public record SnapshotResponse( Long dealId, @@ -190,8 +204,33 @@ public class ProcurementController { int trialExtensionsUsed, boolean licensed, String licenseKey, + // Version label of the signed agreement PDF available for download, else null. + String agreementSignedVersion, + // Buying entity captured at trial setup; null on deals started before that step. + String businessName, + String contactName, + String contactEmail, QuoteResponse latestQuote) {} + /** The filled agreement for review: registry metadata + the rendered markdown body. */ + public record AgreementDocumentResponse( + String docId, + String version, + String versionLabel, + String displayName, + String effectiveDate, + String status, + String markdown) {} + + /** Buyer-supplied signing inputs from the agreement stage. */ + public record SignAgreementRequest( + String customerLegalName, + String signatoryName, + String signatoryTitle, + boolean authorityConfirmed) {} + + public record SignAgreementResponse(Long signatureId, String versionLabel, boolean pdfStored) {} + // ---- endpoints ---------------------------------------------------------- /** @@ -213,7 +252,8 @@ public class ProcurementController { } private static final SnapshotResponse EMPTY_SNAPSHOT = - new SnapshotResponse(null, null, null, 0, null, null, 0, false, null, null); + new SnapshotResponse( + null, null, null, 0, null, null, 0, false, null, null, null, null, null, null); /** * Download the offline / air-gapped licence file (.lic) for the team — available for an @@ -239,6 +279,15 @@ public class ProcurementController { .orElseGet(() -> ResponseEntity.notFound().build()); } + /** Mark the account as looking at enterprise. Idempotent; never disturbs an existing deal. */ + @PostMapping("/interest") + @PreAuthorize("isAuthenticated()") + public ResponseEntity recordInterest(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return ResponseEntity.ok(toSnapshot(procurement.recordInterest(teamId), true)); + } + @PostMapping("/trial/start") @PreAuthorize("isAuthenticated()") public ResponseEntity startTrial( @@ -248,8 +297,30 @@ public class ProcurementController { // Body is optional so an older client (no setup step) still starts a cloud trial. String deployment = request != null ? request.deployment() : null; int seats = request != null ? request.users() : 0; - return ResponseEntity.ok( - toSnapshot(procurement.startTrial(teamId, deployment, seats), true)); + ProcurementDeal deal; + try { + deal = + procurement.startTrial( + teamId, + deployment, + seats, + request != null ? request.businessName() : null, + request != null ? request.contactName() : null, + request != null ? request.contactEmail() : null, + request != null ? request.inviteEmails() : null); + } catch (IllegalStateException e) { + // Past the trial the deal holds a committed licence; restarting would replace it. + log.warn("[procurement] trial start rejected team={}: {}", teamId, e.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + // After the trial exists, so a rejected invite can never stop it starting. + if (request != null) { + procurement.sendTrialInvites( + teamId, + primaryMembership(auth).map(TeamMembership::getUser).orElse(null), + request.inviteEmails()); + } + return ResponseEntity.ok(toSnapshot(deal, true)); } @PostMapping("/trial/extend") @@ -270,8 +341,17 @@ public class ProcurementController { @RequestBody QuoteRequest request, Authentication auth) { Long teamId = requireLeader(auth); if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - return ResponseEntity.ok( - toQuote(procurement.buildQuote(teamId, request.toConfig(), request.toDetails()))); + try { + return ResponseEntity.ok( + toQuote( + procurement.buildQuote( + teamId, request.toConfig(), request.toDetails()))); + } catch (IllegalStateException e) { + // Below the minimum deal size, or the deal is already live. A client error, not a + // fault. + log.warn("[procurement] quote rejected team={}: {}", teamId, e.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } } // Issue + accept are Supabase edge functions (they own Stripe): issue-procurement-quote turns a @@ -293,6 +373,111 @@ public class ProcurementController { } } + /** + * The filled Stirling Enterprise Agreement (MSA + Order Form + DPA) for the team's current + * quote, as markdown, for the buyer to review before signing. 404 when there's no quote yet. + */ + @GetMapping("/agreement/document") + @PreAuthorize("isAuthenticated()") + public ResponseEntity agreementDocument(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return procurement + .agreementDocument(teamId) + .>map( + a -> + ResponseEntity.ok( + new AgreementDocumentResponse( + a.docId(), + a.version(), + a.versionLabel(), + a.displayName(), + a.effectiveDate(), + a.status(), + a.markdown()))) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * Record a signed agreement: capture the typed legal name / signatory / title / authority, pin + * the exact document version + content hash + variable snapshot, and store the rendered PDF + * (best-effort). The caller then proceeds to accept the quote as before. + */ + @PostMapping("/agreement/sign") + @PreAuthorize("isAuthenticated()") + public ResponseEntity signAgreement( + @RequestBody SignAgreementRequest request, + Authentication auth, + HttpServletRequest http) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + if (request == null + || request.signatoryName() == null + || request.signatoryName().isBlank() + || !request.authorityConfirmed()) { + return ResponseEntity.badRequest().build(); + } + try { + ProcurementAgreementSignature sig = + procurement.signAgreement( + teamId, + new AgreementSigning( + request.customerLegalName(), + request.signatoryName(), + request.signatoryTitle(), + request.authorityConfirmed()), + clientIp(http)); + return ResponseEntity.ok( + new SignAgreementResponse( + sig.getSignatureId(), sig.getDocumentLabel(), sig.getPdf() != null)); + } catch (IllegalStateException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + } + + /** Download the stored signed-agreement PDF for the team. 404 if none was rendered/stored. */ + @GetMapping("/agreement/signature/pdf") + @PreAuthorize("isAuthenticated()") + public ResponseEntity signaturePdf(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return procurement + .signedAgreementPdf(teamId) + .>map( + pdf -> + ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment;" + + " filename=\"stirling-enterprise-agreement.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(pdf)) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * Download the current (unsigned) agreement as a PDF — the document shown at the sign step. 404 + * when there's no quote yet or the render runtime is unavailable. + */ + @GetMapping("/agreement/document/pdf") + @PreAuthorize("isAuthenticated()") + public ResponseEntity agreementDocumentPdf(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return procurement + .agreementDocumentPdf(teamId) + .>map( + pdf -> + ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment;" + + " filename=\"stirling-enterprise-agreement.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(pdf)) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + /** * Provision on accept: upgrade the team's licence to the committed annual term, valid * immediately. Called server-side by the accept edge function (ROLE_ADMIN via X-API-Key) once @@ -311,9 +496,38 @@ public class ProcurementController { } } + /** + * Go live once payment settles: advance the deal to active and re-affirm the annual licence. + * Called server-side by the {@code invoice.paid} webhook (ROLE_ADMIN via X-API-Key), alongside + * {@code /provision}, which runs earlier at accept and deliberately leaves the stage alone. + * + *

Answers 200 when the team has no deal at all, rather than erroring: a committed + * subscription can be closed directly in Stripe by sales with no portal deal behind it, and a + * non-2xx would have Stripe retry a webhook that can never succeed. + * + *

{@code invoiceId} is what makes this idempotent without swallowing renewals: the same + * invoice twice is a redelivery, a different one is next year's payment and has to re-issue the + * licence. Optional so an older caller still works, at the cost of that distinction. + */ + @PostMapping("/activate") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity activate( + @RequestParam("teamId") long teamId, + @RequestParam(value = "invoiceId", required = false) String invoiceId) { + try { + procurement.markLive(teamId, invoiceId); + } catch (IllegalStateException e) { + log.info( + "[procurement] activate skipped, no deal for team={}: {}", + teamId, + e.getMessage()); + } + return ResponseEntity.ok().build(); + } + /** * Demo/manual stand-in for the {@code invoice.paid} webhook: mark the deal live (issue the - * annual licence, advance to active). The real go-live is webhook-driven once payment settles. + * annual licence, advance to active). Production go-live runs through {@code /activate}. */ @PostMapping("/go-live") @PreAuthorize("isAuthenticated()") @@ -360,6 +574,21 @@ public class ProcurementController { .orElse(null); } + /** + * Best-effort client IP for the signature record: first X-Forwarded-For hop, else the peer. + * + *

Informational only. That header is client-set, so {@code signer_ip} is spoofable and is + * not evidence of where a signature came from — the document hash and version are what make the + * record trustworthy. Treat the address as a hint, never as proof. + */ + private static String clientIp(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-For"); + if (forwarded != null && !forwarded.isBlank()) { + return forwarded.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } + /** * Build the snapshot for a deal. {@code includeLicenseKey} is true only for the team leader; a * member sees {@code licensed} but not the key itself (see {@link #snapshot}). Mutation @@ -381,6 +610,10 @@ public class ProcurementController { deal.getTrialExtensionsUsed(), deal.getLicenseRef() != null, includeLicenseKey ? deal.getLicenseRef() : null, + procurement.signedAgreementLabel(deal.getDealId()).orElse(null), + deal.getBusinessName(), + deal.getContactName(), + deal.getContactEmail(), latest); } diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java b/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java index 98ec0cd573..ad745cf5bc 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java @@ -30,4 +30,17 @@ public class ProcurementConfigurationProperties { * /go-live is a stand-in for the invoice.paid webhook and would let a leader activate unpaid. */ private boolean demoControlsEnabled = false; + + /** + * Smallest annual fee, in minor units, that may be quoted. The pricing curve has no natural + * floor — a small enough committed volume rounds the meter to zero — and every registered user + * is the leader of their own team, so without this any signup could price a $0 enterprise + * quote, accept it, and be provisioned a committed licence. 12_000_00 is USD 12,000/yr, the + * self-hosted deploy fee, chosen so the floor cannot sit below a line item the quote itself can + * contain. + * + *

This is a commercial number, not a technical one: set it to whatever the smallest + * enterprise deal you will actually sign is. Zero disables the check. + */ + private long minAnnualNetMinor = 12_000_00L; } diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java new file mode 100644 index 0000000000..c641f2e596 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java @@ -0,0 +1,278 @@ +package stirling.software.saas.procurement.legal; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.saas.legal.LegalDocumentMeta; +import stirling.software.saas.legal.LegalDocumentRegistry; +import stirling.software.saas.procurement.model.ProcurementQuote; +import stirling.software.saas.procurement.pricing.ProcurementPricingService; +import stirling.software.saas.procurement.pricing.QuoteConfig; +import stirling.software.saas.procurement.pricing.QuoteLineItem; + +/** + * Builds the full Stirling Enterprise Agreement for a specific quote: the static MSA (Part A) and + * DPA (Part C) from the {@link LegalDocumentRegistry}, with the dynamic Order Form (Part B) + * generated from the quote and slotted where the manifest's {@code @order-form} part sits. + * + *

Only the Order Form varies per deal; the MSA and DPA bodies are rendered verbatim with token + * substitution. The set of values used is returned as {@code variablesJson} so a signature can pin + * exactly what was rendered. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AgreementAssembler { + + public static final String DOC_ID = "enterprise-agreement"; + + private static final DateTimeFormatter DATE = + DateTimeFormatter.ofPattern("MMMM d, yyyy", Locale.US); + private static final String BLANK = "\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_"; + + private final LegalDocumentRegistry registry; + private final ProcurementPricingService pricing; + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Render the agreement for a quote. {@code signing} is null for a preview (before signing) — + * the effective date and signature block then read as blanks / "On signature". + */ + public AssembledAgreement assemble(ProcurementQuote quote, AgreementSigning signing) { + LegalDocumentMeta meta = + registry.meta(DOC_ID) + .orElseThrow( + () -> + new IllegalStateException( + "Enterprise agreement not registered")); + + Map tokens = tokens(quote, signing, meta); + + StringBuilder md = new StringBuilder(); + for (String part : meta.parts()) { + if (md.length() > 0) md.append("\n\n"); + if ("@order-form".equals(part)) { + md.append(LegalDocumentRegistry.fill(orderForm(quote, tokens), tokens)); + } else { + md.append(LegalDocumentRegistry.fill(registry.readPart(meta, part), tokens)); + } + } + + String variablesJson; + try { + variablesJson = objectMapper.writeValueAsString(tokens); + } catch (Exception e) { + variablesJson = "{}"; + } + + return new AssembledAgreement( + meta.id(), + meta.version(), + meta.versionLabel(), + meta.displayName(), + meta.effectiveDate(), + meta.status(), + md.toString(), + variablesJson); + } + + private Map tokens( + ProcurementQuote quote, AgreementSigning signing, LegalDocumentMeta meta) { + QuoteConfig cfg = toConfig(quote); + boolean signed = signing != null; + + String legalName = + signed && notBlank(signing.customerLegalName()) + ? signing.customerLegalName().trim() + : (notBlank(quote.getBusinessName()) + ? quote.getBusinessName().trim() + : "Customer"); + + Map t = new LinkedHashMap<>(registry.commonTokens(meta)); + t.put("effective_date", signed ? LocalDate.now().format(DATE) : "On signature"); + t.put("customer_legal_name", cell(legalName)); + t.put("quote_ref", nz(quote.getQuoteNumber())); + t.put("deployment", ProcurementPricingService.deploymentName(quote.getDeployment())); + t.put("committed_pdfs_yr", String.format(Locale.US, "%,d", Math.max(0, quote.getVolume()))); + t.put("posture", ProcurementPricingService.postureName(quote.getIntensity())); + t.put("processes_per_pdf", String.valueOf(Math.max(1, quote.getIntensity()))); + t.put("rate_per_pdf", String.format(Locale.US, "$%.4f", pricing.effectiveRatePerPdf(cfg))); + t.put("term_years", String.valueOf(quote.getTermYears())); + t.put("term_discount_pct", pricing.termDiscountPct(quote.getTermYears()) + "%"); + t.put("sla_tier", slaTier(quote.getServiceLevel())); + t.put("annual_fee_y1", money(quote.getAnnualNetMinor())); + t.put("contract_total", money(quote.getTcvMinor())); + t.put("elected_or_not", quote.isIndemnification() ? "Elected" : "Not elected"); + t.put("po_number", notBlank(quote.getPoNumber()) ? cell(quote.getPoNumber()) : "—"); + t.put( + "customer_signatory", + signed && notBlank(signing.signatoryName()) + ? cell(signing.signatoryName()) + : BLANK); + t.put( + "customer_signatory_title", + signed && notBlank(signing.signatoryTitle()) + ? cell(signing.signatoryTitle()) + : BLANK); + return t; + } + + /** + * Make a buyer-supplied value safe to slot into a markdown table cell. + * + *

An unescaped {@code |} or newline splits the cell and breaks the Order Form's table. That + * matters beyond appearance: this markdown is what gets hashed into the signature record, so a + * value that restructures the table means the SHA-256 we keep as proof covers a document + * reading differently from the one the signatory saw. + */ + private static String cell(String raw) { + return raw.trim().replace("|", "\\|").replaceAll("\\s*\\R+\\s*", " "); + } + + /** Part B — the Order Form. Generated from the quote; the only per-deal section. */ + private String orderForm(ProcurementQuote quote, Map t) { + String date = t.get("effective_date"); + String signatory = t.get("customer_signatory"); + String signatoryTitle = t.get("customer_signatory_title"); + + StringBuilder sb = new StringBuilder(); + sb.append("## Part B — Order Form · {{quote_ref}}\n\n"); + sb.append("| Term | Value |\n| --- | --- |\n"); + row(sb, "Customer", "{{customer_legal_name}}"); + row(sb, "Subscription", "Enterprise · {{deployment}}"); + row(sb, "Purchase order", "{{po_number}}"); + row(sb, "Committed Volume", "{{committed_pdfs_yr}} PDFs / year at the {{posture}} posture"); + row(sb, "Committed rate", "{{rate_per_pdf}} per PDF"); + row(sb, "Service level", "{{sla_tier}} (per SLA Exhibit)"); + row( + sb, + "Term", + "{{term_years}} year(s) · term discount {{term_discount_pct}} on committed processing"); + row(sb, "Itemized services", itemizedServices(quote)); + row(sb, "Annual Fee (year 1)", "{{annual_fee_y1}}"); + row(sb, "Total (paid in advance)", "{{contract_total}}"); + row(sb, "Escalator", "+3% at each anniversary during the Term"); + row( + sb, + "Payment", + "Full {{term_years}}-year term invoiced in advance on acceptance · net 30 · ACH," + + " wire, or check"); + row(sb, "Overage", "Committed rate, billed quarterly in arrears"); + row( + sb, + "Data schedule", + "First 25 MB per file included; each additional 25 MB or part thereof (decimal MB," + + " rounded up per file, measured once at ingestion) draws down 1 PDF Process." + + " Frozen for the Term (MSA §3.5)."); + row( + sb, + "Drawdown schedule", + "{{posture}}: {{processes_per_pdf}} PDF Processes per PDF (MSA §3.3, frozen for the Term)"); + row( + sb, + "Enhanced IP Protection", + "{{elected_or_not}} — extends §7.3 to patent claims at the §8.2 super-cap"); + row(sb, "Standard terms", "SSO, SCIM, RBAC, and audit logs included."); + + sb.append( + "\n**Itemized services menu (include as elected):** Self-hosted deployment $12,000/yr" + + " · Air-gapped deployment $36,000/yr · Dedicated SE/CSM $30,000/yr · Enhanced IP" + + " Protection (patent coverage, Section 7.3) 5% of committed processing fees ·" + + " Onboarding & training $7,500 one-time · Quarterly business reviews $8,000/yr." + + " Baseline IP indemnification (copyright, trademark, trade secret) is included at" + + " no charge.\n\n"); + sb.append( + "**Signatures.** By signing, each signatory represents they have authority to bind" + + " their Party. Signatures delivered electronically or in counterparts are" + + " effective as originals.\n\n"); + sb.append("| Provider | Customer |\n| --- | --- |\n"); + sb.append("| Stirling PDF, Inc. | {{customer_legal_name}} |\n"); + sb.append("| Name: Matt Joseph | Name: ").append(signatory).append(" |\n"); + sb.append("| Title: CEO | Title: ").append(signatoryTitle).append(" |\n"); + sb.append("| Date: ").append(date).append(" | Date: ").append(date).append(" |\n"); + return sb.toString(); + } + + /** + * The elected add-on lines, taken from the quote's stored breakdown (excludes the base meter). + */ + private String itemizedServices(ProcurementQuote quote) { + List lines = parseLineItems(quote.getLineItemsJson()); + List elected = new ArrayList<>(); + for (QuoteLineItem li : lines) { + if (li.key().equals("usage") + || li.key().equals("seats") + || li.key().equals("multi-year")) { + continue; + } + String suffix = li.kind() == QuoteLineItem.Kind.ONE_TIME ? " (one-time)" : "/yr"; + elected.add(li.label() + " " + money(li.amountMinor()) + suffix); + } + return elected.isEmpty() ? "None elected" : String.join(" · ", elected); + } + + private List parseLineItems(String json) { + if (json == null || json.isBlank()) return List.of(); + try { + return objectMapper.readValue( + json, + objectMapper + .getTypeFactory() + .constructCollectionType(List.class, QuoteLineItem.class)); + } catch (Exception e) { + log.warn("[legal] could not parse quote line items for the order form", e); + return List.of(); + } + } + + private static QuoteConfig toConfig(ProcurementQuote q) { + int users = q.getSeats() == null ? 0 : q.getSeats(); + return new QuoteConfig( + q.getVolume(), + users, + q.getIntensity(), + q.getSizeMult(), + q.getDeployment(), + q.getTermYears(), + q.getServiceLevel(), + q.isIndemnification(), + q.isTraining(), + q.isQbr(), + q.getCurrency()); + } + + private static void row(StringBuilder sb, String term, String value) { + sb.append("| ").append(term).append(" | ").append(value).append(" |\n"); + } + + private static String slaTier(String serviceLevel) { + if ("dedicated".equalsIgnoreCase(serviceLevel)) return "Dedicated"; + if ("priority".equalsIgnoreCase(serviceLevel)) return "Priority"; + return "Standard"; + } + + /** Minor units (cents) → whole-dollar display; the quote figures are whole dollars. */ + private static String money(long minor) { + return String.format(Locale.US, "$%,d", minor / 100L); + } + + private static boolean notBlank(String s) { + return s != null && !s.isBlank(); + } + + private static String nz(String s) { + return s == null ? "" : s; + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java new file mode 100644 index 0000000000..8204df6514 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java @@ -0,0 +1,71 @@ +package stirling.software.saas.procurement.legal; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.commonmark.Extension; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.CustomHtmlSanitizer; +import stirling.software.common.util.FileToPdf; +import stirling.software.common.util.TempFileManager; + +/** + * Renders an assembled agreement's markdown to a PDF, dogfooding Stirling's own conversion path: + * commonmark (markdown → HTML) then {@link FileToPdf#convertHtmlToPdf} (HTML → PDF via WeasyPrint), + * the same pipeline as the product's Markdown-to-PDF tool. + * + *

The signed PDF is a stored artifact, but it must never block signing: {@link #tryRender} + * returns {@code null} if the conversion runtime (WeasyPrint) is unavailable, so the signature is + * still recorded and the buyer keeps the on-the-fly download. + */ +@Service +@RequiredArgsConstructor +public class AgreementPdfRenderer { + + private final RuntimePathConfig runtimePathConfig; + private final TempFileManager tempFileManager; + private final CustomHtmlSanitizer customHtmlSanitizer; + private final CustomPDFDocumentFactory pdfDocumentFactory; + + private static final List EXTENSIONS = List.of(TablesExtension.create()); + + /** Render to PDF, or return null if the conversion runtime isn't available. */ + public byte[] tryRender(String markdown) { + try { + return render(markdown); + } catch (Exception e) { + org.slf4j.LoggerFactory.getLogger(AgreementPdfRenderer.class) + .warn( + "[legal] agreement PDF render unavailable; recording signature without a" + + " stored PDF: {}", + e.getMessage()); + return null; + } + } + + private byte[] render(String markdown) throws Exception { + Parser parser = Parser.builder().extensions(EXTENSIONS).build(); + Node document = parser.parse(markdown); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(EXTENSIONS).build(); + String html = renderer.render(document); + + byte[] pdfBytes = + FileToPdf.convertHtmlToPdf( + runtimePathConfig.getWeasyPrintPath(), + null, + html.getBytes(StandardCharsets.UTF_8), + "agreement.html", + tempFileManager, + customHtmlSanitizer); + return pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java new file mode 100644 index 0000000000..600ecda415 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java @@ -0,0 +1,12 @@ +package stirling.software.saas.procurement.legal; + +/** + * The buyer-supplied inputs captured at the moment of signing the enterprise agreement: the legal + * entity name, the signatory's typed name and title, and their representation of authority to bind. + * Null when the agreement is rendered for preview (before signing). + */ +public record AgreementSigning( + String customerLegalName, + String signatoryName, + String signatoryTitle, + boolean authorityConfirmed) {} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java new file mode 100644 index 0000000000..3e88cf555a --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java @@ -0,0 +1,16 @@ +package stirling.software.saas.procurement.legal; + +/** + * A rendered enterprise agreement: the full markdown the buyer sees (MSA + Order Form + DPA, tokens + * filled), plus the registry metadata that pins it. {@code variablesJson} is the exact set of + * Order-Form values as rendered, stored alongside a signature so the document is reproducible. + */ +public record AssembledAgreement( + String docId, + String version, + String versionLabel, + String displayName, + String effectiveDate, + String status, + String markdown, + String variablesJson) {} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java new file mode 100644 index 0000000000..425b100ba9 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java @@ -0,0 +1,86 @@ +package stirling.software.saas.procurement.model; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * An immutable record of a signed enterprise agreement. Each signature pins the exact legal + * document it was signed against — {@code documentId} + {@code documentVersion} + a SHA-256 {@code + * contentHash} of the rendered markdown — plus the Order-Form variable snapshot and the typed + * signatory details, so the agreement stays reproducible even after the templates version up. The + * rendered PDF is stored when the conversion runtime is available. + */ +@Entity +@Table(name = "procurement_agreement_signature") +@NoArgsConstructor +@Getter +@Setter +public class ProcurementAgreementSignature implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "signature_id") + private Long signatureId; + + @Column(name = "deal_id", nullable = false) + private Long dealId; + + @Column(name = "quote_id", nullable = false) + private Long quoteId; + + // Which legal document, and which version of it, was signed. + @Column(name = "document_id", nullable = false, length = 64) + private String documentId; + + @Column(name = "document_version", nullable = false, length = 32) + private String documentVersion; + + @Column(name = "document_label", length = 64) + private String documentLabel; + + // SHA-256 (hex) of the exact rendered agreement markdown the buyer accepted. + @Column(name = "content_hash", nullable = false, length = 64) + private String contentHash; + + // The Order-Form variable values as rendered, so the document can be reproduced. + @Column(name = "variables_json", columnDefinition = "text") + private String variablesJson; + + @Column(name = "customer_legal_name", length = 255) + private String customerLegalName; + + @Column(name = "signatory_name", nullable = false, length = 255) + private String signatoryName; + + @Column(name = "signatory_title", length = 255) + private String signatoryTitle; + + @Column(name = "authority_confirmed", nullable = false) + private boolean authorityConfirmed; + + @Column(name = "signer_ip", length = 64) + private String signerIp; + + // The rendered PDF artifact; null when the conversion runtime was unavailable at signing. + @Column(name = "pdf") + private byte[] pdf; + + @CreationTimestamp + @Column(name = "signed_at", nullable = false, updatable = false) + private LocalDateTime signedAt; +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java index 3a5d3db902..f0db2fb6db 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java @@ -34,6 +34,13 @@ public class ProcurementDeal implements Serializable { private static final long serialVersionUID = 1L; + /** + * Interest, before any commitment: the account asked about enterprise but has not started a + * trial. Kept as a real stage so intent survives a refresh, so the enterprise surface is only + * shown to accounts that asked for it, and so drop-off at the cheapest step is measurable. + */ + public static final String STAGE_EXPLORING = "exploring"; + public static final String STAGE_TRIAL = "trial"; public static final String STAGE_QUOTE = "quote"; public static final String STAGE_AGREEMENT = "security"; @@ -69,12 +76,38 @@ public class ProcurementDeal implements Serializable { @Column(name = "trial_extensions_used", nullable = false) private int trialExtensionsUsed; + // Captured at trial setup, so the buying entity is known before any quote exists — the quote's + // own copies seed from these and may then diverge (a deal can change hands mid-cycle). + // Nullable: trials started before this step, and older clients, supply none. + @Column(name = "business_name", length = 255) + private String businessName; + + @Column(name = "contact_name", length = 255) + private String contactName; + + @Column(name = "contact_email", length = 320) + private String contactEmail; + + // Addresses the buyer named at setup. Kept as the record of what was asked for; the invitations + // themselves go out through the team-invite path when the trial starts. + @Column(name = "invite_emails", length = 2000) + private String inviteEmails; + @Column(name = "license_ref", length = 128) private String licenseRef; @Column(name = "subscription_id", length = 255) private String subscriptionId; + /** + * The last Stripe invoice whose payment was applied to this deal. Distinguishes a redelivered + * {@code invoice.paid} for a payment already handled from a genuine renewal, which has to + * re-issue: the committed licence expires term years from issue, so a renewal that doesn't + * re-issue leaves the licence lapsing after the customer has paid. + */ + @Column(name = "last_paid_invoice_id", length = 255) + private String lastPaidInvoiceId; + @Column(name = "accepted_quote_id") private Long acceptedQuoteId; diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java index aa94128172..19e9e01004 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java @@ -47,7 +47,11 @@ public class ProcurementQuote implements Serializable { @Column(name = "deal_id", nullable = false) private Long dealId; - @Column(name = "quote_number", nullable = false, length = 64) + /** + * Stripe's quote number, the deal's one buyer-facing reference. Null until the quote is issued: + * Stripe assigns it at finalisation, and the issue edge function writes it back then. + */ + @Column(name = "quote_number", length = 64) private String quoteNumber; @Column(name = "status", nullable = false, length = 24) diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java b/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java index 08ade5a157..5d1234e9fc 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java @@ -60,12 +60,7 @@ public class ProcurementPricingService { rates.discountPerDoubling() * (Math.log(runVol / (double) RUN_CURVE_KNEE) / LOG2)) : 0.0; - double rate = Math.max(rates.floorRatePerRun(), rates.listRatePerRun() * (1.0 - volDisc)); - // File-size multiplier (D93): larger, image-heavy PDFs cost more OCR/compute/storage. Folds - // into the per-run rate after the floor, so it flows through the meter, TCV and renewal. - // QuoteConfig has already snapped it to a known tier, so a tampered request can't sneak a - // cheaper factor in. - rate *= cfg.sizeMult(); + double rate = perRunRate(cfg, rates); double termDisc = rates.termDiscount(cfg.termYears()); // The meter is a whole-dollar figure (the quote reads in dollars), then minor units. @@ -160,6 +155,48 @@ public class ProcurementPricingService { return new QuoteBreakdown(lines, annualNet, tcv, renewalAnnual, cfg.currency()); } + /** + * The per-run rate after the committed-volume curve, the half-cent floor, and the file-size + * multiplier — the same value {@link #price} meters against. Extracted so read-only callers + * (the Order Form) can quote it without re-deriving the curve. + */ + private static double perRunRate(QuoteConfig cfg, PricingRates rates) { + long runVol = Math.max(0, cfg.volume()) * (long) Math.max(1, cfg.intensity()); + double volDisc = + runVol > RUN_CURVE_KNEE + ? Math.min( + 0.5, + rates.discountPerDoubling() + * (Math.log(runVol / (double) RUN_CURVE_KNEE) / LOG2)) + : 0.0; + return Math.max(rates.floorRatePerRun(), rates.listRatePerRun() * (1.0 - volDisc)) + * cfg.sizeMult(); + } + + /** + * The effective per-PDF rate at the chosen posture, in dollars (4-decimal quote figure). This + * is what the Order Form and quote copy speak in — never the per-run rate. Read-only; does not + * affect billing. + */ + public double effectiveRatePerPdf(QuoteConfig cfg) { + return perRunRate(cfg, PricingRates.defaults()) * Math.max(1, cfg.intensity()); + } + + /** The multi-year term discount as a whole-percent figure for the Order Form (0.05 → 5). */ + public int termDiscountPct(int termYears) { + return (int) Math.round(PricingRates.defaults().termDiscount(termYears) * 100.0); + } + + /** Buyer-facing posture name (Essentials / Governed / Regulated) for the given intensity. */ + public static String postureName(int intensity) { + return postureLabel(intensity); + } + + /** Buyer-facing deployment name (Stirling Cloud / Self-hosted / Air-gapped). */ + public static String deploymentName(String deployment) { + return deploymentLabel(deployment); + } + /** The default CPI escalator (fraction) applied to the annual fee on each post-term renewal. */ public double cpiEscalator() { return PricingRates.defaults().cpiEscalator(); diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java b/app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java new file mode 100644 index 0000000000..419189617e --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java @@ -0,0 +1,29 @@ +package stirling.software.saas.procurement.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import stirling.software.saas.procurement.model.ProcurementAgreementSignature; + +public interface ProcurementAgreementSignatureRepository + extends JpaRepository { + + Optional findFirstByDealIdOrderBySignedAtDesc(Long dealId); + + Optional findFirstByQuoteIdOrderBySignedAtDesc(Long quoteId); + + /** + * Version labels of a deal's signatures, newest first. Projects just the label column so the + * frequently-polled snapshot never loads the PDF bytes. A signature means the agreement is + * signed; the PDF is resolved (stored or re-rendered) at download time. + */ + @Query( + "SELECT s.documentLabel FROM ProcurementAgreementSignature s" + + " WHERE s.dealId = :dealId" + + " ORDER BY s.signedAt DESC") + List findSignedLabels(@Param("dealId") Long dealId); +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java b/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java index cae1cf36fb..a739e538b2 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java @@ -5,7 +5,6 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.UUID; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -20,16 +19,24 @@ import stirling.software.common.model.enumeration.TeamRole; import stirling.software.proprietary.model.TeamMembership; import stirling.software.proprietary.security.repository.TeamMembershipRepository; import stirling.software.saas.procurement.config.ProcurementConfigurationProperties; +import stirling.software.saas.procurement.legal.AgreementAssembler; +import stirling.software.saas.procurement.legal.AgreementPdfRenderer; +import stirling.software.saas.procurement.legal.AgreementSigning; +import stirling.software.saas.procurement.legal.AssembledAgreement; import stirling.software.saas.procurement.license.EnterpriseLicenseService; import stirling.software.saas.procurement.license.LicenseEntitlements; +import stirling.software.saas.procurement.model.ProcurementAgreementSignature; import stirling.software.saas.procurement.model.ProcurementDeal; import stirling.software.saas.procurement.model.ProcurementQuote; import stirling.software.saas.procurement.model.QuoteDetails; import stirling.software.saas.procurement.pricing.ProcurementPricingService; import stirling.software.saas.procurement.pricing.QuoteBreakdown; import stirling.software.saas.procurement.pricing.QuoteConfig; +import stirling.software.saas.procurement.repository.ProcurementAgreementSignatureRepository; import stirling.software.saas.procurement.repository.ProcurementDealRepository; import stirling.software.saas.procurement.repository.ProcurementQuoteRepository; +import stirling.software.saas.service.SaasTeamService; +import stirling.software.saas.util.LogRedactionUtils; /** * Orchestrates a linked team's procurement journey: start a (mock-licensed) trial, build a @@ -52,6 +59,12 @@ public class ProcurementService { private final EnterpriseLicenseService licenses; private final ProcurementConfigurationProperties config; private final TeamMembershipRepository memberRepo; + private final AgreementAssembler agreementAssembler; + private final AgreementPdfRenderer agreementPdfRenderer; + private final ProcurementAgreementSignatureRepository signatureRepo; + // Trial-setup invitations run through the team-invite path, with its seat, role and + // rate-limit rules rather than a second implementation here. + private final SaasTeamService teams; public ProcurementService( ProcurementDealRepository dealRepo, @@ -59,13 +72,21 @@ public class ProcurementService { ProcurementPricingService pricing, EnterpriseLicenseService licenses, ProcurementConfigurationProperties config, - TeamMembershipRepository memberRepo) { + TeamMembershipRepository memberRepo, + AgreementAssembler agreementAssembler, + AgreementPdfRenderer agreementPdfRenderer, + ProcurementAgreementSignatureRepository signatureRepo, + SaasTeamService teams) { this.dealRepo = dealRepo; this.quoteRepo = quoteRepo; this.pricing = pricing; this.licenses = licenses; this.config = config; this.memberRepo = memberRepo; + this.agreementAssembler = agreementAssembler; + this.agreementPdfRenderer = agreementPdfRenderer; + this.signatureRepo = signatureRepo; + this.teams = teams; } /** @@ -89,11 +110,45 @@ public class ProcurementService { return dealRepo.findByTeamId(teamId); } + /** + * Starting a trial is only legitimate before one exists, while the buyer is still exploring, or + * to restart within the trial itself. A null stage is a deal that has just been constructed. + * + *

Package-private so the policy can be tested without the service's ten dependencies. Adding + * a later stage here would let a leader replace a paying customer's committed licence. + */ + static boolean canStartTrial(String stage) { + return stage == null + || ProcurementDeal.STAGE_EXPLORING.equals(stage) + || ProcurementDeal.STAGE_TRIAL.equals(stage); + } + @Transactional(readOnly = true) public List quotesForDeal(Long dealId) { return quoteRepo.findByDealIdOrderByCreatedAtDesc(dealId); } + /** + * Record that the account is looking at enterprise. Creates the deal at {@code exploring} when + * there is none; an existing deal is returned untouched, so this can never walk a live deal + * backwards or restart a trial. + */ + @Transactional + public ProcurementDeal recordInterest(Long teamId) { + return dealRepo.findByTeamId(teamId) + .orElseGet( + () -> { + ProcurementDeal deal = new ProcurementDeal(teamId); + deal.setStage(ProcurementDeal.STAGE_EXPLORING); + ProcurementDeal saved = dealRepo.save(deal); + log.info( + "[procurement] interest recorded team={} deal={}", + teamId, + saved.getDealId()); + return saved; + }); + } + /** * Start (or restart) the free trial for a team: issue a mock trial licence and stamp the trial * window on the deal. No Stripe: a no-card trial has no subscription; the entitlement is the @@ -101,10 +156,40 @@ public class ProcurementService { * ({@code cloud}/{@code selfhost}/{@code airgap}) and seat count are captured here so the quote * builder opens seeded to their environment; both are still editable when the quote is built. */ - @Transactional public ProcurementDeal startTrial(Long teamId, String deployment, int seats) { + return startTrial(teamId, deployment, seats, null, null, null, null); + } + + /** + * Start (or restart) the trial, capturing the buying entity if the setup step collected it. + * Blank details are ignored rather than written, so a re-run without them keeps what is there. + * + *

Only from before the trial or during it. Past that, {@code licenseRef} points at the + * committed annual licence, and this method would replace it with a fresh 14-day trial key + * while Stripe kept billing — the same hazard {@link #extendTrial} guards against, one step + * worse because it re-issues rather than re-dates. It would also rewind the stage and reset the + * extension counter. + * + *

The transaction is declared here rather than on the 3-arg overload: that one only + * delegates, and Spring's proxy cannot intercept a self-invocation, so an annotation there does + * nothing for either path. This is the method the controller calls, and it reaches out to + * Keygen between the read and the write. + */ + @Transactional + public ProcurementDeal startTrial( + Long teamId, + String deployment, + int seats, + String businessName, + String contactName, + String contactEmail, + String inviteEmails) { ProcurementDeal deal = dealRepo.findByTeamId(teamId).orElseGet(() -> new ProcurementDeal(teamId)); + if (!canStartTrial(deal.getStage())) { + throw new IllegalStateException( + "Trial cannot be started from stage " + deal.getStage()); + } LocalDateTime now = LocalDateTime.now(); LocalDateTime ends = now.plusDays(config.getTrialDurationDays()); deal.setStage(ProcurementDeal.STAGE_TRIAL); @@ -113,6 +198,10 @@ public class ProcurementService { deal.setTrialStartedAt(now); deal.setTrialEndsAt(ends); deal.setTrialExtensionsUsed(0); + if (isNotBlank(businessName)) deal.setBusinessName(businessName.trim()); + if (isNotBlank(contactName)) deal.setContactName(contactName.trim()); + if (isNotBlank(contactEmail)) deal.setContactEmail(contactEmail.trim()); + if (isNotBlank(inviteEmails)) deal.setInviteEmails(inviteEmails.trim()); deal.setLicenseRef(licenses.issueTrialLicense(teamId, leaderEmail(teamId), ends)); deal = dealRepo.save(deal); log.info( @@ -128,6 +217,45 @@ public class ProcurementService { /** * Constrain a caller-supplied deployment to the known set; anything else falls back to cloud. */ + /** + * Send the invitations named at trial setup. Best-effort per address: a rejection (already a + * member, an invitee with their own paid plan, the hourly rate limit) must not fail the trial, + * so each is logged and skipped rather than propagated. + * + *

Note the first accepted invitation converts a personal team into a shared one with + * unlimited seats — that is {@code inviteUserToTeam}'s own rule, and naming teammates here is + * the buyer asking for exactly that. + */ + public void sendTrialInvites( + Long teamId, stirling.software.proprietary.security.model.User inviter, String emails) { + if (inviter == null || !isNotBlank(emails)) return; + for (String raw : emails.split("[,;\s]+")) { + String email = raw.trim(); + if (email.isEmpty()) continue; + try { + teams.inviteUserToTeam(teamId, email, inviter); + // Redacted: an invitee list is third-party PII, and these logs are the one place it + // would otherwise be written in full. LogRedactionUtils is what the rest of the + // SaaS + // module uses for the same reason. + log.info( + "[procurement] trial invite sent team={} to={}", + teamId, + LogRedactionUtils.redactEmail(email)); + } catch (Exception e) { + log.warn( + "[procurement] trial invite skipped team={} to={}: {}", + teamId, + LogRedactionUtils.redactEmail(email), + e.getMessage()); + } + } + } + + private static boolean isNotBlank(String value) { + return value != null && !value.isBlank(); + } + private static String normalizeDeployment(String deployment) { if (deployment == null) return "cloud"; String d = deployment.trim().toLowerCase(Locale.ROOT); @@ -172,16 +300,32 @@ public class ProcurementService { } // (Re)building a quote returns the deal to the quote stage and drops any prior acceptance, // so a rebuild from security/payment can't leave a stale stage or accepted-quote pointer. + QuoteBreakdown breakdown = pricing.price(cfg); + // Enforced before anything is persisted, and server-side rather than in the builder: the + // pricing curve has no natural floor (a small enough committed volume rounds the meter to + // zero) and every registered user leads their own team, so without this any signup could + // price a $0 enterprise quote, accept it, and be provisioned a committed licence. + long floor = config.getMinAnnualNetMinor(); + if (floor > 0 && breakdown.annualNetMinor() < floor) { + throw new IllegalStateException( + "Quoted annual fee " + + breakdown.annualNetMinor() + + " is below the minimum enterprise deal size " + + floor); + } + deal.setStage(ProcurementDeal.STAGE_QUOTE); deal.setAcceptedQuoteId(null); deal = dealRepo.save(deal); - QuoteBreakdown breakdown = pricing.price(cfg); - ProcurementQuote quote = new ProcurementQuote(); quote.setDealId(deal.getDealId()); - quote.setQuoteNumber(nextQuoteNumber(deal.getDealId())); - // Priced but not yet issued: the edge fn creates the Stripe Quote and flips this to SENT. + // No quote number here: the deal's one reference is Stripe's, and Stripe does not assign it + // until the quote is finalised. The edge fn creates the Stripe Quote, flips this to SENT, + // and + // writes the number back. Nothing displays a reference in between — the builder only shows + // one + // for an issued quote, and the agreement is not assembled until after issue. quote.setStatus(ProcurementQuote.STATUS_DRAFT); quote.setCurrency(cfg.currency()); quote.setVolume(cfg.volume()); @@ -210,10 +354,11 @@ public class ProcurementService { quote.setLineItemsJson(writeLineItems(breakdown)); quote.setValidUntil(LocalDate.now().plusDays(30)); quote = quoteRepo.save(quote); + // Logged by id, not reference: a draft has no reference until Stripe issues it. log.info( "[procurement] quote built team={} quote={} annualNet={} tcv={}", teamId, - quote.getQuoteNumber(), + quote.getQuoteId(), quote.getAnnualNetMinor(), quote.getTcvMinor()); return quote; @@ -241,6 +386,164 @@ public class ProcurementService { return deal; } + /** The quote a team is currently transacting on: its accepted quote, else the most recent. */ + @Transactional(readOnly = true) + public Optional currentQuote(Long teamId) { + return dealRepo.findByTeamId(teamId) + .flatMap( + deal -> { + if (deal.getAcceptedQuoteId() != null) { + Optional accepted = + quoteRepo.findById(deal.getAcceptedQuoteId()); + if (accepted.isPresent()) return accepted; + } + return quoteRepo + .findByDealIdOrderByCreatedAtDesc(deal.getDealId()) + .stream() + .findFirst(); + }); + } + + /** + * The filled enterprise agreement for a team's current quote, rendered for review (unsigned). + */ + @Transactional(readOnly = true) + public Optional agreementDocument(Long teamId) { + return currentQuote(teamId).map(q -> agreementAssembler.assemble(q, null)); + } + + /** + * The current (unsigned) agreement rendered to PDF, for download at the sign step. Empty when + * there's no quote yet or the render runtime is unavailable. The signed PDF (with the signature + * block filled) is a separate artifact recorded at signing (see {@link #latestSignature}). + */ + @Transactional(readOnly = true) + public Optional agreementDocumentPdf(Long teamId) { + return currentQuote(teamId) + .map(q -> agreementAssembler.assemble(q, null)) + .map(a -> agreementPdfRenderer.tryRender(a.markdown())); + } + + /** + * Record a signed enterprise agreement: assemble the final document, hash it, render + store + * the PDF (best-effort), and persist an immutable signature pinned to the exact document + * version. Does not itself accept the quote into a subscription — the caller proceeds to accept + * as before. + * + *

Deliberately not {@code @Transactional}: rendering the PDF shells out to WeasyPrint, and + * holding the deal's row lock across an external process buys nothing here. The only write is a + * single insert, which {@code save} makes atomic on its own. + */ + public ProcurementAgreementSignature signAgreement( + Long teamId, AgreementSigning signing, String signerIp) { + ProcurementDeal deal = + dealRepo.findByTeamId(teamId) + .orElseThrow(() -> new IllegalStateException("No deal for team " + teamId)); + ProcurementQuote quote = + currentQuote(teamId) + .orElseThrow( + () -> + new IllegalStateException( + "No quote to sign for team " + teamId)); + + // Signing is only meaningful against an issued quote at the agreement stage. Without this a + // direct API call could record a signature over a draft (whose quote_ref is still empty), + // or + // re-sign a deal that has already moved on. + if (!ProcurementDeal.STAGE_AGREEMENT.equals(deal.getStage())) { + throw new IllegalStateException( + "Deal is not at the agreement stage for team " + teamId); + } + if (!ProcurementQuote.STATUS_SENT.equals(quote.getStatus())) { + throw new IllegalStateException("Quote is not issued for team " + teamId); + } + + AssembledAgreement assembled = agreementAssembler.assemble(quote, signing); + + ProcurementAgreementSignature sig = new ProcurementAgreementSignature(); + sig.setDealId(deal.getDealId()); + sig.setQuoteId(quote.getQuoteId()); + sig.setDocumentId(assembled.docId()); + sig.setDocumentVersion(assembled.version()); + sig.setDocumentLabel(assembled.versionLabel()); + sig.setContentHash(sha256(assembled.markdown())); + sig.setVariablesJson(assembled.variablesJson()); + sig.setCustomerLegalName(signing.customerLegalName()); + sig.setSignatoryName(signing.signatoryName()); + sig.setSignatoryTitle(signing.signatoryTitle()); + sig.setAuthorityConfirmed(signing.authorityConfirmed()); + sig.setSignerIp(signerIp); + sig.setPdf(agreementPdfRenderer.tryRender(assembled.markdown())); + sig = signatureRepo.save(sig); + log.info( + "[procurement] agreement signed team={} quote={} doc={} pdf={}", + teamId, + quote.getQuoteId(), + assembled.versionLabel(), + sig.getPdf() != null); + return sig; + } + + /** The latest recorded signature for a team's deal, if any (for the signed-PDF download). */ + @Transactional(readOnly = true) + public Optional latestSignature(Long teamId) { + return dealRepo.findByTeamId(teamId) + .flatMap( + deal -> + signatureRepo.findFirstByDealIdOrderBySignedAtDesc( + deal.getDealId())); + } + + /** + * The version label of the deal's latest signed agreement, if any. Used to surface the + * "download signed agreement" action once a signature exists; the snapshot polls this, so it + * deliberately avoids loading the PDF bytes. + * + *

Deliberately not conditional on a stored PDF: download re-renders from the pinned document + * version on demand, so the action works whether or not the render succeeded at signing time. + */ + @Transactional(readOnly = true) + public Optional signedAgreementLabel(Long dealId) { + return signatureRepo.findSignedLabels(dealId).stream().findFirst(); + } + + /** + * The signed agreement as a PDF for download: the artifact stored at signing, or — if the + * render runtime was unavailable then — re-rendered now from the signature's details. Empty + * when the team has no signature or the render runtime is still unavailable. + */ + @Transactional(readOnly = true) + public Optional signedAgreementPdf(Long teamId) { + return latestSignature(teamId) + .flatMap( + sig -> { + if (sig.getPdf() != null) return Optional.of(sig.getPdf()); + return quoteRepo + .findById(sig.getQuoteId()) + .map( + q -> + agreementAssembler.assemble( + q, + new AgreementSigning( + sig.getCustomerLegalName(), + sig.getSignatoryName(), + sig.getSignatoryTitle(), + sig.isAuthorityConfirmed()))) + .map(a -> agreementPdfRenderer.tryRender(a.markdown())); + }); + } + + private static String sha256(String s) { + try { + byte[] digest = + java.security.MessageDigest.getInstance("SHA-256") + .digest(s.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + return java.util.HexFormat.of().formatHex(digest); + } catch (java.security.NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 unavailable", e); + } + } + /** * Provision on accept: upgrade the team's licence to the committed annual term (valid * immediately), so the buyer can get going the moment they accept — before the invoice is paid. @@ -260,22 +563,58 @@ public class ProcurementService { } /** - * Mark the deal fully live (advance to the active stage) once payment settles. In production - * this is the {@code invoice.paid} webhook; here it's the demo/manual stand-in. Re-affirms the + * Mark the deal fully live (advance to the active stage) once payment settles — driven by the + * {@code invoice.paid} webhook, and by the demo control when those are enabled. Re-affirms the * annual licence in case provisioning didn't run at accept. + * + *

Idempotent per invoice rather than per stage, which matters because {@code invoice.paid} + * carries two different meanings. Stripe redelivers events, so the same invoice + * arriving twice must do nothing. But the renewal payment a year later is also an {@code + * invoice.paid}, and the committed licence expires term years from issue — so a + * different invoice has to re-issue, moving the expiry out, or the customer's licence + * lapses after they have paid. Keying on the stage alone couldn't tell those apart and treated + * every renewal as a duplicate. + * + * @param paidInvoiceId the Stripe invoice that was paid, or null when the caller has no invoice + * to identify the payment by (the demo control). Null keeps the old conservative behaviour: + * a live deal short-circuits, since there is nothing to tell a renewal from a repeat. */ @Transactional - public ProcurementDeal markLive(Long teamId) { + public ProcurementDeal markLive(Long teamId, String paidInvoiceId) { ProcurementDeal deal = dealRepo.findByTeamId(teamId) .orElseThrow(() -> new IllegalStateException("No deal for team " + teamId)); + if (ProcurementDeal.STAGE_LIVE.equals(deal.getStage()) + && (paidInvoiceId == null || paidInvoiceId.equals(deal.getLastPaidInvoiceId()))) { + log.debug( + "[procurement] invoice.paid already applied team={} deal={} invoice={}", + teamId, + deal.getDealId(), + paidInvoiceId); + return deal; + } + boolean renewal = ProcurementDeal.STAGE_LIVE.equals(deal.getStage()); deal.setLicenseRef(issueOrUpgradeAnnual(deal)); + if (paidInvoiceId != null) deal.setLastPaidInvoiceId(paidInvoiceId); deal.setStage(ProcurementDeal.STAGE_LIVE); deal = dealRepo.save(deal); - log.info("[procurement] deal live team={} deal={}", teamId, deal.getDealId()); + log.info( + "[procurement] deal live team={} deal={} renewal={} invoice={}", + teamId, + deal.getDealId(), + renewal, + paidInvoiceId); return deal; } + /** + * Go live with no invoice reference — the demo control. See {@link #markLive(Long, String)}. + */ + @Transactional + public ProcurementDeal markLive(Long teamId) { + return markLive(teamId, null); + } + /** * Issue or upgrade the committed annual licence from the deal's accepted (else latest) quote, * stamping the full entitlement snapshot onto it and upgrading the trial licence in place when @@ -327,15 +666,34 @@ public class ProcurementService { * before paying — that's bounded: the trial licence carries {@code expiry = trialEndsAt}, so * the file the verifier accepts self-expires at trial end. The buyer must re-download after * provisioning to get the committed-term file (the portal warns about this). + * + *

Once a quote exists, the quote's deployment decides — not the deal's. They are two + * different values: the deal's is chosen free at trial setup, the quote's is the one carrying + * the air-gap deploy fee. Reading the deal's here meant selecting air-gapped in the trial and + * then buying a cloud quote still yielded the offline file, and after provisioning it was + * checked out against the committed annual licence — so the self-expiry above no longer bounded + * it. */ @Transactional(readOnly = true) public Optional offlineLicenseFile(Long teamId) { ProcurementDeal deal = dealRepo.findByTeamId(teamId).orElse(null); if (deal == null || deal.getLicenseRef() == null) return Optional.empty(); - if (!"airgap".equalsIgnoreCase(deal.getDeployment())) return Optional.empty(); + if (!"airgap".equalsIgnoreCase(entitledDeployment(deal))) return Optional.empty(); return Optional.of(licenses.checkOutLicenseFile(deal.getLicenseRef())); } + /** + * The deployment the team is actually entitled to: the priced quote's once one exists, + * otherwise the trial's self-selected target. Paid entitlements must follow what was quoted. + */ + private String entitledDeployment(ProcurementDeal deal) { + ProcurementQuote quote = currentQuote(deal.getTeamId()).orElse(null); + if (quote != null && quote.getDeployment() != null && !quote.getDeployment().isBlank()) { + return quote.getDeployment(); + } + return deal.getDeployment(); + } + /** * Reset a team's procurement: delete the deal (quotes + activity cascade). For * re-demos/testing. @@ -346,12 +704,6 @@ public class ProcurementService { log.info("[procurement] deal reset team={}", teamId); } - private String nextQuoteNumber(Long dealId) { - int seq = quoteRepo.findByDealIdOrderByCreatedAtDesc(dealId).size() + 1; - String token = UUID.randomUUID().toString().substring(0, 4).toUpperCase(Locale.ROOT); - return String.format(Locale.ROOT, "QT-%s-%04d", token, seq); - } - private String writeLineItems(QuoteBreakdown breakdown) { try { return OBJECT_MAPPER.writeValueAsString(breakdown.lineItems()); diff --git a/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md new file mode 100644 index 0000000000..12cceaef65 --- /dev/null +++ b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md @@ -0,0 +1,53 @@ +## Part C — Data Processing Addendum + +This DPA forms part of the Agreement and applies where Provider processes Personal Data on Customer's behalf. + +### C1. Roles; scope; instructions + +Customer is the controller (or a processor on behalf of its own controllers); Provider is a processor (or subprocessor, as applicable). Provider processes Personal Data only on Customer's documented instructions — including processing initiated by Customer's users, policies, pipelines, and API calls — unless required by law (in which case Provider informs Customer unless legally prohibited). **Provider will inform Customer without undue delay if, in Provider's opinion, an instruction infringes the GDPR, UK GDPR, or other applicable data-protection law.** Customer is responsible for the lawfulness of the Personal Data it submits and the instructions it gives; Customer's rights under this DPA include instruction, audit (C9), objection to subprocessors (C5), assistance (C6), and return or deletion of data (C10). + +### C2. Details of processing + +**Subject matter/nature:** PDF processing and governance (classification, redaction, routing, retention, conversion, signing, extraction, AI-assisted analysis). **Duration:** the Term plus the deletion period. **Categories of data:** any Personal Data contained in Customer files and metadata (names, contact details, identifiers, financial or health data if present in Customer files), account data of Customer users. **Data subjects:** Customer's employees, users, customers, and other persons appearing in Customer files. **Sensitive data:** may be present in Customer files at Customer's discretion; Customer is responsible for the lawful basis. + +### C3. Confidentiality; personnel + +Provider ensures persons authorized to process Personal Data are bound by confidentiality and receive security training. Zero-standing-access applies: content access is just-in-time, logged, and audited (MSA Section 4.2). + +### C4. Security measures (Annex II summary) + +Encryption in transit (TLS 1.2+) and at rest (AES-256); zero-standing-access with audited JIT elevation; role-based access control; SSO/SCIM; tenant isolation; vulnerability management and penetration testing; audit logging of processing events (including file name, hash, size, and operations); backup and recovery. For Self-hosted and Air-gapped deployments, Customer operates the runtime environment and is responsible for infrastructure-level controls; Provider's measures apply to license/metering services and support access. + +### C5. Subprocessors + +Customer generally authorizes the subprocessors listed at {{subprocessor_url}}: cloud infrastructure (Amazon Web Services), payment processing (Stripe, as independent controller for payment data), email delivery (Google), account infrastructure (Supabase), product telemetry (PostHog, EU-hosted; pseudonymous usage events, never file content), and AI model providers: **Anthropic** (Claude models — receives prompts and the document text or excerpts needed for the requested AI feature) and **Voyage AI** (embedding models — receives extracted text excerpts solely to generate embeddings where Customer enables Ingestion/RAG features). AI features are optional and may be disabled; where used, AI providers receive only the content needed for the requested feature. **Whole Customer files are never transmitted to any AI provider.** Neither AI provider trains on Customer data (verified against the signed provider agreements, Jul 10, 2026). Provider gives thirty (30) days' notice of new subprocessors; Customer may object on reasonable data-protection grounds, and if unresolved, may terminate the affected Services with a pro-rata refund. **Provider imposes data-protection obligations on each subprocessor by written contract that are at least as protective as this DPA, and remains fully responsible to Customer for each subprocessor's performance.** + +### C6. Data subject requests; assistance + +Taking into account the nature of the processing, Provider provides reasonable assistance (including through the Processor's search, redaction, and audit tools) for Customer's obligations under GDPR Articles 32–36: security of processing, breach notification to authorities and data subjects, data protection impact assessments, and prior consultations with supervisory authorities, as well as responses to data subject requests. Provider forwards requests received directly to Customer and does not respond except as legally required. **Provider makes available to Customer all information necessary to demonstrate compliance with this DPA and allows for and contributes to audits, including inspections, per Section C9.** + +### C7. Breach notification + +Per MSA Section 5.3: without undue delay after becoming aware of a Personal Data Breach, and in any event within forty-eight (48) hours of awareness, with information provided in phases as available — including the nature of the breach, categories and approximate volumes affected, likely consequences, and measures taken or proposed. + +### C8. International transfers + +Where Personal Data subject to GDPR/UK GDPR is transferred to countries without adequacy, the Parties incorporate the EU Standard Contractual Clauses (Commission Decision 2021/914): **Module 2** (controller-to-processor) where Customer is a controller, and **Module 3** (processor-to-processor) where Customer acts as a processor, with the following selections — Clause 7 (docking): included; Clause 9(a): Option 2 (general written authorization, 30 days' notice per C5); Clause 11(a) optional language: not used; Clause 17: the law of Ireland; Clause 18: the courts of Ireland; competent supervisory authority: the Irish Data Protection Commission (per Annex I.C). Annex I (parties, description of transfer: as per Section C2), Annex II (technical and organizational measures: as per Section C4), and Annex III (subprocessors: as per Section C5 and {{subprocessor_url}}) are completed by reference to this DPA. For UK transfers, the UK International Data Transfer Addendum applies with its Tables completed by reference to the foregoing. Provider is not certified under the EU-U.S. Data Privacy Framework; the SCCs are the transfer mechanism. + +**Note:** Provider does not currently offer contractual EU data residency for Stirling Cloud; residency is achieved via Self-hosted or Air-gapped deployment. + +### C9. Audits + +Provider's security reports and documentation (Section 5.1) are the ordinary means of demonstrating compliance. Customer may additionally audit — by itself or a mandated auditor — once per year on thirty (30) days' notice, and at any time where: (a) a security incident affecting Customer Personal Data has occurred; (b) provided documentation reveals a material deficiency; (c) a competent supervisory authority requires it; or (d) Customer reasonably suspects material noncompliance with this DPA. Audits are conducted during business hours, under confidentiality, at Customer's cost, with reasonable notice, without unreasonable interference with Provider's operations, and without access to other customers' data. + +### C10. Return & deletion + +On termination, at Customer's choice, Provider returns Customer file content and Personal Data (export of Customer files and the governed-record metadata) and/or deletes them — from live systems within thirty (30) days and from backups within ninety (90) days — except as retention is required by law, and certifies deletion on request. Where Customer uses HYOK, key destruction by Customer renders content cryptographically inaccessible immediately. + +### C11. CCPA/CPRA + +Provider is a "service provider" under the CCPA/CPRA. Provider: (a) processes Personal Information only for the business purposes specified in this Agreement — providing, securing, metering, and supporting the Services described in Section C2; (b) shall not sell or share Personal Information; (c) shall not retain, use, or disclose it for any purpose other than those business purposes, or outside the direct business relationship between the Parties; (d) shall not combine it with Personal Information received from other sources, except as permitted by CCPA regulations for the business purposes; (e) provides the same level of privacy protection required of businesses by the CCPA; (f) will notify Customer if it determines it can no longer meet its CCPA obligations; (g) grants Customer the right, upon reasonable notice, to take reasonable and appropriate steps to ensure Provider's use of Personal Information is consistent with Customer's obligations, and to stop and remediate any unauthorized use; and (h) flows these requirements down to its subprocessors per Section C5. Provider certifies that it understands these restrictions and will comply with them. + +### C12. Liability + +Liability under this DPA is subject to the MSA's limitations (Section 8). diff --git a/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md new file mode 100644 index 0000000000..cd3f6742eb --- /dev/null +++ b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md @@ -0,0 +1,119 @@ +# Stirling Enterprise Agreement + +One signature executes all three parts: the Master Services Agreement (Part A), the Order Form (Part B), and the Data Processing Addendum (Part C). The Stirling EULA & Commercial Terms is incorporated by reference. + +## Part A — Master Services Agreement + +This Master Services Agreement (the "Agreement") is entered into as of {{effective_date}} (the "Effective Date") by and between **Stirling PDF, Inc.**, a Delaware corporation with offices at 548 Market Street PMB 887643, San Francisco, CA 94104 ("Provider"), and **{{customer_legal_name}}** ("Customer"). Each a "Party," together the "Parties." + +### 1. Services & License + +1.1 **The Services.** Provider will provide the Stirling PDF Processor (the "Processor") — the hosted or customer-deployed platform for distributing PDF editors and governing PDF processing, including policies, pipelines, the Stirling Agent, API access, and the administrative console — and the Stirling PDF Editor (the "Editor"), as described in the Order Form. + +1.2 **License grants.** Provider grants Customer, for the Term: (a) a non-exclusive, non-transferable right to access and use the Processor for Customer's internal business operations, up to the Committed Volume; and (b) a non-exclusive right to deploy and distribute the Editor to Customer's authorized users without limit on user count. Open-source components of the Editor remain governed by their own licenses, which control for those components. + +1.3 **Deployment.** The Services are delivered via the deployment stated in the Order Form (Stirling Cloud, Self-hosted, or Air-gapped). Self-hosted deployments validate their license and report metering data online; Air-gapped deployments verify a signed activation bundle offline and reconcile usage periodically as described in the Documentation. Customer shall not disable, circumvent, or falsify license validation or usage metering. **The metered rate does not vary by deployment**; deployment-specific services are priced as line items in the Order Form. + +1.4 **Restrictions.** Customer shall not: resell or provide the Services to third parties as a service bureau; reverse engineer non-open-source components; use the Services to violate law; or exceed the scope of the Order Form other than through Overage (Section 3.4). + +### 2. Term & Renewal + +2.1 **Initial Term.** {{term_years}} year(s) from the Effective Date. + +2.2 **Renewal.** The Agreement auto-renews for successive periods equal to the Initial Term unless either Party gives sixty (60) days' written notice of non-renewal before the end of the then-current term. The Annual Fee for each renewal year equals the immediately preceding year's Annual Fee increased by three percent (3%) — the same formula as Section 2.3. Itemized services escalate at the same rate unless restated in a superseding Order Form. + +2.3 **In-term escalator.** The Annual Fee (including itemized services) increases by a fixed three percent (3%) at each anniversary of the Effective Date during the Term. + +### 3. Fees & Payment + +3.1 **Annual Fee.** Customer shall pay the Annual Fee stated in the Order Form, calculated as the Committed Volume ({{committed_pdfs_yr}} PDFs per year) at {{rate_per_pdf}} per PDF at the {{posture}} governance posture, plus the itemized services in the Order Form, less the term discount stated there. + +3.2 **Invoicing.** Fees are invoiced annually in advance, due net thirty (30) days. Late amounts accrue interest at 1.5% per month or the maximum permitted by law, whichever is less. Fees are exclusive of taxes; Customer is responsible for all taxes other than Provider's income taxes. + +3.3 **Committed Volume; measurement.** The Committed Volume is denominated in PDFs processed per year at the stated posture, and converts to a drawdown allowance in PDF Processes at the fixed conversion schedule below, which is frozen for the Term: + +| Posture | PDF Processes per PDF | +| --- | --- | +| Essentials | 2 | +| Governed | 4 | +| Regulated | 7 | + +A **"PDF Process"** is one policy execution, one pipeline run, or one Stirling Agent returned artifact, applied to one file, plus Data Processing increments under Section 3.5. For clarity: a pipeline run counts as one PDF Process regardless of the number of operations in its chain; a Stirling Agent artifact counts as one regardless of the number of messages that produced it; failed processes (those that do not complete) are not counted; reprocessing the same file and duplicate submissions are counted; counts are whole numbers (no rounding). The Processor's audit log records each PDF Process and is the system of record, subject to Section 3.7. Provider will make a per-file usage statement (file identifier, size, processes, drawdown) available for audit. + +**Worked example.** At the Governed posture, a commitment of 90,000,000 PDFs/year provides a drawdown allowance of 360,000,000 PDF Processes. A 60 MB file that runs the four Governed policies draws down 4 PDF Processes plus 2 Data Processing increments (Section 3.5) = 6 PDF Processes. The allowance is a purchased quantity, not a feature limit: Customer may run any number of policies or pipelines; actual consumption simply draws the allowance down faster, and consumption beyond it bills as Overage (Section 3.4). + +3.4 **Overage.** Consumption beyond the Committed Volume in a contract year is billed quarterly in arrears at the committed rate stated in the Order Form. Overage does not increase subsequent years' Committed Volume. + +3.5 **Data Processing.** Each file includes its first twenty-five (25) megabytes (decimal, 1 MB = 1,000,000 bytes) at no additional drawdown. Each additional twenty-five (25) megabytes or part thereof (rounded up per file) draws down one (1) additional PDF Process. File size is measured once per file at ingestion, on the file as submitted. This schedule is stated here in full, is frozen for the Term, and is not subject to alteration through the Documentation. + +3.6 **No refunds.** Except as expressly stated (Sections 7.1, 7.3, 10.3, and DPA Section C5), fees are non-refundable and Committed Volume does not roll over between contract years. + +3.7 **Billing disputes.** Customer may dispute any invoice or metering record in good faith within sixty (60) days of the invoice date. Provider will investigate promptly, provide the relevant audit-log extracts and usage statements, and correct confirmed errors by credit or refund. The audit log is presumptively accurate but not conclusive; Customer may rebut it with reasonable evidence. Undisputed amounts remain payable when due. + +### 4. Data Protection + +4.1 The Data Processing Addendum at Part C (the "DPA") is incorporated into this Agreement and governs Provider's processing of Customer Personal Data, in compliance with the GDPR, UK GDPR, and CCPA/CPRA to the extent applicable. + +4.2 **Zero-standing-access.** Customer file content is encrypted in transit and at rest. Provider personnel have no standing access to Customer file content; access is granted just-in-time under audited elevation, solely as necessary to provide the Services or as instructed by Customer. Document metadata is maintained to operate the governed record. Where the Order Form includes BYOK or HYOK key management, the key terms in the Documentation apply. + +### 5. Security & Availability + +5.1 **Security program.** Provider maintains a written information security program including access controls, encryption (TLS 1.2+ in transit, AES-256 at rest), audit logging, vulnerability management, and personnel security. Provider will provide its available security documentation (including its security program overview and penetration-test attestation) upon request under confidentiality. + +5.2 **Availability.** For Stirling Cloud deployments, Provider targets 99.9% monthly uptime, excluding scheduled maintenance announced at least 48 hours in advance. The uptime figure is a target, not a credited commitment, and no service credits apply. Support response commitments for the {{sla_tier}} tier are set out in the SLA Exhibit referenced by the Order Form. + +5.3 **Breach notice.** Provider will notify Customer without undue delay after becoming aware of a Personal Data Breach affecting Customer Personal Data, and in any event within forty-eight (48) hours of awareness. Provider may provide information in phases as it becomes available and will supplement its notice as investigation proceeds. + +5.4 **Updates.** Provider will make security patches and product upgrades available to Customer at no additional charge for supported versions. + +### 6. Confidentiality + +6.1 Each Party shall protect the other's Confidential Information with at least the care it uses for its own similar information and no less than reasonable care, use it solely to perform under this Agreement, and disclose it only to personnel and advisors with a need to know who are bound by confidentiality obligations at least as protective. Confidential Information excludes information that is public without breach, independently developed, or rightfully received from a third party. + +6.2 Compelled disclosure is permitted with prompt notice (where lawful) and reasonable cooperation to seek protective treatment. + +6.3 Obligations survive three (3) years after termination; trade secrets survive as long as they remain trade secrets. + +### 7. Warranties & Indemnification + +7.1 **Performance warranty.** Provider warrants the Services will perform materially in accordance with the Documentation. Customer's exclusive remedy for breach is re-performance or, if Provider cannot re-perform within thirty (30) days, termination of the affected Services and a pro-rata refund of prepaid, unused fees for those Services. + +7.2 **Mutual warranties.** Each Party warrants it has the authority to enter this Agreement and will comply with applicable law in its performance. + +7.3 **IP indemnification.** Provider shall defend Customer against third-party claims that the Services, as provided and used per this Agreement, infringe a copyright or trademark or misappropriate a trade secret, and shall indemnify Customer for resulting damages finally awarded or agreed in settlement. Where **Enhanced IP Protection** is elected on the Order Form, this obligation extends to patent claims and carries the enhanced cap stated in Section 8.2. Exclusions: combinations with non-Provider materials, Customer content, modifications not made by Provider, and use after notice to stop. Provider may procure rights, modify, or replace the Services; if none is practicable, Provider may terminate the affected Services and refund prepaid, unused fees. This section states Customer's exclusive remedy for IP claims. + +7.4 **Customer indemnification.** Customer shall defend and indemnify Provider against third-party claims arising from Customer content, Customer's breach of Section 1.4, or Customer's violation of law. + +7.5 **Disclaimer.** EXCEPT AS EXPRESSLY STATED, THE SERVICES ARE PROVIDED WITHOUT OTHER WARRANTIES, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. AI-ASSISTED OUTPUTS (INCLUDING CLASSIFICATION, EXTRACTION, AND AGENT ARTIFACTS) ARE PROBABILISTIC; CUSTOMER IS RESPONSIBLE FOR HUMAN REVIEW WHERE OUTPUTS HAVE LEGAL OR REGULATORY EFFECT. + +### 8. Limitation of Liability + +8.1 NEITHER PARTY IS LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR LOST PROFITS OR REVENUE. + +8.2 **General cap.** EACH PARTY'S AGGREGATE LIABILITY IS CAPPED AT THE FEES PAID OR PAYABLE UNDER THIS AGREEMENT IN THE TWELVE (12) MONTHS PRECEDING THE FIRST EVENT GIVING RISE TO LIABILITY. **Super-cap:** for breaches of Section 6 (Confidentiality), breaches of the DPA or Section 4–5 security obligations, and IP indemnification under Section 7.3, the cap is TWO TIMES (2x) such fees. **Uncapped:** fraud, willful misconduct, Customer's payment obligations, and Customer's indemnification under Section 7.4 for claims arising from Customer's willful violation of law. + +8.3 All claims arising from the same event or series of connected events count as a single claim for the purposes of the caps in Section 8.2. + +### 9. General + +9.1 **Entire agreement; precedence.** This Agreement (Parts A–C, the Order Form, the SLA Exhibit, and the Standard Contractual Clauses where applicable) is the entire agreement and supersedes prior proposals and quotes, including {{quote_ref}}. Only the following provisions of the Stirling EULA & Commercial Terms are incorporated: Section 3 (Definitions), Section 8 (AI features), Section 10 (Self-hosted and desktop software), and Section 11 (Fair use). The website Terms of Service do not apply to this Agreement; without limitation, their arbitration and class-waiver provisions, online auto-renewal rules, unilateral-amendment provision, self-serve pricing, and clickwrap acceptance mechanism are expressly excluded. Precedence: Order Form → Standard Contractual Clauses (for international transfers) → DPA → MSA → SLA Exhibit → incorporated EULA sections → Documentation. + +9.2 **Governing law; venue.** Delaware law, excluding conflicts rules. Exclusive jurisdiction and venue in the state or federal courts located in San Francisco County, California, and the Parties consent to personal jurisdiction there. + +9.3 **Assignment.** Neither Party may assign without the other's consent, except to a successor in a merger, acquisition, or sale of substantially all assets, with notice. + +9.4 **Notices.** Written notices to the addresses on the Order Form; email permitted with confirmation of receipt. + +9.5 **Force majeure; independent contractors; waiver; severability.** Standard terms apply: neither Party is liable for delay caused by events beyond reasonable control; the Parties are independent contractors; failure to enforce is not waiver; unenforceable provisions are severed with the remainder in effect. + +9.6 **Publicity.** Neither Party may use the other's name or marks publicly without prior written consent, except Provider may identify Customer as a customer with Customer's prior approval of the specific use. + +9.7 **Suspension.** Provider may suspend the Services for material breach that threatens the security or integrity of the Services, with notice and opportunity to cure where practicable. Undisputed unpaid fees more than thirty (30) days late are grounds for suspension after ten (10) days' notice. + +### 10. Termination + +10.1 Either Party may terminate for material breach uncured thirty (30) days after written notice, or immediately upon the other's insolvency. + +10.2 On termination: Customer's access ends (self-hosted licenses expire per the license mechanism); each Party returns or destroys the other's Confidential Information; the DPA's deletion terms govern Customer Personal Data; Sections 3 (accrued fees), 6, 7, 8, 9, and 10 survive. + +10.3 If Customer terminates for Provider's uncured material breach, Provider refunds prepaid fees for the unused remainder of the then-current contract year. diff --git a/app/saas/src/main/resources/legal/eula/1.0.0/eula.md b/app/saas/src/main/resources/legal/eula/1.0.0/eula.md new file mode 100644 index 0000000000..f6dd32d5e1 --- /dev/null +++ b/app/saas/src/main/resources/legal/eula/1.0.0/eula.md @@ -0,0 +1,75 @@ +# Stirling EULA & Commercial Terms + +This document fills the EULA slot the website Terms of Service §5 already contemplates ("If a separate end-user license (EULA) accompanies software, that license governs to the extent of any conflict"). It carries the commercial terms of the actual product: the PDF Process meter, spend limits, prepaid capacity, the free allotment, trials, self-hosted licensing, and AI features. For customers under a signed Stirling Enterprise Agreement, that agreement controls. + +**Effective:** {{version_date}} · **Version:** {{version}} + +## 1. Agreement; precedence + +These EULA & Commercial Terms ("EULA") supplement the Stirling Terms of Service (stirling.com/legal/terms-of-service). If they conflict, this EULA controls for the software and the commercial terms below. Open-source components are governed by their own licenses, which control for those components. By clicking accept, creating a workspace, or using the software, you agree on behalf of yourself and, if applicable, the organization you represent ("you"). + +## 2. The products + +**The Stirling PDF Editor** is free: manual editing, the tool catalog, and team administration (including SSO) carry no subscription fee or per-seat charge, whether used in the browser, as a desktop application, or self-hosted. Usage limits on automated processing, fair-use rules (Section 11), support levels, and the feature set may change over time, and third-party costs (such as your own hosting) are yours. The Editor's open-source components remain available under their own licenses independently of this EULA. **The Stirling PDF Processor** is the paid platform that processes PDFs automatically — policies, pipelines, the Stirling Agent, and API processing — billed on the meter below. + +## 3. Definitions + +**"PDF Process"** — one policy execution, one pipeline run (regardless of the number of operations in its chain), or one Stirling Agent returned artifact (a processed file or summary, regardless of the number of messages that produced it), applied to one file. **"Data Processing"** — the data-volume component of the meter: files carry their first 25 MB included per file; volume past that is billed per Section 4. **"file"** — a document processed by the Processor. Chatting with the Stirling Agent is free; only returned artifacts meter. + +## 4. Metered billing (pay as you go) + +4.1 **Rates.** 1¢ per PDF Process, plus Data Processing at 1¢ per 25 MB increment past the first 25 MB of each file. Megabytes are decimal (1 MB = 1,000,000 bytes); size is measured once per file as submitted; increments round up ("part thereof" counts — a 26 MB file incurs one Data Processing increment, a 60 MB file incurs two). Rates may change on thirty (30) days' notice; changes apply prospectively. **Example:** two policies on a 3 MB contract = 2¢; two policies on a 60 MB scan set = 2¢ + 2¢ data = 4¢. + +4.2 **Free allotment.** New workspaces receive a one-time allotment of 500 PDF Processes. A file processed by two processes consumes two of the 500. When the allotment is exhausted, processing pauses until the Processor is switched on. + +4.3 **Invoices.** Usage is invoiced monthly on the 1st for the prior cycle, charged to your payment method on file (card or ACH debit). You authorize these charges. + +4.4 **Usage records.** The Processor's audit log is the system of record, subject to Section 4.5. Your Usage & Billing page shows consumption, and a per-file usage statement (name, size, processes, charge) is available for download. + +4.5 **Billing disputes.** You may dispute a charge or metering record in good faith within sixty (60) days of the invoice or charge date. We will investigate, provide the relevant usage-statement detail, and correct confirmed errors by credit or refund. The audit log is presumptively accurate but not conclusive; reasonable contrary evidence will be considered. Undisputed amounts remain payable. + +## 5. Spend limits + +5.1 You may set a monthly spend limit. By default, processing pauses when usage reaches the limit; queued documents resume when you raise the limit or the cycle resets. Nothing already processed is lost. + +5.2 If you enable **keep-processing** ("Keep processing if you hit your limit"), usage past the limit continues to accrue and be billed per Section 4; the limit then functions as a notification threshold. You can change the limit or the toggle at any time in Usage & Billing. + +## 6. Cancellation; downgrade + +You may revert to the free Editor plan at any time from Usage & Billing. Accrued usage remains payable. Your policies, configuration, and history are retained per the Terms of Service data-retention practices. + +## 7. Prepaid capacity (self-serve annual) + +7.1 **Offer.** You may prepay twelve (12) months of processing capacity for the price of ten (10) (the "12-for-10 rate"), sized at purchase. Payment by card, or by bank transfer against a generated invoice (net 30); prepaid capacity activates when payment clears. + +7.2 **No renewal of prepaid capacity; automatic transition to pay-as-you-go.** Prepaid capacity does not renew for another prepaid term. At purchase, you affirmatively consent to the following transition, which is disclosed before you pay: when the term ends, metered billing (Section 4) applies automatically at then-current rates so processing does not pause. We remind you thirty (30) days before term end; the reminder states the metered rates that will apply and how to cancel or revert to the free Editor plan (one click in Usage & Billing). + +7.3 **Consumption; overage; expiry.** Capacity draws down in PDF Processes. If you exhaust capacity mid-term, you may top up at the same 12-for-10 rate, or metered billing applies at list rates (with a card on file) or processing pauses (without one). Unused capacity expires at term end and is not refunded and does not roll over. + +7.4 **Cap.** Self-serve prepaid capacity is limited to 1,000,000 PDF Processes per year; larger commitments are available under a Stirling Enterprise Agreement. + +## 8. AI features + +AI features — classification, extraction, redaction-assist, and the Stirling Agent — are optional. They run only when you invoke a feature that uses them, and an administrator can disable them for the workspace; the rest of the Processor works without them. When used, they call machine-learning models from the providers listed at {{subprocessor_url}}. Currently: **Anthropic** (Claude models), which receives prompts and the document text or excerpts needed to perform the requested task; and **Voyage AI** (embedding models), which receives extracted text excerpts solely to generate embeddings when you enable Ingestion/RAG features. Only the document text or excerpts needed for the requested feature are sent — not your whole files — and only when that feature runs. AI charges are included in the price of whatever runs — there is no separate AI surcharge. Your content is not used to train models, by us or by these providers (verified against our signed provider agreements). AI outputs are probabilistic; review outputs before relying on them where accuracy has legal effect. + +## 9. Evaluations and trials + +Enterprise trials run fourteen (14) days, require no payment method, and are provided for evaluation only, AS IS, without service level commitments. Either party may end an evaluation at any time; on expiry your workspace continues on the free Editor plan. + +## 10. Self-hosted and desktop software + +10.1 **License.** We grant you a non-exclusive, non-transferable license to install and run the Editor and, with an active plan, the self-hosted Processor, for your internal business use. Open-source components remain under their own licenses. + +10.2 **License validation and metering.** Self-hosted Processor deployments validate their license online and transmit usage metering data (process counts, file sizes, and file hashes for billing integrity, and diagnostic data — never file content or file names) to Stirling. File names used for unique PDF identification remain on your server and are not transmitted. Air-gapped deployments verify a signed activation bundle offline and reconcile usage periodically. You will not disable, circumvent, or falsify validation or metering. **The meter is the same regardless of where the software runs.** + +10.3 **Updates.** Security patches and upgrades are made available for supported versions; some updates may install automatically per Terms of Service §5. + +10.4 **Authorized users and administration.** "Authorized Users" are your employees, and the employees of your affiliates and contractors working on your behalf, whom you provision through your workspace. You are responsible for your users' credentials, your administrators' actions, and your users' compliance with this EULA. One workspace serves one legal entity and its affiliates; serving unrelated third parties requires a separate agreement. You may not redistribute the Processor or offer it as a hosted service to others. On termination or downgrade, self-hosted Processor licenses expire per the license mechanism; installed Editor copies remain usable under the free plan. We may verify license compliance through the validation mechanism in Section 10.2. + +## 11. Fair use + +Free-tier and flat-price features are subject to fair use: we may throttle or decline usage patterns that abuse free processing (for example, automation disguised as manual editing) after notice where practicable. + +## 12. Changes to this EULA + +We may update this EULA. Material changes take effect thirty (30) days after notice. Changes that materially increase your price or reduce your rights take effect at your next billing cycle or prepaid term start, or upon your affirmative acceptance — whichever comes first — except changes strictly necessary for legal compliance or security, which may take effect sooner with notice. Continued use after the effective date is acceptance. Version history is available at {{eula_url}}. diff --git a/app/saas/src/main/resources/legal/manifest.json b/app/saas/src/main/resources/legal/manifest.json new file mode 100644 index 0000000000..c34559275d --- /dev/null +++ b/app/saas/src/main/resources/legal/manifest.json @@ -0,0 +1,38 @@ +{ + "subprocessorUrl": "https://www.stirlingpdf.com/legal/subprocessors", + "eulaUrl": "https://www.stirlingpdf.com/legal/eula", + "documents": { + "enterprise-agreement": { + "label": "SEA", + "displayName": "Stirling Enterprise Agreement", + "version": "0.9.1", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["msa.md", "@order-form", "dpa.md"] + }, + "eula": { + "label": "EULA", + "displayName": "Stirling EULA & Commercial Terms", + "version": "1.0.0", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["eula.md"] + }, + "sla": { + "label": "SLA", + "displayName": "Stirling SLA Exhibit", + "version": "1.0.0", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["sla.md"] + }, + "subprocessors": { + "label": "SUBP", + "displayName": "Stirling Subprocessors", + "version": "1.0.0", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["subprocessors.md"] + } + } +} diff --git a/app/saas/src/main/resources/legal/sla/1.0.0/sla.md b/app/saas/src/main/resources/legal/sla/1.0.0/sla.md new file mode 100644 index 0000000000..42271171d5 --- /dev/null +++ b/app/saas/src/main/resources/legal/sla/1.0.0/sla.md @@ -0,0 +1,37 @@ +# SLA Exhibit — Stirling Enterprise Agreement + +Referenced by the Order Form's service-level row and MSA §5.2. One document, three tiers; the Order Form's tier selection determines the applicable column. Uptime is a target, not a credited commitment: no service credits apply at any tier. Tiers differentiate support response, channels, and people. + +## 1. Availability + +For Stirling Cloud deployments, Provider targets **99.9% monthly uptime**, measured at the API and console endpoints, excluding scheduled maintenance announced at least 48 hours in advance and events beyond Provider's reasonable control. Current and historical status is published at the system status page. No service credits apply; persistent material failure to meet the target is addressed through MSA §7.1 (performance warranty and remedies) and §10 (termination for material breach). + +Self-hosted and Air-gapped deployments: availability of the runtime is Customer's responsibility; this Section applies to Provider's license, metering, and update services. + +## 2. Support tiers + +| | **Standard** | **Priority** | **Dedicated** | +| --- | --- | --- | --- | +| Included with | Every Enterprise Agreement | Every Enterprise Agreement | The Dedicated SE/CSM line item ($30,000/yr) | +| Hours | Business hours (Mon–Fri, 9:00–18:00 US Eastern, excl. US holidays) | Business hours + extended (7:00–21:00 US Eastern) | 24×7 for Severity 1 | +| Channels | Email, in-product | Email, in-product, private Slack/Teams channel | All Priority channels + named Solutions Engineer and CSM | +| Severity 1 first response (production down / processing halted org-wide) | 8 business hours | 4 hours | 1 hour, 24×7 | +| Severity 2 (major feature degraded, no workaround) | Next business day | 8 business hours | 4 hours | +| Severity 3 (minor defect, workaround exists) | 3 business days | 2 business days | Next business day | +| Severity 4 (question, cosmetic) | 5 business days | 3 business days | 2 business days | +| Escalation path | Support queue | Support lead | Named SE → CSM → Provider executive | +| Business reviews | — | — | Quarterly, where the QBR line item is elected | + +First response = a qualified human engaging with the issue, not an acknowledgment autoresponder. Resolution times are not committed; Provider works Severity 1 issues continuously within the tier's hours until resolved or downgraded. + +## 3. Severity is set by Customer, subject to reasonable reclassification + +Customer designates severity at filing; Provider may reclassify with explanation. Severity 1 requires production impact in a live (non-evaluation) environment. + +## 4. Maintenance and updates + +Scheduled maintenance is announced at least 48 hours ahead and targeted at low-usage windows. Security patches for supported versions ship to all tiers at no charge (MSA §5.4). Trials and evaluations are provided AS IS and are outside this Exhibit (EULA §9). + +## 5. Exclusions + +This Exhibit does not apply to: issues caused by Customer's environment, modifications, or third-party systems; usage exceeding the fair-use provisions; Preview/Beta features; or Force Majeure events (MSA §9.5). diff --git a/app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md b/app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md new file mode 100644 index 0000000000..670ae53150 --- /dev/null +++ b/app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md @@ -0,0 +1,19 @@ +# Stirling PDF — Subprocessors + +Referenced by DPA §C5 and Annex III, and by EULA §8. Changes to this list carry 30 days' notice per DPA §C5. Last updated: {{version_date}}. + +Stirling PDF, Inc. uses the following subprocessors to provide the Services. Customer files are processed within Stirling's own infrastructure. AI features are optional and can be disabled; where they are used, AI providers receive only the document text or excerpts needed for the requested feature, never whole customer files. + +| Subprocessor | Purpose | Data processed | Location | +| --- | --- | --- | --- | +| **Amazon Web Services (AWS)** | Cloud infrastructure and storage for Stirling Cloud | Customer files (encrypted at rest), account and usage data | United States (EU region availability per deployment — see DPA §C8 note) | +| **Stripe** | Payment processing | Billing contact and transaction data. Payment card details go directly to Stripe, which acts as an independent controller for them | United States | +| **Supabase** | Account and workspace data infrastructure | Account, workspace, and configuration data | United States | +| **Google** | Transactional and operational email delivery | Names, email addresses, message content of service emails | United States | +| **Anthropic** | AI models (Claude) powering the Stirling Agent and AI-assisted features | Prompts and the document text or excerpts needed for the requested feature — never whole files; only where AI features are used | United States | +| **Voyage AI** | Embedding models for Ingestion/RAG features | Extracted text excerpts, only where the customer enables Ingestion/RAG, solely to generate embeddings — never customer files | United States | +| **PostHog** | Product telemetry and usage analytics | Pseudonymous usage events and diagnostic data — never file content | European Union (EU-hosted) | + +Neither AI provider uses customer data for model training (contractually confirmed). + +Self-hosted and air-gapped deployments: customer files remain in the customer's environment; Stirling receives license-validation and metering data only (process counts, file sizes, file hashes — never file names or content). diff --git a/app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java b/app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java new file mode 100644 index 0000000000..86f361ff87 --- /dev/null +++ b/app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java @@ -0,0 +1,44 @@ +package stirling.software.saas.procurement.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.saas.procurement.model.ProcurementDeal; + +/** + * Which stages may (re)start a trial is a security policy, not a convenience. + * + *

From the agreement stage onward a deal's {@code licenseRef} points at the committed annual + * licence. Starting a trial replaces it with a fresh 14-day key, rewinds the stage and resets the + * extension counter — so an unguarded restart would downgrade a paying customer's entitlement while + * Stripe kept billing them. This pins the allowed set so widening it has to be deliberate. + */ +class ProcurementTrialRestartPolicyTest { + + @Test + @DisplayName("allowed before a deal exists, while exploring, and within the trial") + void allowsOnlyPreCommitmentStages() { + assertThat(ProcurementService.canStartTrial(null)).isTrue(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_EXPLORING)).isTrue(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_TRIAL)).isTrue(); + } + + @Test + @DisplayName("refused once the deal is quoting or beyond") + void refusesCommittedStages() { + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_QUOTE)).isFalse(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_AGREEMENT)).isFalse(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_PAYMENT)).isFalse(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_LIVE)).isFalse(); + } + + @Test + @DisplayName("an unrecognised stage is refused, not waved through") + void refusesUnknownStages() { + // A stage added later, or a hand-edited row, must fail closed. + assertThat(ProcurementService.canStartTrial("renewal")).isFalse(); + assertThat(ProcurementService.canStartTrial("")).isFalse(); + } +} diff --git a/frontend/.storybook/a11y-baseline.json b/frontend/.storybook/a11y-baseline.json index 10fc530c77..a31d84613b 100644 --- a/frontend/.storybook/a11y-baseline.json +++ b/frontend/.storybook/a11y-baseline.json @@ -1707,6 +1707,9 @@ "editor/src/portal/components/billing/InvoicesList.stories.tsx :: Default": [ "color-contrast" ], + "editor/src/portal/components/billing/LinkAccountPrompt.stories.tsx :: Default": [ + "color-contrast" + ], "editor/src/portal/components/billing/PrepaidCapacityCard.stories.tsx :: Bundle Healthy": [ "aria-progressbar-name", "color-contrast" @@ -2107,22 +2110,15 @@ "color-contrast" ], "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: License": [ - "aria-dialog-name" + "color-contrast" ], "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: License Trial": [ - "aria-dialog-name" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Schedule Call": [ - "aria-dialog-name" + "color-contrast" ], "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Manage": [ - "aria-dialog-name" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Manage Maxed": [ - "aria-dialog-name" + "color-contrast" ], "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Setup": [ - "aria-dialog-name", "color-contrast" ], "editor/src/portal/components/procurement/ProcurementFlow.stories.tsx :: Default": [ @@ -2135,6 +2131,9 @@ "editor/src/portal/components/procurement/ProcurementHome.stories.tsx :: Default": [ "color-contrast" ], + "editor/src/portal/components/procurement/ProcurementModal.stories.tsx :: Open": [ + "color-contrast" + ], "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License": [ "color-contrast" ], @@ -2144,6 +2143,9 @@ "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License Online Only": [ "color-contrast" ], + "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: Payment": [ + "color-contrast" + ], "editor/src/portal/components/procurement/QuoteBuilder.stories.tsx :: Default": [ "color-contrast" ], diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index bf00cfd08d..33cf0ee8e1 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7331,13 +7331,16 @@ tagline = "Native app for Microsoft Windows" title = "Windows" [portal.home.editor] -activeUsers = "{{n}} active" -install = "Install the editor" -invite = "Invite teammates" -name = "Stirling PDF Editor" +activeOfDeployed = "{{active}} of {{total}} active this month" +name = "PDF Editor" open = "Open in browser" updated = "updated {{time}}" +[portal.home.editor.deploy] +finish = "Finish deployment" +options = "View deployment options" +start = "Deploy the Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -7348,25 +7351,6 @@ afternoon = "Good afternoon" evening = "Good evening" morning = "Good morning" -[portal.home.onboarding.enterprise] -body = "Org-wide SSO + SCIM + RBAC, committed volume pricing, and air-gapped deployment." -cta = "Start Trial" -ctaQuote = "Get Quote" -lead = "For 250+ employees." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Install the desktop app or self-host" -title = "Download the editor" - -[portal.home.onboarding.steps.invite] -blurb = "Bring your team into the secure workspace" -title = "Invite teammates" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} active · {{recommended}} recommended" -title = "Confirm your policies" - [portal.infrastructure] manageEditorDeployment = "Manage Editor deployment" sectionsAriaLabel = "Infrastructure sections" @@ -7767,6 +7751,12 @@ pipelines = "Pipelines" policies = "Policies" sources = "Sources" +[portal.legal] +draft = "{{label}} · draft" +loadError = "Could not load this document. Please try again." +loading = "Loading…" +title = "Legal document" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -8430,19 +8420,27 @@ email = "Email intake" subtitle = "Get your team evaluated, contracted, and onboarded. Every document in one place." title = "Procurement" -[portal.procurement.action] -download = "Download" -pay = "Pay now" -request = "Request" -sign = "Review & sign" -upload = "Upload" - [portal.procurement.agreement] -agreeCta = "Agree & subscribe" -confirm = "I have read and agree to the Stirling Enterprise Agreement." -eyebrow = "Agreement" -intro = "One combined agreement covers your deal: Master Service Agreement, Order Form, EULA, and Data Processing Agreement. Review it, then agree to accept the quote into a committed subscription." -title = "Review your enterprise agreement" +agreeCta = "Sign agreement" +confidential = "Confidential" +confirm = "I have read and agree to this Agreement, and I represent that I am authorized to sign it on behalf of the organization named above." +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA and DPA, combined into one signature." +download = "Download" +downloadDraftError = "Could not generate the agreement PDF - the document renderer is unavailable. Please try again shortly or contact support." +downloadError = "Could not download the signed agreement. Please try again." +legalName = "Legal entity name" +legalNamePlaceholder = "The legal entity that will sign" +loadError = "Could not load the agreement. Please try again." +loading = "Loading the agreement..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Request changes" +scrollHint = "Scroll to review" +signatory = "Signatory name" +signatoryPlaceholder = "Full name" +signatoryTitle = "Signatory title" +signatoryTitlePlaceholder = "e.g. General Counsel" +signError = "Could not record your signature. Please try again." [portal.procurement.builder] addons = "Add-ons" @@ -8455,16 +8453,19 @@ businessName = "Business name" businessNamePlaceholder = "Your company" city = "City" cityPlaceholder = "San Francisco" +completeRequired = "Please complete the required fields (marked *) with a valid email before generating the quote." contactEmail = "Contact email" contactEmailPlaceholder = "jane@acme.com" contactName = "Contact name" contactNamePlaceholder = "Jane Doe" continue = "Continue" +done = "Done" eula = "I have read and agree to the Stirling Enterprise EULA. It governs the agreement generated from this quote." generate = "Generate quote" -included = "Included" -indemnification = "IP indemnification" -indemnificationSub = "We defend qualifying IP claims, per the EULA" +indemnification = "Enhanced IP Protection" +indemnificationSub = "Extends our IP defense to patent claims. Baseline copyright, trademark and trade-secret indemnification is included free." +paperEyebrow = "Enterprise quote" +paperFor = "Prepared for" pdfSize = "PDF size" poNumber = "PO number" poNumberPlaceholder = "Optional" @@ -8512,6 +8513,7 @@ training = "Onboarding & training" trainingSub = "Live sessions to get your team running" users = "Total users" usersPlaceholder = "e.g. 250" +viewEula = "Read it" volEstimated = "Estimated from {{count}} users (~2,000 PDFs each, including automation). Edit if you know better." volManual = "Using your figure. Re-estimate from your team size any time." volNoUsers = "Not sure? Enter your team size and we'll estimate it." @@ -8520,54 +8522,55 @@ volumePlaceholder = "e.g. 1,000,000" years_one = "{{count}} year" years_other = "{{count}} years" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Done" -here = "You're here" -hide = "Hide" -optional = "Optional" -paidAddon = "Paid add-on" -show = "Show" -subtitle = "Everything you need at each step of the journey, surfaced as the deal moves through it." -supportingSubtitle = "SOC 2, security reviews, tax forms and more, ready when your security or procurement team asks. Some carry a one-time fee." -supportingTitle = "Supporting your evaluation" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form and DPA in one signature." +download = "Download" +eula = "EULA & Commercial Terms" +eulaSub = "The software and self-serve commercial terms." +invoice = "Invoice" +invoiceSub = "Your first subscription invoice." +laterInvoice = "Available after you accept" +laterQuote = "Available once your quote is issued" +quote = "Quote" +quoteSub = "Your itemised enterprise quote." +sla = "SLA Exhibit" +slaSub = "Support tiers and response targets." +subprocessors = "Subprocessors" +subprocessorsSub = "Third parties that process data on your behalf." +subtitle = "Your deal paperwork, available any time." title = "Documents" -upcoming = "Upcoming" +view = "View" [portal.procurement.error] title = "Something went wrong" [portal.procurement.hero] -company = "Your enterprise deal" -ctaLive = "You're live" -ctaPayment = "Add payment" +barAria = "Stage {{current}} of {{total}}" +ctaAgreement = "Review & sign agreement" +ctaExploring = "Set up your trial" ctaQuote = "Review your quote" +ctaReviewQuote = "Review quote" ctaTrial = "Build your quote" -eyebrow = "Enterprise procurement" +documents = "Documents" +eyebrow = "Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Invite teammates" licenseKey = "Licence key" -nextStep = "Next step: {{action}}" -notStarted = "Not started" +liveSub = "Your organization is provisioned and your licence is active." +liveTitle = "You are live on Stirling Enterprise" +next = "Next: {{stage}}" open = "Open procurement" scheduleCall = "Schedule a call" -setup1Sub = "Invite your teammates" -setup1Title = "Deploy the PDF Editor" -setup2Sub = "Turn on processing across editors and other sources" -setup2Title = "Connect the PDF Processor" -setup3Sub = "Turn on Security, Compliance, Routing, or Retention when you need them" -setup3Title = "Add recommended policies" +sentenceAgreement = "review and sign the agreement" +sentenceLive = "provisioning and rollout" +sentencePayment = "purchase order and payment" +sentenceQuote = "review and accept your quote" +sentenceTrial = "set up your trial workspace" [portal.procurement.journey] daysLeft_one = "{{count}} day left" daysLeft_other = "{{count}} days left" -engineerLabel = "Your solutions engineer" -eyebrow = "Your rollout" -live = "You're live on Stirling Enterprise" -nextStep = "Next step: {{action}}" -subtitle = "Your solutions engineer is on every step. One next action at a time; the full checklist is below." -title = "From trial to live, one guided path" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "One signature covers MSA, order form, EULA and DPA." @@ -8616,45 +8619,26 @@ description = "Your subscription is active and your licence is issued. Your team eyebrow = "Live" title = "You're live on Stirling Enterprise" -[portal.procurement.locked] -description = "Trial keys, committed-volume quotes, the one-signature agreement, payment, and your document ledger all live here once you start an enterprise evaluation." -eyebrow = "Enterprise only" -talkToSales = "Talk to sales" -title = "The procurement track opens with Enterprise" - [portal.procurement.milestone] -download = "Download PDF" downloadError = "Could not download the quote PDF just yet — please try again in a moment." -edit = "Edit quote" - -[portal.procurement.modal] -cancel = "Cancel" -chooseFile = "Choose file" -close = "Close" -downloadBody = "Your download will begin shortly." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "No file selected" -payBody = "Pay your committed contract by card or bank transfer through Stripe. Your workspace provisions as soon as payment clears." -payCta = "Continue to Stripe" -payTitle = "Confirm payment" -requestBodyFree = "We generate this on demand. Confirm and your solutions engineer will send it across shortly." -requestBodyPaid = "This is a paid add-on. Confirm and your solutions engineer will scope it and send the paperwork." -requestCta = "Request" -requestTitle = "Request this document" -signBody = "Opens the Stirling Enterprise Agreement for e-signature: one signature covers the MSA, order form, EULA and DPA. We countersign automatically and you advance to payment." -signCta = "Open for signature" -signTitle = "Review and sign your agreement" -uploadBody = "Send us your PO and we invoice against it on your terms. Drag in the PDF or pick a file below." -uploadCta = "Upload purchase order" -uploadTitle = "Upload your purchase order" [portal.procurement.payment] description = "Your quote is accepted and your licence is already active — your team can start right away. Pay the first invoice when you're ready; you can pay or download it here, no email needed." +downloadAgreement = "Download signed agreement" downloadInvoice = "Download invoice" +eyebrow = "Payment" title = "Subscription created" viewInvoice = "View & pay invoice" +[portal.procurement.review] +acceptCta = "Accept quote" +annual = "Annual fee (year 1)" +downloadCta = "Download quote" +poNumber = "Purchase order: {{po}}" +renewal = "Renews at {{amount}}/year after the term (+{{pct}}% CPI)" +tcv = "{{years}}-year term, paid in full up front: {{tcv}}" +validUntil = "Valid until {{date}}" + [portal.procurement.schedule] fallback = "Couldn't load the scheduler." fallbackLink = "Open scheduling in a new tab" @@ -8664,25 +8648,33 @@ title = "Schedule a call" [portal.procurement.setup] airgap = "Air-gapped" -airgapSub = "Fully offline, isolated network. Includes a downloadable licence file." -cloud = "Cloud" -cloudSub = "Fully managed by Stirling. Nothing for you to run." -deployment = "Where will you run Stirling?" -seats = "Team size" -seatsHint = "Roughly how many people will use it. You can refine this when you build your quote." -seatsPlaceholder = "e.g. 250" +airgapSub = "Sealed, no outbound" +back = "Back" +businessName = "Business name" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Managed, fastest start" +continue = "Continue" +deployment = "Where should it deploy?" +eula = "I agree to the Stirling EULA & Commercial Terms." +fullName = "Full name" +fullNamePlaceholder = "Your name" +invites = "Invite teammates · optional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Schedule a call" +seats = "People testing" +seatsPlaceholder = "e.g. 15" selfhost = "Self-hosted" -selfhostSub = "Run it in your own cloud or data centre." +selfhostSub = "Docker or Kubernetes" start = "Start trial" -subtitle = "Tell us how you plan to run Stirling so we can tailor your trial and quote. No card required." +stepOf = "Step {{n}} of {{total}}" +subtitle = "Free for 14 days, no card required." +subtitleDetails = "A few details for your quote and agreement." +talkFirst = "Prefer to talk first?" title = "Set up your trial" - -[portal.procurement.status] -action = "Action needed" -available = "Available" -complete = "Complete" -pending = "Pending" -request = "On request" +viewEula = "Read it" +workEmail = "Work email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Extending adds 7 days and notifies your solutions engineer." @@ -8693,12 +8685,6 @@ maxed = "Maxed out" subtitle = "Your free trial runs through {{date}}. No card required." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Committed volume pricing, org-wide SSO + SCIM + RBAC, 90-day immutable audit, and a dedicated SE." -homeCta = "Start Trial →" -homeHeadline = "Process millions of PDFs." - [portal.search] ariaLabel = "Search" placeholder = "Search Stirling — endpoints, pipelines, docs…" @@ -8923,6 +8909,9 @@ name = "Name" namePlaceholder = "e.g. Claims intake" type = "Type" +[portal.stepModal] +close = "Close" + [portal.usage] managePayment = "Manage Payment" subtitle = "Consumption, invoices, and plan management for every PDF Stirling has billed, in one console." @@ -8985,14 +8974,6 @@ summary = "Owns a team — manages its members' resources and shared configs." limited = "{{used}} / {{limit}}" unlimited = "{{used}} · Unlimited" -[portal.welcome] -ariaLabel = "Welcome to Stirling PDF" -install = "Install the editor" -invite = "Invite teammates" -openInBrowser = "Open in browser" -productName = "PDF Editor" -stats = "30M downloads · 60+ PDF operations · Free forever" - [printFile] title = "Print File" diff --git a/frontend/editor/src/portal/ViewRouter.tsx b/frontend/editor/src/portal/ViewRouter.tsx index 76adc1ad1d..8e77c20830 100644 --- a/frontend/editor/src/portal/ViewRouter.tsx +++ b/frontend/editor/src/portal/ViewRouter.tsx @@ -11,7 +11,6 @@ import { Policies } from "@portal/views/Policies"; import { EditorAdmin } from "@portal/views/EditorAdmin"; import { Infrastructure } from "@portal/views/Infrastructure"; import { PortalBillingGate } from "@portal/components/billing/PortalBillingGate"; -import { Procurement } from "@portal/views/Procurement"; import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; // Lazy so the generated docs manifest (bundled JSON) lands in its own chunk. @@ -62,7 +61,6 @@ export function ViewRouter() { element={} /> } /> - } /> null); +} + +afterEach(() => vi.restoreAllMocks()); + +describe("openApiUrl", () => { + it("opens an https URL in a new tab with noopener", () => { + const open = spyOpen(); + openApiUrl("https://invoice.stripe.com/i/abc123"); + expect(open).toHaveBeenCalledWith( + "https://invoice.stripe.com/i/abc123", + "_blank", + "noopener,noreferrer", + ); + }); + + it("opens a relative URL by resolving it against the origin", () => { + const open = spyOpen(); + openApiUrl("/invoices/abc.pdf"); + expect(open).toHaveBeenCalledWith( + `${window.location.origin}/invoices/abc.pdf`, + "_blank", + "noopener,noreferrer", + ); + }); + + // The reason this module exists: a navigation sink must not evaluate script. + it.each([ + "javascript:alert(document.domain)", + "JavaScript:alert(1)", + "data:text/html,", + "vbscript:msgbox(1)", + "file:///etc/passwd", + ])("refuses %s", (hostile) => { + const open = spyOpen(); + openApiUrl(hostile); + expect(open).not.toHaveBeenCalled(); + }); + + it("does nothing for an empty or missing URL", () => { + const open = spyOpen(); + openApiUrl(null); + openApiUrl(undefined); + openApiUrl(""); + expect(open).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/editor/src/portal/api/externalUrl.ts b/frontend/editor/src/portal/api/externalUrl.ts new file mode 100644 index 0000000000..10919e1544 --- /dev/null +++ b/frontend/editor/src/portal/api/externalUrl.ts @@ -0,0 +1,25 @@ +/** + * Open a URL that arrived from the API — a Stripe hosted-invoice page or invoice + * PDF — in a new tab, after checking its scheme. + * + * These values are relayed from Stripe through our own edge functions, so they + * are not attacker-controlled today. But a `javascript:` or `data:` URL reaching + * a navigation sink is an XSS primitive, and nothing between Stripe and this call + * promises the field can only ever hold a web address. Anything that is not plain + * http(s) is dropped instead of opened. + */ +export function openApiUrl(url: string | null | undefined): void { + if (!url) return; + let parsed: URL; + try { + parsed = new URL(url, window.location.origin); + } catch { + console.warn("[portal] refusing to open an unparseable URL"); + return; + } + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + console.warn(`[portal] refusing to open a ${parsed.protocol} URL`); + return; + } + window.open(parsed.href, "_blank", "noopener,noreferrer"); +} diff --git a/frontend/editor/src/portal/api/procurement.ts b/frontend/editor/src/portal/api/procurement.ts index 4d8a28e057..b49de2f6b6 100644 --- a/frontend/editor/src/portal/api/procurement.ts +++ b/frontend/editor/src/portal/api/procurement.ts @@ -1,12 +1,12 @@ import { apiClient } from "@portal/api/http"; +import { resolveDemoResponse } from "@portal/api/demoData"; +import { saasApiBase } from "@portal/api/saasApiBase"; import { getSupabaseClient } from "@app/auth/supabase/supabaseClient"; -import type { Tier } from "@portal/contexts/TierContext"; /* - * Procurement models the enterprise commercial journey, trial → quote → - * agreement → payment → implementation, plus the paperwork ledger that rides - * alongside it. The journey is enterprise-only; free/pro tiers receive a - * minimal locked payload the view renders as an upgrade prompt. + * Procurement models the enterprise commercial journey: trial → quote → agreement → payment → + * implementation. It is surfaced by the deal-status hero on Home and the takeover flow beside it; + * there is no separate procurement route. */ /* ──────────────────────────────────────────────────────────────────────── */ @@ -19,11 +19,8 @@ import type { Tier } from "@portal/contexts/TierContext"; * Payment read more plainly than the internal `security` / `procurement`). */ export type DealStage = - | "trial" - | "quote" - | "security" - | "procurement" - | "active"; + /** Asked about enterprise, nothing committed yet. Precedes the journey rather than joining it. */ + "exploring" | "trial" | "quote" | "security" | "procurement" | "active"; export interface JourneyStep { stage: DealStage; @@ -38,9 +35,14 @@ export interface JourneyStep { gatingAction: string; } -/** Ordered journey definition, the stepper renders this verbatim. */ -/** `label`/`blurb`/`gatingAction` values are i18n keys — render with t(). */ -export const JOURNEY: JourneyStep[] = [ +/** + * The commercial flow's stages, rendered verbatim as the hero's progress band. Quote and Agreement + * are distinct: the buyer accepts the quote first (no Stripe), then signs the agreement, which is + * what accepts into a committed subscription. + * + *

`label`/`blurb`/`gatingAction` are i18n keys — render with t(). + */ +export const FLOW_JOURNEY: JourneyStep[] = [ { stage: "trial", label: "portal.procurement.journeySteps.trial.label", @@ -73,204 +75,9 @@ export const JOURNEY: JourneyStep[] = [ }, ]; -/** - * The commercial flow's stepper stages. The real backend collapses quote + agreement into one - * accept step (accepting the issued quote is accepting the agreement), so the flow shows one fewer - * step than the mock ledger's {@link JOURNEY} — the separate "Agreement" step is dropped. Reuses - * JOURNEY's i18n keys. - */ -export const FLOW_JOURNEY: JourneyStep[] = JOURNEY.filter( - (s) => s.stage !== "security", -); - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Deal header */ -/* ──────────────────────────────────────────────────────────────────────── */ - -export interface SolutionsEngineer { - name: string; - title: string; - email: string; -} - -export interface TrialInfo { - /** License key seeded for the evaluation. */ - key: string; - /** ISO date the trial began. */ - startedOn: string; - /** ISO date the trial expires. */ - endsOn: string; - /** Whole days remaining (derived in the fixture for a stable demo number). */ - daysLeft: number; - extensionsUsed: number; - maxExtensions: number; -} - -export interface QuoteInfo { - number: string; - /** Annual contract value, in USD. */ - amount: number; - /** Contract term, e.g. "12 months". */ - term: string; - /** ISO date the quote expires. */ - validUntil: string; -} - -export interface Deal { - company: string; - currentStage: DealStage; - engineer: SolutionsEngineer; - trial: TrialInfo; - quote: QuoteInfo; -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Document ledger + supporting pool */ -/* ──────────────────────────────────────────────────────────────────────── */ - -/** - * Lifecycle of a single document. - * available: ready to grab now (download/sign/pay/upload as the action says) - * action: waiting on the buyer to act (the gating paperwork of a stage) - * pending: issued, awaiting the other side / a system step - * request: not generated yet; the buyer asks for it (some carry a fee) - * complete: done, kept for the record - */ -export type DocStatus = - | "available" - | "action" - | "pending" - | "request" - | "complete"; - -/** What pressing the document's button does. */ -export type DocAction = "download" | "sign" | "pay" | "upload" | "request"; - -export interface LedgerDoc { - id: string; - name: string; - /** Sub-line describing what the document is / what it covers. */ - sub: string; - status: DocStatus; - action: DocAction; - /** Buyer-skippable paperwork (e.g. paid onboarding). */ - optional?: boolean; - /** One-off fee in USD when the document/service is a paid add-on. */ - fee?: number; -} - -/** Document ledger grouped by the journey stage the paperwork belongs to. */ -export interface LedgerGroup { - stage: DealStage; - /** Buyer-facing stage name (matches JourneyStep.label). */ - label: string; - docs: LedgerDoc[]; -} - -/** Categories the stage-agnostic supporting pool is grouped under. */ -export type SupportingCategory = - | "security" - | "legal" - | "corporate" - | "procurement"; - -export interface SupportingGroup { - category: SupportingCategory; - label: string; - docs: LedgerDoc[]; -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Full procurement payload */ -/* ──────────────────────────────────────────────────────────────────────── */ - -export interface ProcurementResponse { - tier: Tier; - /** True only for enterprise, gates the whole journey + ledger. */ - unlocked: boolean; - /** Present only when unlocked. */ - deal: Deal | null; - journey: JourneyStep[]; - ledger: LedgerGroup[]; - supporting: SupportingGroup[]; -} - -/** GET /v1/procurement?tier=…, the deal, journey, ledger and supporting pool. */ -export async function fetchProcurement( - tier: Tier, -): Promise { - return apiClient.local.json( - `/v1/procurement?tier=${encodeURIComponent(tier)}`, - ); -} - -/* - * Commercial actions. Each mutates the deal server-side and returns the updated - * ProcurementResponse, the new canonical state, which the view applies so the - * journey progresses. The MSW layer answers these today; a real backend honours - * the same contracts unchanged. - */ - -/** Advance the deal to the next stage (the journey's primary CTA). */ -export async function advanceStage( - fromStage: DealStage, -): Promise { - return apiClient.local.json("/v1/procurement/advance", { - method: "POST", - body: { fromStage }, - }); -} - -/** Sign the Stirling Enterprise Agreement (MSA + order form + EULA + DPA). */ -export async function signAgreement( - docId: string, -): Promise { - // A real backend opens an e-signature envelope and completes on callback; - // here it completes immediately and advances the deal. - return apiClient.local.json("/v1/procurement/sign", { - method: "POST", - body: { docId }, - }); -} - -/** Pay the contract online (card / bank transfer via Stripe). */ -export async function payOnline(): Promise { - return apiClient.local.json("/v1/procurement/pay", { - method: "POST", - }); -} - -/** Upload a purchase order to invoice against (an alternate payment path). */ -export async function uploadPurchaseOrder( - file: File, -): Promise { - // A real backend takes the PO as multipart; the mock only needs the name. - return apiClient.local.json( - "/v1/procurement/purchase-order", - { - method: "POST", - body: { fileName: file.name }, - }, - ); -} - -/** Request a document that is generated on demand (some carry a one-off fee). */ -export async function requestDocument( - docId: string, - action: DocAction, -): Promise { - return apiClient.local.json( - `/v1/procurement/documents/${encodeURIComponent(docId)}/request`, - { method: "POST", body: { action } }, - ); -} - // ============================================================================ -// Enterprise procurement — real SaaS backend (/api/v1/procurement). -// -// The journey/ledger visuals above still ride the MSW mock; the commercial spine -// below (trial, server-priced quote, accept -> Stripe checkout) is the real thing, -// served by the saas Java backend and gated on a linked account. +// Enterprise procurement — the real SaaS backend (/api/v1/procurement): trial, +// server-priced quote, agreement, accept -> Stripe. Gated on a linked account. // ============================================================================ export type QuoteLineItemKind = @@ -288,7 +95,11 @@ export interface QuoteLineItem { export interface QuoteResult { quoteId: number; - quoteNumber: string; + /** + * Stripe's quote number — the deal's one reference, shown on the quote, the agreement and the + * invoice. Null while the quote is a local draft: Stripe assigns it only at finalisation. + */ + quoteNumber: string | null; /** draft (priced, editable) | sent (issued Stripe quote — PDF + shareable) | accepted | expired. */ status: string; currency: string; @@ -332,6 +143,12 @@ export interface ProcurementSnapshot { licensed: boolean; /** The team's Keygen licence key (present once licensed); shown in the portal to copy/install. */ licenseKey: string | null; + /** Version label of the signed agreement PDF available to download (e.g. "SEA v0.9.1"), else null. */ + agreementSignedVersion: string | null; + /** Buying entity captured at trial setup; null on deals started before that step existed. */ + businessName: string | null; + contactName: string | null; + contactEmail: string | null; latestQuote: QuoteResult | null; } @@ -385,13 +202,37 @@ export function fetchLicenseFile(): Promise { * Start the trial with the buyer's chosen deployment target and seat count (captured in the setup * step). These seed the quote builder; both remain editable when the quote is built. */ +/** Details collected by trial setup's second step; every field optional server-side. */ +export interface TrialSetupDetails { + businessName?: string; + contactName?: string; + contactEmail?: string; + /** + * Raw comma/space/semicolon separated addresses. Recorded on the deal AND sent through the + * team-invite path once the trial starts; a rejected address is skipped, never fatal. + */ + inviteEmails?: string; +} + export function startTrial( deployment: string, seats: number, + details?: TrialSetupDetails, ): Promise { return apiClient.saas.json( "/api/v1/procurement/trial/start", - { method: "POST", body: { deployment, users: seats } }, + { method: "POST", body: { deployment, users: seats, ...details } }, + ); +} + +/** + * Record that this account is looking at enterprise. Idempotent, and never disturbs an existing + * deal — so it is safe to call on every entry into the flow. + */ +export function recordInterest(): Promise { + return apiClient.saas.json( + "/api/v1/procurement/interest", + { method: "POST" }, ); } @@ -418,12 +259,114 @@ export function buildQuote(cfg: QuoteConfigInput): Promise { }); } +/** The filled enterprise agreement (MSA + Order Form + DPA) for the current quote, as markdown. */ +export interface AgreementDocument { + docId: string; + version: string; + /** e.g. "SEA v0.9.1" — the exact document version a signature will be pinned to. */ + versionLabel: string; + displayName: string; + effectiveDate: string; + /** "draft" until counsel clears it — the UI badges drafts. */ + status: string; + markdown: string; +} + +/** Buyer-supplied signing inputs captured on the agreement stage. */ +export interface SignAgreementInput { + customerLegalName: string; + signatoryName: string; + signatoryTitle: string; + authorityConfirmed: boolean; +} + +export interface SignAgreementResult { + signatureId: number; + versionLabel: string; + /** Whether the signed PDF was rendered + stored (false when the render runtime was unavailable). */ + pdfStored: boolean; +} + +/** Fetch the filled agreement to review before signing. */ +export function fetchAgreementDocument(): Promise { + return apiClient.saas.json( + "/api/v1/procurement/agreement/document", + ); +} + +/** Record the signed agreement (pins version + hash + variable snapshot + signatory + PDF). */ +export function recordAgreementSignature( + input: SignAgreementInput, +): Promise { + return apiClient.saas.json( + "/api/v1/procurement/agreement/sign", + { method: "POST", body: input }, + ); +} + +/** Fetch a static legal document (eula, sla, subprocessors) by id for in-product viewing. */ +export function fetchLegalDocument(docId: string): Promise { + return apiClient.saas.json(`/api/v1/legal/${docId}`); +} + +/** Download the stored, signed enterprise-agreement PDF for the team (post-signing). */ +export function fetchSignedAgreementPdf(): Promise { + return apiClient.saas.blob("/api/v1/procurement/agreement/signature/pdf"); +} + +/** Download the current (unsigned) enterprise-agreement PDF — the document shown at the sign step. */ +export function fetchAgreementPdf(): Promise { + return apiClient.saas.blob("/api/v1/procurement/agreement/document/pdf"); +} + +/** + * Record a clickwrap consent to a legal document (e.g. the EULA at trial start / quote generation). + * Best-effort — never block the flow it accompanies on a consent-logging failure. + */ +export function recordLegalConsent( + documentId: string, + context: string, +): Promise { + return apiClient.saas + .json("/api/v1/legal/consent", { + method: "POST", + body: { documentId, context }, + }) + .catch(() => undefined); +} + // ---- Stripe Quote operations (Supabase edge functions) --------------------- // Java has no Stripe SDK, so issuing/accepting the quote and fetching its PDF run in edge functions // that own Stripe; they persist results back through SECURITY DEFINER RPCs. The portal invokes them // directly (same pattern the PAYG checkout uses). +/** + * Fixture response for an edge function while demo data is on. + * + * These calls go out through the Supabase client rather than {@code apiClient}, which is where demo + * data is normally intercepted — so until this existed they were never mocked. The portal has no + * service worker: the MSW handlers are matched by URL inside {@link resolveDemoResponse}, so the URL + * has to be reconstructed here to match what mocks/handlers/procurementSaas.ts registers. + * + * This mattered rather more than a missing fixture: issue and accept create a real Stripe Quote and a + * real committed subscription. With a live session — the normal state when developing against the + * shared project — pressing "Generate quote" in dev billed nothing but left real objects behind. + */ +async function demoEdgeResponse( + fn: string, + quoteId: number, +): Promise { + const base = saasApiBase(); + if (!base) return undefined; + return resolveDemoResponse(new URL(`${base}/functions/v1/${fn}`), { + method: "POST", + body: { quote_id: quoteId }, + }); +} + async function invokeEdge(fn: string, quoteId: number): Promise { + const demo = await demoEdgeResponse(fn, quoteId); + if (demo) return (await demo.json()) as T; const supabase = getSupabaseClient(); if (!supabase) throw new Error("No SaaS session"); const { data, error } = await supabase.functions.invoke(fn, { @@ -446,6 +389,8 @@ export function acceptQuote(quoteId: number): Promise { /** Fetch the Stripe-generated quote PDF as a blob (for download / share). */ export async function fetchQuotePdf(quoteId: number): Promise { + const demo = await demoEdgeResponse("get-procurement-quote-pdf", quoteId); + if (demo) return await demo.blob(); const supabase = getSupabaseClient(); if (!supabase) throw new Error("No SaaS session"); const { data, error } = await supabase.functions.invoke( diff --git a/frontend/editor/src/portal/components/EditorStatusCard.css b/frontend/editor/src/portal/components/EditorStatusCard.css index e71a50bac3..692d08adc3 100644 --- a/frontend/editor/src/portal/components/EditorStatusCard.css +++ b/frontend/editor/src/portal/components/EditorStatusCard.css @@ -13,20 +13,18 @@ .portal-editor-hero__row { display: flex; align-items: center; - gap: 1.25rem; - padding: 1rem 1.25rem; - background: color-mix(in srgb, var(--c-primary) 22%, var(--c-hero-dark)); + gap: 0.75rem; + padding: 0.625rem 0.875rem; + background: var(--c-surface); } .portal-editor-hero__logo { - width: 3.5rem; - height: 3.5rem; - border-radius: 0.875rem; + width: 2rem; + height: 2rem; + border-radius: 0.5rem; overflow: hidden; flex-shrink: 0; - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.1), - 0 0 0 1px rgba(0, 0, 0, 0.06); + box-shadow: var(--shadow-sm); } .portal-editor-hero__mark { width: 100%; @@ -39,7 +37,7 @@ min-width: 0; display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.25rem; } .portal-editor-hero__title-row { @@ -50,31 +48,36 @@ } .portal-editor-hero__name { - font-size: 1.125rem; + font-size: 0.9375rem; font-weight: 700; - color: #fff; + color: var(--c-text); margin-right: 0.125rem; } -.portal-editor-hero__chip { - display: inline-flex; - align-items: center; - gap: 0.3125rem; - padding: 0.25rem 0.6875rem; - border-radius: var(--radius-pill); - border: 1px solid rgba(255, 255, 255, 0.16); - background: rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.85); - font-size: 0.71875rem; - font-weight: 600; - cursor: pointer; - transition: background var(--motion-fast); +/* The deployment host, sat inline with the name as the rail's subject. */ +.portal-editor-hero__host { + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 500; + color: var(--c-text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.portal-editor-hero__chip:hover { - background: rgba(255, 255, 255, 0.16); + +/* Quiet separator between the host and the adoption count. */ +.portal-editor-hero__dot { + width: 0.3125rem; + height: 0.3125rem; + border-radius: 999px; + background: var(--c-border); + flex-shrink: 0; } -.portal-editor-hero__chip svg { - color: rgba(255, 255, 255, 0.6); + +.portal-editor-hero__actives { + font-size: 0.75rem; + color: var(--c-text-muted); + white-space: nowrap; } .portal-editor-hero__meta { @@ -83,15 +86,10 @@ gap: 0.5rem; flex-wrap: wrap; font-size: 0.75rem; - color: rgba(255, 255, 255, 0.5); -} -.portal-editor-hero__host { - color: rgba(255, 255, 255, 0.92); - font-family: var(--font-mono); - font-weight: 500; + color: var(--c-text-subtle); } .portal-editor-hero__meta-sep { - color: rgba(255, 255, 255, 0.3); + color: var(--c-border); } .portal-editor-hero__action { @@ -101,35 +99,6 @@ gap: 0.5rem; } -/* Icon-only actions (e.g. install) on the dark header. */ -.portal-editor-hero__icon-btn { - display: grid; - place-items: center; - width: 2.25rem; - height: 2.25rem; - flex-shrink: 0; - border-radius: var(--radius-md); - border: 1px solid rgba(255, 255, 255, 0.16); - background: rgba(255, 255, 255, 0.08); - color: #fff; - cursor: pointer; - transition: background var(--motion-fast); -} -.portal-editor-hero__icon-btn:hover { - background: rgba(255, 255, 255, 0.16); -} - -/* White CTA on the dark header, matching the marketing card. */ -.portal-editor-hero__action .portal-editor-hero__cta.sui-btn { - background: #ffffff; - border-color: #ffffff; - color: var(--c-hero-dark-cta-text); -} -.portal-editor-hero__action .portal-editor-hero__cta.sui-btn:hover { - background: rgba(255, 255, 255, 0.88); - border-color: rgba(255, 255, 255, 0.88); -} - @media (max-width: 48rem) { .portal-editor-hero__row { flex-wrap: wrap; diff --git a/frontend/editor/src/portal/components/EditorStatusCard.stories.tsx b/frontend/editor/src/portal/components/EditorStatusCard.stories.tsx index 9e73983026..144ded6dc4 100644 --- a/frontend/editor/src/portal/components/EditorStatusCard.stories.tsx +++ b/frontend/editor/src/portal/components/EditorStatusCard.stories.tsx @@ -1,19 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { http, HttpResponse } from "msw"; import { EditorStatusCard } from "@portal/components/EditorStatusCard"; -import { SetupChecklist } from "@portal/components/SetupChecklist"; -import type { OnboardingProgress } from "@portal/hooks/useOnboardingProgress"; - -const progress: OnboardingProgress = { - loading: false, - deployed: true, - editorDone: true, - policiesDone: true, - inviteDone: false, - policiesActive: 3, - policiesRecommended: 4, - allComplete: false, -}; const meta: Meta = { title: "Portal/Home/EditorStatusCard", @@ -31,25 +18,14 @@ const meta: Meta = { export default meta; type Story = StoryObj; -/** The deployed-Editor status card on its own. */ +/** The deployment rail, reporting a live deployment. */ export const Default: Story = {}; -/** As it renders on the subscribed home: the setup checklist attached as the footer. */ -export const WithSetupChecklist: Story = { - args: { - footer: , - }, -}; - /** - * Backend without the editor-deployment endpoint (404): the status row is - * skipped and the card falls back to just the footer (setup checklist). It - * lights up automatically once /v1/editor/deployment is served. + * Backend without the editor-deployment endpoint (404): the rail keeps its identity and the neutral + * deploy ask, and states no host or adoption figure, since neither can be read. */ export const DeploymentUnavailable: Story = { - args: { - footer: , - }, parameters: { msw: { handlers: [ diff --git a/frontend/editor/src/portal/components/EditorStatusCard.tsx b/frontend/editor/src/portal/components/EditorStatusCard.tsx index 9726cc1009..b506a0a366 100644 --- a/frontend/editor/src/portal/components/EditorStatusCard.tsx +++ b/frontend/editor/src/portal/components/EditorStatusCard.tsx @@ -3,15 +3,12 @@ import { Fragment, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Skeleton } from "@app/ui"; import { useTier } from "@portal/contexts/TierContext"; -import { useView } from "@portal/contexts/ViewContext"; -import { useEditorDeployment } from "@portal/queries/infrastructure"; -import { type EditorInstance } from "@portal/api/editorDeploy"; +import { EDITOR_URL } from "@portal/auth/editorUrl"; import { - DownloadIcon, - ExternalLinkIcon, - UsersIcon, - UserPlusIcon, -} from "@portal/components/icons"; + useEditorDeployment, + useFleetStats, +} from "@portal/queries/infrastructure"; +import { type EditorInstance } from "@portal/api/editorDeploy"; import { DownloadEditorModal } from "@portal/components/DownloadEditorModal"; import "@portal/components/EditorStatusCard.css"; @@ -48,41 +45,64 @@ function primaryInstance(instances: EditorInstance[]): EditorInstance | null { ); } +/** + * The rail's primary action matures with adoption rather than deployment alone (marketing note + * D243): the loud ask only stands while the org is still one person on an undeployed editor. Once + * a deployment is in flight, finishing it takes over; once teammates are in, the ask goes quiet. + */ +type DeployAsk = "finish" | "start" | "options"; + +function deployAsk( + instances: EditorInstance[], + activeUsers: number, +): DeployAsk { + const deployed = instances.some((i) => i.status === "healthy"); + if (!deployed && instances.some((i) => i.status === "pairing")) + return "finish"; + if (!deployed && activeUsers <= 1) return "start"; + return "options"; +} + interface EditorStatusCardProps { - /** - * Rendered as an attached footer strip inside the card (e.g. the "Finish - * setting up" checklist), matching the free-tier hero's footer seam. - */ + /** Attached footer strip inside the card — the deal-status hero while a deal is underway. */ footer?: ReactNode; - /** - * Hide the active-users / invite chips. Used on enterprise, where the - * attached procurement deal hero already owns the invite action. - */ - hideChips?: boolean; } /** - * Subscribed/enterprise home hero: a status card for the org's deployed PDF - * Editor. Reads the same `/v1/editor/deployment` data as the Editor admin view - * (host, version, live users, deployment shape) and headlines the busiest - * instance, with a single "Open in browser" action to the workspace URL. + * The Home hero: the deployment rail for the org's PDF Editor. Host and build meta come from the + * same `/v1/editor/deployment` data as the Editor admin view, headlining the busiest instance; the + * adoption count is the figure Usage & Billing reports. The rail always renders — it carries the + * deploy ask itself — and each figure stands down independently when its source can't supply it. */ -export function EditorStatusCard({ footer, hideChips }: EditorStatusCardProps) { +export function EditorStatusCard({ footer }: EditorStatusCardProps) { const { t } = useTranslation(); const { tier } = useTier(); - const { setActiveView } = useView(); const [installOpen, setInstallOpen] = useState(false); const { data, loading } = useEditorDeployment(tier); + // The adoption figure is the same one Usage & Billing reports, read through the shared cache so + // the two agree and only one request is made. Either field is null when the backend can't compute + // it (e.g. EE auditing off), in which case the rail states no figure rather than a misleading 0. + const { data: fleet } = useFleetStats(); + const adoption = + fleet?.activeThisMonth != null && fleet.editorsDeployed != null + ? { active: fleet.activeThisMonth, total: fleet.editorsDeployed } + : null; const view = useMemo(() => { if (!data) return null; - const primary = primaryInstance(data.instances); - if (!primary) return null; const activeUsers = data.instances.reduce((s, i) => s + i.activeUsers, 0); + const ask = deployAsk(data.instances, activeUsers); + const primary = primaryInstance(data.instances); + // Nothing deployed yet: the rail still shows, carrying the deploy ask. Only the host and + // build meta are instance-derived, so they simply stand down. + if (!primary) { + return { host: null, activeUsers, ask, meta: [] as string[] }; + } const targetLabel = t(`portal.home.editor.target.${primary.target}`); return { host: primary.host, activeUsers, + ask, workspaceUrl: data.summary.workspaceUrl, meta: [ // Skip the deployment label when it just repeats the host (e.g. the @@ -95,21 +115,8 @@ export function EditorStatusCard({ footer, hideChips }: EditorStatusCardProps) { }; }, [data, t]); - const ready = !loading && !!view; - // The editor-deployment endpoint isn't implemented on every backend yet. - // When it's unavailable (finished loading with no data — e.g. a 404), skip - // the status row entirely and fall back to just the footer (the setup - // checklist, which reads supported endpoints). It lights up automatically - // once the backend serves /v1/editor/deployment. - const unavailable = !loading && !view; - - if (unavailable) { - return footer ? ( -

- {footer} -
- ) : null; - } + // Only the deploy ask can go loud; unknown deployment state keeps it quiet. + const loudAsk = !!view && view.ask !== "options"; return (
- {!ready || !view ? ( + {/* Each figure stands down on its own: the host and build meta need the deployment + endpoint, the adoption count needs fleet stats. Neither absence blanks the rail, and + nothing is asserted that its own source could not supply. */} + {loading ? ( <> @@ -133,65 +143,60 @@ export function EditorStatusCard({ footer, hideChips }: EditorStatusCardProps) { {t("portal.home.editor.name")} - {!hideChips && ( - + {view?.host && ( + {view.host} + )} + {adoption && ( + <> + + + {t("portal.home.editor.activeOfDeployed", adoption)} + + )}
-
- {view.host} - {view.meta.map((item, i) => ( - - · - {item} - - ))} -
+ {view && view.meta.length > 0 && ( +
+ {view.meta.map((item, i) => ( + + {i > 0 && ( + · + )} + {item} + + ))} +
+ )} )}
+ {/* Open in browser is a left-seated secondary in every state, and carries no arrow + (marketing note D244); the deploy ask beside it is the only button that can go loud. + Reaching the editor never depends on deployment data — it falls back to the configured + editor URL — so this stays live even when the deployment endpoint is unavailable. */}
- {!hideChips && ( - - )} - +
diff --git a/frontend/editor/src/portal/components/HomeHero.stories.tsx b/frontend/editor/src/portal/components/HomeHero.stories.tsx index dd1ea07c04..8784bde97a 100644 --- a/frontend/editor/src/portal/components/HomeHero.stories.tsx +++ b/frontend/editor/src/portal/components/HomeHero.stories.tsx @@ -16,17 +16,10 @@ const meta = { export default meta; type Story = StoryObj; -/** Pay-as-you-go tier: welcome header + setup checklist until onboarding completes. */ -export const Default: Story = { - args: { tier: "pro" }, -}; - -/** Free tier renders the same welcome-header composition as pro. */ -export const FreeTier: Story = { - args: { tier: "free" }, -}; - -/** Enterprise tier hides the status chips — the procurement deal hero owns the invite step. */ -export const EnterpriseTier: Story = { - args: { tier: "enterprise" }, -}; +/** + * The hero is the Editor deployment rail on every tier and in both editions — it reports its own + * deployment state and deploy ask, so there is nothing tier-specific left to compose. A live + * procurement deal attaches the deal-status hero as the rail's footer; that comes from + * useProcurement, so it follows the mocked backend rather than a story arg. + */ +export const Default: Story = {}; diff --git a/frontend/editor/src/portal/components/HomeHero.tsx b/frontend/editor/src/portal/components/HomeHero.tsx index 03bcc17ed0..e81d42dfef 100644 --- a/frontend/editor/src/portal/components/HomeHero.tsx +++ b/frontend/editor/src/portal/components/HomeHero.tsx @@ -1,60 +1,50 @@ -import type { Tier } from "@portal/contexts/TierContext"; +import { useEffect } from "react"; +import { Skeleton } from "@app/ui"; import { useUI } from "@portal/contexts/UIContext"; -import { WelcomeBanner } from "@portal/components/WelcomeBanner"; import { EditorStatusCard } from "@portal/components/EditorStatusCard"; -import { SetupChecklist } from "@portal/components/SetupChecklist"; -import { useOnboardingProgress } from "@portal/hooks/useOnboardingProgress"; import { ControlledDealStatusHero } from "@portal/components/procurement/ProcurementBanner"; import { ProcurementFlow } from "@portal/components/procurement/ProcurementFlow"; import { useProcurement } from "@portal/components/procurement/useProcurement"; /** - * The Home hero, composed with a procurement-aware, progress-aware footer: - * - * - no live deployment → welcome header (+ setup steps until complete) - * - deployment live → deployed-Editor status header (+ steps until complete) - * - onboarding complete → header only; the setup steps collapse away - * - enterprise → status header with chips hidden (the deal hero owns invite) - * - * The footer is the deal-status hero while a procurement deal is underway - * (procurement is a bolt-on to any tier); otherwise the setup checklist, until - * every step is done — then it collapses to just the header, matching the - * deployed-status card. The procurement takeover modals render alongside. + * The Home hero: always the Editor deployment rail, carrying the deal-status hero as its footer + * while a procurement deal is underway (procurement is a bolt-on to any tier). The rail states its + * own deployment status and deploy ask, so there is nothing for a tier to choose between. The + * procurement takeover modals render alongside. */ -export function HomeHero({ tier }: { tier: Tier }) { - const { openLinkModal } = useUI(); +export function HomeHero() { const procurement = useProcurement(); - const progress = useOnboardingProgress(); + const { trialSetupRequested, clearTrialSetupRequest } = useUI(); const dealActive = procurement.isLinked && procurement.started && !!procurement.data; - // Start the enterprise flow right here on Home: open the trial-setup modal when the account is - // linked, otherwise prompt to link first — no navigating off to the procurement view. - const onStartEnterprise = () => { - if (procurement.isLinked) procurement.onStartTrial(); - else openLinkModal(); - }; - - // Steps collapse once onboarding is complete; a live deal always keeps its - // hero. Otherwise the setup checklist carries the (progress-aware) steps. - const footer = dealActive ? ( - - ) : progress.allComplete ? undefined : ( - - ); - - // The live-status header (EditorStatusCard) needs a real deployment to show; - // without one it renders nothing, so route to it only when actually deployed. - // Everything else — including a step completed via the local download flag — - // keeps the always-present welcome header, so the card never vanishes. - const showStatus = progress.deployed; + // Someone said yes to enterprise elsewhere (the billing upsell, a sales link). Open trial setup + // once the snapshot has landed, so a buyer who already has a deal is not asked to start another. + useEffect(() => { + if (!trialSetupRequested || procurement.loading) return; + clearTrialSetupRequest(); + if (!procurement.started) procurement.onExploreEnterprise(); + }, [trialSetupRequested, procurement, clearTrialSetupRequest]); return ( <> - {showStatus ? ( - + {procurement.loading ? ( + // Hold the rail's shape rather than committing to a footer: branching before the snapshot + // lands paints the no-deal rail first, flashing on every refresh of an active deal. +
+
+ + +
+
) : ( - + + ) : undefined + } + /> )} diff --git a/frontend/editor/src/portal/components/SetupChecklist.css b/frontend/editor/src/portal/components/SetupChecklist.css deleted file mode 100644 index 0fcb71e678..0000000000 --- a/frontend/editor/src/portal/components/SetupChecklist.css +++ /dev/null @@ -1,122 +0,0 @@ -/* ──────────────────────────────────────────────────────────────────────── */ -/* Getting-started steps (home-hero body) */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-setup { - display: flex; - flex-direction: column; -} - -.portal-setup__list { - list-style: none; - margin: 0; - padding: 0; -} - -.portal-setup__row { - display: grid; - grid-template-columns: auto 1fr; - align-items: center; - gap: 0.875rem; - width: 100%; - padding: 0.6875rem 1.25rem; - border: none; - border-top: 1px solid var(--c-border-subtle); - background: transparent; - text-align: left; - cursor: pointer; - transition: background var(--motion-fast); -} -.portal-setup__item:first-child .portal-setup__row { - border-top: none; -} -.portal-setup__row:hover { - background: var(--c-hover); -} - -/* Numbered step marker */ -.portal-setup__num { - display: grid; - place-items: center; - width: 1.5rem; - height: 1.5rem; - flex-shrink: 0; - border-radius: 50%; - border: 1px solid var(--c-border); - font-size: 0.75rem; - font-weight: 600; - color: var(--c-text-subtle); -} -/* Completed step: filled green check. */ -.portal-setup__num.is-done { - border-color: var(--color-green); - background: var(--color-green); - color: #fff; -} -.portal-setup__row.is-done .portal-setup__text strong { - color: var(--c-text-muted); -} - -.portal-setup__text { - display: flex; - flex-direction: column; - min-width: 0; -} -.portal-setup__text strong { - font-size: 0.875rem; - font-weight: 600; - color: var(--c-text); -} -.portal-setup__text span { - font-size: 0.75rem; - line-height: 1.4; - color: var(--c-text-subtle); -} - -/* ── Enterprise upsell rung ── */ -.portal-setup__enterprise { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; - padding: 0.75rem 1.25rem; - border-top: 1px solid var(--c-border-subtle); - background: linear-gradient( - 90deg, - color-mix(in srgb, var(--c-primary) 5%, transparent) 0%, - transparent 55% - ); -} - -.portal-setup__enterprise-copy { - display: flex; - align-items: center; - gap: 0.75rem; - min-width: 0; - flex: 1; -} - -.portal-setup__enterprise-tag { - flex-shrink: 0; - padding: 0.1875rem 0.5625rem; - border-radius: var(--radius-md); - font-size: 0.59375rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--c-primary-hover); - background: var(--c-primary-tint); -} - -.portal-setup__enterprise-text { - margin: 0; - font-size: 0.8125rem; - line-height: 1.45; - color: var(--c-text-subtle); - min-width: 0; -} -.portal-setup__enterprise-text strong { - color: var(--c-text); - font-weight: 700; -} diff --git a/frontend/editor/src/portal/components/SetupChecklist.stories.tsx b/frontend/editor/src/portal/components/SetupChecklist.stories.tsx deleted file mode 100644 index 432be155d7..0000000000 --- a/frontend/editor/src/portal/components/SetupChecklist.stories.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { SetupChecklist } from "@portal/components/SetupChecklist"; -import type { OnboardingProgress } from "@portal/hooks/useOnboardingProgress"; - -const base: OnboardingProgress = { - loading: false, - deployed: false, - editorDone: false, - policiesDone: false, - inviteDone: false, - policiesActive: 0, - policiesRecommended: 6, - allComplete: false, -}; - -const meta: Meta = { - title: "Portal/Home/SetupChecklist", - component: SetupChecklist, - parameters: { layout: "padded" }, - args: { progress: base }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** A fresh workspace — no step complete yet. */ -export const NotStarted: Story = {}; - -/** Policies confirmed; editor + invite still open. */ -export const InProgress: Story = { - args: { - progress: { - ...base, - policiesDone: true, - policiesActive: 2, - policiesRecommended: 5, - }, - }, -}; - -/** Editor deployed + policies on; only the invite step remains. */ -export const AlmostDone: Story = { - args: { - progress: { - ...base, - editorDone: true, - policiesDone: true, - policiesActive: 4, - policiesRecommended: 3, - }, - }, -}; diff --git a/frontend/editor/src/portal/components/SetupChecklist.tsx b/frontend/editor/src/portal/components/SetupChecklist.tsx deleted file mode 100644 index d854beb771..0000000000 --- a/frontend/editor/src/portal/components/SetupChecklist.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@app/ui"; -import { useTier } from "@portal/contexts/TierContext"; -import { useView } from "@portal/contexts/ViewContext"; -import type { OnboardingProgress } from "@portal/hooks/useOnboardingProgress"; -import { DownloadEditorModal } from "@portal/components/DownloadEditorModal"; -import CheckRounded from "@mui/icons-material/CheckRounded"; -import "@portal/components/SetupChecklist.css"; - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Enterprise upsell rung */ -/* ──────────────────────────────────────────────────────────────────────── */ - -/** - * Enterprise on-ramp rung. The CTA differs by tier: free orgs start a guided - * trial, subscribed (paying) orgs jump straight to a quote — both open the - * procurement flow. When {@code onStart} is given the CTA opens the flow's setup - * modal over Home; otherwise it falls back to navigating to the procurement view. - */ -function EnterpriseRung({ - paying, - onStart, -}: { - paying: boolean; - onStart?: () => void; -}) { - const { t } = useTranslation(); - const { setActiveView } = useView(); - return ( -
-
- - {t("portal.home.onboarding.enterprise.tag")} - -

- {t("portal.home.onboarding.enterprise.lead")}{" "} - {t("portal.home.onboarding.enterprise.body")} -

-
- -
- ); -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Getting-started steps */ -/* ──────────────────────────────────────────────────────────────────────── */ - -interface Step { - id: string; - title: string; - blurb: string; - done: boolean; - onClick: () => void; -} - -/** - * Numbered getting-started steps, rendered as the body of the home hero. Each - * row opens its in-app surface; a completed step (from {@link OnboardingProgress}) - * swaps its number for a check. When every step is done the parent collapses the - * hero to the deployed-status header and stops rendering this list entirely. - */ -export function SetupChecklist({ - progress, - onStartEnterprise, -}: { - progress: OnboardingProgress; - /** Start the enterprise flow in place (opens the setup modal over Home). Falls back to - * navigating to the procurement view when omitted (e.g. in isolated stories). */ - onStartEnterprise?: () => void; -}) { - const { t } = useTranslation(); - const { tier } = useTier(); - const { setActiveView } = useView(); - const [downloadOpen, setDownloadOpen] = useState(false); - - const steps: Step[] = [ - { - id: "editor", - title: t("portal.home.onboarding.steps.editor.title"), - blurb: t("portal.home.onboarding.steps.editor.blurb"), - done: progress.editorDone, - // Downloads are per-OS, so open the install picker rather than route away. - onClick: () => setDownloadOpen(true), - }, - { - id: "policies", - title: t("portal.home.onboarding.steps.policies.title"), - blurb: t("portal.home.onboarding.steps.policies.blurb", { - active: progress.policiesActive, - recommended: progress.policiesRecommended, - }), - done: progress.policiesDone, - onClick: () => setActiveView("policies"), - }, - { - id: "invite", - title: t("portal.home.onboarding.steps.invite.title"), - blurb: t("portal.home.onboarding.steps.invite.blurb"), - done: progress.inviteDone, - onClick: () => setActiveView("users"), - }, - ]; - - return ( -
-
    - {steps.map((s, i) => ( -
  1. - -
  2. - ))} -
- - - - setDownloadOpen(false)} - /> -
- ); -} diff --git a/frontend/editor/src/portal/components/WelcomeBanner.css b/frontend/editor/src/portal/components/WelcomeBanner.css deleted file mode 100644 index 157ee24ac0..0000000000 --- a/frontend/editor/src/portal/components/WelcomeBanner.css +++ /dev/null @@ -1,103 +0,0 @@ -/* ──────────────────────────────────────────────────────────────────────── */ -/* Free-tier welcome hero — compact product header + steps */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-welcome { - border-radius: var(--radius-xl); - border: 1px solid var(--c-border-subtle); - overflow: hidden; - isolation: isolate; - background: var(--c-surface); -} - -/* ── Dark product header strip ── */ -.portal-welcome__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; - padding: 0.875rem 1.25rem; - background: color-mix(in srgb, var(--c-primary) 22%, var(--c-hero-dark)); -} - -.portal-welcome__brand { - display: flex; - align-items: center; - gap: 0.875rem; - min-width: 0; -} - -.portal-welcome__mark { - display: grid; - place-items: center; - flex-shrink: 0; -} -.portal-welcome__mark img { - display: block; - height: 1.75rem; - width: auto; -} - -.portal-welcome__brand-text { - display: flex; - align-items: baseline; - gap: 0.625rem; - min-width: 0; - flex-wrap: wrap; -} - -.portal-welcome__product { - font-size: 1.125rem; - font-weight: 700; - letter-spacing: -0.01em; - color: #fff; -} - -.portal-welcome__stats { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.8125rem; - color: rgba(255, 255, 255, 0.55); -} - -/* Header action group: icon buttons + the CTA. */ -.portal-welcome__actions { - display: flex; - align-items: center; - gap: 0.5rem; -} -.portal-welcome__icon-btn { - display: grid; - place-items: center; - width: 2.25rem; - height: 2.25rem; - flex-shrink: 0; - border-radius: var(--radius-md); - border: 1px solid rgba(255, 255, 255, 0.16); - background: rgba(255, 255, 255, 0.08); - color: #fff; - cursor: pointer; - transition: background var(--motion-fast); -} -.portal-welcome__icon-btn:hover { - background: rgba(255, 255, 255, 0.16); -} - -/* White CTA on the dark header, matching the marketing card. */ -.portal-welcome__header .portal-welcome__cta.sui-btn { - background: #ffffff; - border-color: #ffffff; - color: var(--c-hero-dark-cta-text); -} -.portal-welcome__header .portal-welcome__cta.sui-btn:hover { - background: rgba(255, 255, 255, 0.88); - border-color: rgba(255, 255, 255, 0.88); -} - -/* ── Steps + enterprise (setup checklist) sit directly under the header ── */ -.portal-welcome__footer { - background: var(--c-surface); -} diff --git a/frontend/editor/src/portal/components/WelcomeBanner.stories.tsx b/frontend/editor/src/portal/components/WelcomeBanner.stories.tsx deleted file mode 100644 index 8dba4caf36..0000000000 --- a/frontend/editor/src/portal/components/WelcomeBanner.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { WelcomeBanner } from "@portal/components/WelcomeBanner"; -import { SetupChecklist } from "@portal/components/SetupChecklist"; -import type { OnboardingProgress } from "@portal/hooks/useOnboardingProgress"; - -const progress: OnboardingProgress = { - loading: false, - deployed: false, - editorDone: false, - policiesDone: true, - inviteDone: false, - policiesActive: 2, - policiesRecommended: 5, - allComplete: false, -}; - -const meta: Meta = { - title: "Portal/Home/WelcomeBanner", - component: WelcomeBanner, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** The hero on its own, no attached footer. */ -export const Default: Story = {}; - -/** The hero as it renders on the free-tier home: the "Finish setting up" - * checklist attached as the footer strip. */ -export const WithSetupChecklist: Story = { - args: { - footer: , - }, -}; diff --git a/frontend/editor/src/portal/components/WelcomeBanner.tsx b/frontend/editor/src/portal/components/WelcomeBanner.tsx deleted file mode 100644 index c1d6857bdb..0000000000 --- a/frontend/editor/src/portal/components/WelcomeBanner.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { ReactNode } from "react"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@app/ui"; -import { useView } from "@portal/contexts/ViewContext"; -import { EDITOR_URL } from "@portal/auth/editorUrl"; -import { - DownloadIcon, - ExternalLinkIcon, - UserPlusIcon, -} from "@portal/components/icons"; -import { DownloadEditorModal } from "@portal/components/DownloadEditorModal"; -import markDark from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg"; -import "@portal/components/WelcomeBanner.css"; - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Free-tier welcome hero */ -/* */ -/* A compact product header — brand mark, "PDF Editor" + social-proof */ -/* stats, and a single "Open in browser" CTA — over the getting-started */ -/* steps (passed in as {@code footer}). Deliberately lean: the onboarding */ -/* steps, not marketing copy, are the point of the card. */ -/* ──────────────────────────────────────────────────────────────────────── */ - -interface WelcomeBannerProps { - /** - * The getting-started steps + enterprise rung, rendered inside the card - * below the header. Kept as a slot so the hero stays a presentational shell. - */ - footer?: ReactNode; -} - -export function WelcomeBanner({ footer }: WelcomeBannerProps) { - const { t } = useTranslation(); - const { setActiveView } = useView(); - const [installOpen, setInstallOpen] = useState(false); - - return ( -
-
-
- - - -
- - {t("portal.welcome.productName")} - - - {t("portal.welcome.stats")} - -
-
-
- - - -
-
- - {footer &&
{footer}
} - - setInstallOpen(false)} - /> -
- ); -} diff --git a/frontend/editor/src/portal/components/billing/BundleCheckoutModal.tsx b/frontend/editor/src/portal/components/billing/BundleCheckoutModal.tsx index b5d0d58a3b..7d322a7be6 100644 --- a/frontend/editor/src/portal/components/billing/BundleCheckoutModal.tsx +++ b/frontend/editor/src/portal/components/billing/BundleCheckoutModal.tsx @@ -665,7 +665,6 @@ export function BundleCheckoutModal({ {t("portal.billing.prepaid.buy.cancel", "Cancel")} - @@ -701,7 +700,6 @@ export function BundleCheckoutModal({ {t("portal.billing.prepaid.buy.back", "Back")} diff --git a/frontend/editor/src/portal/components/billing/FreePlanView.tsx b/frontend/editor/src/portal/components/billing/FreePlanView.tsx index d3a1baf036..e2bbb24a34 100644 --- a/frontend/editor/src/portal/components/billing/FreePlanView.tsx +++ b/frontend/editor/src/portal/components/billing/FreePlanView.tsx @@ -85,7 +85,6 @@ export function FreePlanView({ wallet, unsynced, onSubscribed }: Props) { const switchOnAction = isLeader ? ( } diff --git a/frontend/editor/src/portal/components/billing/PrepayModalHeader.tsx b/frontend/editor/src/portal/components/billing/PrepayModalHeader.tsx index c099836ee4..f07083f215 100644 --- a/frontend/editor/src/portal/components/billing/PrepayModalHeader.tsx +++ b/frontend/editor/src/portal/components/billing/PrepayModalHeader.tsx @@ -1,15 +1,14 @@ import { useTranslation } from "react-i18next"; -import { Button } from "@app/ui"; -// The trademarked Stirling wordmark — the font is baked into the SVG (no brand webfont is loaded), so -// we render the same asset the portal nav uses rather than styled text. Theme-switched in CSS. -import wordmarkLight from "@app/assets/brand/modern-logo/StirlingProcessorLogoBlackText.svg"; -import wordmarkDark from "@app/assets/brand/modern-logo/StirlingProcessorLogoWhiteText.svg"; +import { StepModalHeader } from "@portal/components/shared/StepModalHeader"; /** - * Shared header for the prepay-flow modals — the prepaid wizard (activation → calculator → pay, of 3) - * and the metered checkout (spend limit → payment, of 2): Stirling brand + "Step N of M" badge + close, - * an M-segment progress bar, and the step title. Pass {@code step=undefined} to hide the badge + - * progress (e.g. a terminal confirmation). + * The prepay flows' stepped header — the prepaid wizard (activation → calculator → pay, of 3) and + * the metered checkout (spend limit → payment, of 2). + * + * Chrome and copy only: the layout is the shared {@link StepModalHeader}, so this flow reads the + * same as every other stepped modal. Keeps the billing root class, which the framed-checkout rule + * targets to own the header's padding. Pass {@code step=undefined} to hide the badge + progress + * (e.g. a terminal confirmation). */ export function PrepayModalHeader({ step, @@ -24,68 +23,27 @@ export function PrepayModalHeader({ onClose: () => void; }) { const { t } = useTranslation(); - const showSteps = step != null; - const filled = step ?? 0; return ( -
-
-
- Stirling - -
-
- {showSteps && ( - - {t( - "portal.billing.prepaid.buy.step", - "Step {{current}} of {{total}}", - { current: step, total }, - )} - - )} -
-
- {showSteps && ( -
- = 1 ? "is-filled" : ""} /> - = 2 ? "is-filled" : ""} /> - {total >= 3 && = 3 ? "is-filled" : ""} />} -
- )} -
{title}
-
+ ); } diff --git a/frontend/editor/src/portal/components/billing/SpendLimitCard.tsx b/frontend/editor/src/portal/components/billing/SpendLimitCard.tsx index 9ee2fa1f5e..5c7b510618 100644 --- a/frontend/editor/src/portal/components/billing/SpendLimitCard.tsx +++ b/frontend/editor/src/portal/components/billing/SpendLimitCard.tsx @@ -191,7 +191,7 @@ export function SpendLimitCard({ > {t("portal.billing.spendLimit.cancel", "Cancel")} - diff --git a/frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx b/frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx index 4787c40b37..ee76a780c2 100644 --- a/frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx +++ b/frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx @@ -451,7 +451,6 @@ export function StripeCheckoutModal({ {t("portal.billing.checkout.cap.back", "Back")} - - - } - > -

{copy.body}

- {needsFile && ( -
- setFile(e.target.files?.[0] ?? null)} - /> - - - {file ? file.name : t("portal.procurement.modal.noFile")} - -
- )} - - ); -} diff --git a/frontend/editor/src/portal/components/procurement/DealJourney.stories.tsx b/frontend/editor/src/portal/components/procurement/DealJourney.stories.tsx deleted file mode 100644 index a59386dc3a..0000000000 --- a/frontend/editor/src/portal/components/procurement/DealJourney.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DealJourney } from "@portal/components/procurement/DealJourney"; -import { buildProcurement } from "@portal/mocks/procurement"; -import type { Deal } from "@portal/api/procurement"; -import "@portal/views/Procurement.css"; - -const data = buildProcurement("enterprise"); -const deal = data.deal as Deal; - -const meta: Meta = { - title: "Portal/Procurement/DealJourney", - component: DealJourney, - parameters: { layout: "padded" }, - args: { deal, journey: data.journey, onAdvance: () => {} }, -}; -export default meta; -type Story = StoryObj; - -// Mid-journey at the Agreement stage, the seeded deal state. -export const Default: Story = {}; - -// Evaluating: the trial strip shows runway + key; next step builds the quote. -export const AtTrial: Story = { - args: { deal: { ...deal, currentStage: "trial" } }, -}; - -// Terminal stage, provisioning, no further CTA. -export const Live: Story = { - args: { deal: { ...deal, currentStage: "active" } }, -}; diff --git a/frontend/editor/src/portal/components/procurement/DealJourney.tsx b/frontend/editor/src/portal/components/procurement/DealJourney.tsx deleted file mode 100644 index 3e8c86e58d..0000000000 --- a/frontend/editor/src/portal/components/procurement/DealJourney.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button, Card } from "@app/ui"; -import type { Deal, DealStage, JourneyStep } from "@portal/api/procurement"; -import { StageStepper } from "@portal/components/procurement/StageStepper"; - -/** - * The deal's commercial journey in one card: who's guiding it (the solutions - * engineer), where it sits (the stage stepper), trial runway while evaluating, - * and the single next action that advances the deal. Mirrors "one next action - * at a time"; the full per-stage checklist lives in the Documents card. - */ -export function DealJourney({ - deal, - journey, - onAdvance, - advancing = false, -}: { - deal: Deal; - journey: JourneyStep[]; - onAdvance: (stage: DealStage) => void; - advancing?: boolean; -}) { - const { t } = useTranslation(); - const { engineer, trial, currentStage } = deal; - const currentStep = journey.find((s) => s.stage === currentStage); - const isTerminal = - journey.length > 0 && journey[journey.length - 1].stage === currentStage; - - return ( - -
-
- - {t("portal.procurement.journey.eyebrow")} - -

- {t("portal.procurement.journey.title")} -

-

- {t("portal.procurement.journey.subtitle")} -

-
-
- - {t("portal.procurement.journey.engineerLabel")} - - {engineer.name} - - {engineer.email} - -
-
- -
- -
- - {currentStage === "trial" && ( -
- - {t("portal.procurement.journey.trialTitle")} - - - {t("portal.procurement.journey.daysLeft", { - count: trial.daysLeft, - })} - - {trial.key} -
- )} - -
-
- - - {isTerminal - ? t("portal.procurement.journey.live") - : t("portal.procurement.journey.nextStep", { - action: currentStep ? t(currentStep.gatingAction) : "", - })} - -
- {!isTerminal && currentStep && ( - - )} -
-
- ); -} diff --git a/frontend/editor/src/portal/components/procurement/DealStatusHero.stories.tsx b/frontend/editor/src/portal/components/procurement/DealStatusHero.stories.tsx index 9421de28bb..77c008ce41 100644 --- a/frontend/editor/src/portal/components/procurement/DealStatusHero.stories.tsx +++ b/frontend/editor/src/portal/components/procurement/DealStatusHero.stories.tsx @@ -12,6 +12,10 @@ const base: ProcurementSnapshot = { trialExtensionsUsed: 0, licensed: false, licenseKey: null, + businessName: null, + contactName: null, + contactEmail: null, + agreementSignedVersion: null, latestQuote: null, }; @@ -23,11 +27,11 @@ const meta: Meta = { args: { canSchedule: true, onExpand: () => {}, + onAcceptQuote: () => {}, onLicense: () => {}, onInvite: () => {}, onSchedule: () => {}, onManageTrial: () => {}, - onNavigate: () => {}, }, }; export default meta; diff --git a/frontend/editor/src/portal/components/procurement/DealStatusHero.tsx b/frontend/editor/src/portal/components/procurement/DealStatusHero.tsx index 092e259bb3..ef2172bb27 100644 --- a/frontend/editor/src/portal/components/procurement/DealStatusHero.tsx +++ b/frontend/editor/src/portal/components/procurement/DealStatusHero.tsx @@ -1,31 +1,62 @@ -import { useEffect } from "react"; +import { useEffect, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@app/ui"; -import type { ViewId } from "@portal/contexts/ViewContext"; import { FLOW_JOURNEY, + type DealStage, type ProcurementSnapshot, } from "@portal/api/procurement"; -import { StageStepper } from "@portal/components/procurement/StageStepper"; +import { + CalendarIcon, + CheckIcon, + DocumentsIcon, + KeyIcon, + UserPlusIcon, +} from "@portal/components/icons"; import { warmCalendly } from "@portal/components/procurement/CalendlyInline"; +import { openApiUrl } from "@portal/api/externalUrl"; import "@portal/views/Procurement.css"; +/** What each stage asks of the buyer, read out in the stage sentence. */ +const STAGE_SENTENCE: Record = { + // Exploring sits on the Trial rung: same sentence, since the ask is what differs. + exploring: "portal.procurement.hero.sentenceTrial", + trial: "portal.procurement.hero.sentenceTrial", + quote: "portal.procurement.hero.sentenceQuote", + security: "portal.procurement.hero.sentenceAgreement", + procurement: "portal.procurement.hero.sentencePayment", + active: "portal.procurement.hero.sentenceLive", +}; + +/** The primary action for each stage; expanding the flow runs it. */ +const STAGE_CTA: Record = { + exploring: "portal.procurement.hero.ctaExploring", + trial: "portal.procurement.hero.ctaTrial", + quote: "portal.procurement.hero.ctaQuote", + security: "portal.procurement.hero.ctaAgreement", + // Same label whether it links straight to Stripe or, lacking an invoice URL, opens the stage dialog + // where the invoice actions live — the buyer is being sent to the invoice either way. + procurement: "portal.procurement.payment.viewInvoice", + active: "portal.procurement.hero.open", +}; + /** - * The enterprise deal-status hero on Home (procurement lives here, not as a nav tab). Adapts to the - * deal stage: quick-action chips (trial countdown → manage, licence key, invite teammates, - * schedule a call), a rollout checklist during the trial, and a stage-specific primary CTA that - * expands the flow into the takeover modal. Matches the marketing prototype. + * The enterprise deal-status hero on Home (procurement lives here, not as a nav tab) — this card IS + * the procurement surface. It carries the journey as a segmented progress band plus a stage + * sentence, and one primary action with quiet icon buttons beside it; the flow itself opens in the + * takeover modal. Rollout setup lives in the non-procurement setup checklist, not here. */ export function DealStatusHero({ snapshot, busy = false, canSchedule, onExpand, + onAcceptQuote, onLicense, onInvite, onSchedule, onManageTrial, - onNavigate, + onDocuments, }: { snapshot: ProcurementSnapshot; busy?: boolean; @@ -33,11 +64,17 @@ export function DealStatusHero({ * "Schedule a call" action only appears when the org has linked its account. */ canSchedule: boolean; onExpand: () => void; + /** + * Accept the issued quote, which advances the deal to the agreement. Offered here rather than + * inside the quote review so the buyer can circulate the quote and come back to decide. + */ + onAcceptQuote: () => void; onLicense: () => void; onInvite: () => void; onSchedule: () => void; onManageTrial: () => void; - onNavigate: (view: ViewId) => void; + /** Open the Documents reference (agreement, quote, invoice, EULA, SLA, subprocessors). */ + onDocuments: () => void; }) { const { t } = useTranslation(); @@ -49,129 +86,181 @@ export function DealStatusHero({ const stage = snapshot.stage ?? "trial"; const inTrial = stage === "trial"; - const cta = - stage === "trial" - ? t("portal.procurement.hero.ctaTrial") - : stage === "quote" - ? t("portal.procurement.hero.ctaQuote") - : stage === "procurement" - ? t("portal.procurement.hero.ctaPayment") - : t("portal.procurement.hero.ctaLive"); + const isLive = stage === "active"; + // A live quote is sitting with the buyer. A draft (or an expired/cancelled one) is not something to + // accept — that stage still means "finish building it". + const quoteAwaitingDecision = + stage === "quote" && + (snapshot.latestQuote?.status === "sent" || + snapshot.latestQuote?.status === "open"); + // Paying happens on Stripe, so the card links straight there rather than opening a dialog whose only + // real action was the same link. Without an invoice URL there is nothing to link to, so the stage + // falls back to its dialog, where the signed agreement is still reachable. + const invoiceUrl = + stage === "procurement" ? snapshot.latestQuote?.invoiceUrl : null; + // Known from trial setup onward. The quote's own copy wins when present, since the buyer may have + // corrected it there; before either exists the eyebrow stands alone rather than inventing a name. + const company = + snapshot.latestQuote?.config.businessName?.trim() || + snapshot.businessName?.trim(); - const setupSteps: { title: string; sub: string; view: ViewId }[] = [ - { - title: t("portal.procurement.hero.setup1Title"), - sub: t("portal.procurement.hero.setup1Sub"), - view: "users", - }, - { - title: t("portal.procurement.hero.setup2Title"), - sub: t("portal.procurement.hero.setup2Sub"), - view: "sources", - }, - { - title: t("portal.procurement.hero.setup3Title"), - sub: t("portal.procurement.hero.setup3Sub"), - view: "policies", - }, - ]; + // Exploring is presented as the Trial rung — same position, sentence and next step — because the + // buyer has entered the journey; only the ask differs, since no trial has actually started. + const journeyStage = stage === "exploring" ? "trial" : stage; + const currentIdx = Math.max( + 0, + FLOW_JOURNEY.findIndex((s) => s.stage === journeyStage), + ); + const nextStage = FLOW_JOURNEY[currentIdx + 1]; return (
-
+
- {t("portal.procurement.hero.eyebrow")} + {company + ? t("portal.procurement.hero.eyebrowCompany", { company }) + : t("portal.procurement.hero.eyebrow")} - - {t("portal.procurement.hero.company")} - -
-
+ +
+ {FLOW_JOURNEY.map((s, i) => ( + + ))} +
+ +

+ {t(FLOW_JOURNEY[currentIdx].label)} + {` · ${t(STAGE_SENTENCE[stage])} `} + {nextStage && ( + + {t("portal.procurement.hero.next", { + stage: t(nextStage.label), + })} + + )} +

+ {inTrial && snapshot.trialEndsAt && ( - - )} - {snapshot.licenseKey && ( - - )} - {stage !== "active" && ( - - )} - {canSchedule && ( - +
+ +
)}
-
- -
- - {inTrial && ( -
    - {setupSteps.map((s) => ( -
  • - -
  • - ))} -
+ {isLive && ( +
+ + + + + + {t("portal.procurement.hero.liveTitle")} + + + {t("portal.procurement.hero.liveSub")} + + +
)} -
- - - {t("portal.procurement.hero.nextStep", { action: cta })} - -
- + + + ) : invoiceUrl ? ( + + ) : ( + + )} +
+ {snapshot.licenseKey && ( + + + + )} + + + + {!isLive && ( + + + + )} + {canSchedule && ( + + + + )}
); } +/** A quiet icon-only secondary action; its label carries in the tooltip and to screen readers. */ +function IconAction({ + label, + onClick, + children, +}: { + label: string; + onClick: () => void; + children: ReactNode; +}) { + return ( + + ); +} + function daysLeft(iso: string): number { const end = new Date(iso).getTime(); return Math.max(0, Math.ceil((end - Date.now()) / 86_400_000)); diff --git a/frontend/editor/src/portal/components/procurement/DocRow.stories.tsx b/frontend/editor/src/portal/components/procurement/DocRow.stories.tsx deleted file mode 100644 index 695b8c6941..0000000000 --- a/frontend/editor/src/portal/components/procurement/DocRow.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DocRow } from "@portal/components/procurement/DocRow"; -import type { LedgerDoc } from "@portal/api/procurement"; -import "@portal/views/Procurement.css"; - -const meta: Meta = { - title: "Portal/Procurement/DocRow", - component: DocRow, - parameters: { layout: "padded" }, - args: { onAction: () => {} }, -}; -export default meta; -type Story = StoryObj; - -const sign: LedgerDoc = { - id: "d1", - name: "Stirling Enterprise Agreement", - sub: "One signature: MSA + order form + EULA + DPA.", - status: "action", - action: "sign", -}; - -const download: LedgerDoc = { - id: "d2", - name: "SOC 2 Type II report", - sub: "Independent audit of our security controls.", - status: "available", - action: "download", -}; - -const paidAddon: LedgerDoc = { - id: "d3", - name: "Onboarding & training", - sub: "Guided rollout and live training for your team.", - status: "request", - action: "request", - optional: true, - fee: 7_500, -}; - -const done: LedgerDoc = { - id: "d4", - name: "Formal quote", - sub: "Committed-volume pricing, term and line items.", - status: "complete", - action: "download", -}; - -// Deal-advancing action, filled purple CTA. -export const SignAction: Story = { args: { doc: sign } }; - -// Quiet outline action for a ready download. -export const Download: Story = { args: { doc: download } }; - -// Optional paid add-on, chips flag it and the fee folds into the CTA. -export const PaidAddon: Story = { args: { doc: paidAddon } }; - -// Completed paperwork keeps a record but offers no further action. -export const Complete: Story = { args: { doc: done } }; - -// A row in a future, not-yet-reached stage, dimmed, marked "Upcoming", inert. -export const Locked: Story = { args: { doc: sign, locked: true } }; diff --git a/frontend/editor/src/portal/components/procurement/DocRow.tsx b/frontend/editor/src/portal/components/procurement/DocRow.tsx deleted file mode 100644 index b4329fbfa4..0000000000 --- a/frontend/editor/src/portal/components/procurement/DocRow.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button, Chip, StatusBadge } from "@app/ui"; -import type { LedgerDoc } from "@portal/api/procurement"; -import { - ACTION_LABEL_KEY, - STATUS_LABEL_KEY, - STATUS_TONE, - USD, -} from "@portal/components/procurement/format"; - -/** Maps a document's action to the button accent + variant. */ -function buttonStyle(doc: LedgerDoc): { - variant: "primary" | "secondary"; - accent: "premium" | "default"; -} { - // The agreement signature and online payment are the deal-advancing actions; - // give them the filled premium CTA. Everything else is a quieter outline. - if (doc.action === "sign" || doc.action === "pay") { - return { variant: "primary", accent: "premium" }; - } - return { variant: "secondary", accent: "default" }; -} - -/** - * A single document in the ledger or supporting pool: name + sub-line on the - * left, status badge and action button on the right. Optional/fee-bearing docs - * carry a chip so the buyer sees a paid add-on before clicking. `locked` is for - * rows in a future, not-yet-reached stage: dimmed, marked "Upcoming", inert. - */ -export function DocRow({ - doc, - onAction, - locked = false, -}: { - doc: LedgerDoc; - onAction: (doc: LedgerDoc) => void; - locked?: boolean; -}) { - const { t } = useTranslation(); - const { variant, accent } = buttonStyle(doc); - // Locked (future-stage), in-progress (pending) and completed paperwork all - // offer no action; only "available", "action" and "request" docs do. - const actionable = - !locked && doc.status !== "complete" && doc.status !== "pending"; - const label = t(ACTION_LABEL_KEY[doc.action]); - const actionLabel = - doc.fee !== undefined ? `${label} · ${USD.format(doc.fee)}` : label; - - return ( -
-
-
- {doc.name} - {doc.optional && ( - - {t("portal.procurement.docs.optional")} - - )} - {doc.fee !== undefined && ( - - {t("portal.procurement.docs.paidAddon")} - - )} -
-

{doc.sub}

-
-
- - {locked - ? t("portal.procurement.docs.upcoming") - : t(STATUS_LABEL_KEY[doc.status])} - - {actionable && ( - - )} -
-
- ); -} diff --git a/frontend/editor/src/portal/components/procurement/DocumentLedger.stories.tsx b/frontend/editor/src/portal/components/procurement/DocumentLedger.stories.tsx deleted file mode 100644 index 5de6692999..0000000000 --- a/frontend/editor/src/portal/components/procurement/DocumentLedger.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DocumentLedger } from "@portal/components/procurement/DocumentLedger"; -import { buildProcurement } from "@portal/mocks/procurement"; -import "@portal/views/Procurement.css"; - -const data = buildProcurement("enterprise"); - -const meta: Meta = { - title: "Portal/Procurement/DocumentLedger", - component: DocumentLedger, - parameters: { layout: "padded" }, - args: { - groups: data.ledger, - supporting: data.supporting, - journey: data.journey, - currentStage: data.deal?.currentStage ?? "trial", - onAction: () => {}, - }, -}; -export default meta; -type Story = StoryObj; - -// Mid-journey: the Agreement stage is open, earlier stages read as done, later -// stages are locked previews, and the supporting pool sits collapsed below. -export const Default: Story = {}; - -// Day one: only the Trial stage has been reached; everything ahead is locked. -export const AtTrial: Story = { - args: { currentStage: "trial" }, -}; diff --git a/frontend/editor/src/portal/components/procurement/DocumentLedger.tsx b/frontend/editor/src/portal/components/procurement/DocumentLedger.tsx deleted file mode 100644 index aa9e6ae8ce..0000000000 --- a/frontend/editor/src/portal/components/procurement/DocumentLedger.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Card, Chip, Collapsible } from "@app/ui"; -import type { - DealStage, - JourneyStep, - LedgerDoc, - LedgerGroup, - SupportingGroup, -} from "@portal/api/procurement"; -import { DocRow } from "@portal/components/procurement/DocRow"; - -/** - * The "Documents" card: every artifact the deal needs, as a stage accordion - * that mirrors the journey. Only the current stage is open by default; earlier - * stages read as done, later stages are locked previews. A collapsed-by-default - * "Supporting your evaluation" pool holds the stage-agnostic paperwork. - */ -export function DocumentLedger({ - groups, - supporting, - journey, - currentStage, - onAction, -}: { - groups: LedgerGroup[]; - supporting: SupportingGroup[]; - journey: JourneyStep[]; - currentStage: DealStage; - onAction: (doc: LedgerDoc) => void; -}) { - const { t } = useTranslation(); - const order = journey.map((s) => s.stage); - const curIdx = order.indexOf(currentStage); - // Follow the deal: the stage you're in opens first; any other stage can be - // peeked. null collapses them all. Advancing moves the open section along. - const [openStage, setOpenStage] = useState(currentStage); - const [supportingOpen, setSupportingOpen] = useState(false); - useEffect(() => setOpenStage(currentStage), [currentStage]); - - return ( - -
-

- {t("portal.procurement.docs.title")} -

-

- {t("portal.procurement.docs.subtitle")} -

-
- -
- {groups.map((group) => { - const idx = order.indexOf(group.stage); - const done = idx < curIdx; - const cur = group.stage === currentStage; - const locked = idx > curIdx; - const blurb = journey.find((s) => s.stage === group.stage)?.blurb; - const open = openStage === group.stage; - const count = group.docs.length; - - return ( - setOpenStage(open ? null : group.stage)} - header={ - <> - - - {t(group.label)} - - {blurb && ( - - · {t(blurb)} - - )} - {cur && ( - - {t("portal.procurement.docs.here")} - - )} - {done && ( - - {t("portal.procurement.docs.done")} - - )} - - } - aside={ - - {t("portal.procurement.docs.count", { count })} - - } - > -
- {group.docs.map((doc) => ( - - ))} -
-
- ); - })} - - {supporting.length > 0 && ( - setSupportingOpen((o) => !o)} - header={ - - - {t("portal.procurement.docs.supportingTitle")} - - - {t("portal.procurement.docs.supportingSubtitle")} - - - } - aside={ - - {supportingOpen - ? t("portal.procurement.docs.hide") - : t("portal.procurement.docs.show")} - - } - > -
- {supporting.map((group) => ( -
-
{group.label}
-
- {group.docs.map((doc) => ( - - ))} -
-
- ))} -
-
- )} -
-
- ); -} diff --git a/frontend/editor/src/portal/components/procurement/LockedState.stories.tsx b/frontend/editor/src/portal/components/procurement/LockedState.stories.tsx deleted file mode 100644 index eae9db4d99..0000000000 --- a/frontend/editor/src/portal/components/procurement/LockedState.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { LockedState } from "@portal/components/procurement/LockedState"; -import { JOURNEY } from "@portal/api/procurement"; -import "@portal/views/Procurement.css"; - -const meta: Meta = { - title: "Portal/Procurement/LockedState", - component: LockedState, - parameters: { layout: "padded" }, - args: { onTalkToSales: () => {} }, -}; -export default meta; -type Story = StoryObj; - -// Shown to free/pro buyers, the journey preview behind the upgrade prompt. -export const Default: Story = { - args: { journey: JOURNEY }, -}; diff --git a/frontend/editor/src/portal/components/procurement/LockedState.tsx b/frontend/editor/src/portal/components/procurement/LockedState.tsx deleted file mode 100644 index 8e70ba18c4..0000000000 --- a/frontend/editor/src/portal/components/procurement/LockedState.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button, Card, EmptyState } from "@app/ui"; -import type { JourneyStep } from "@portal/api/procurement"; -import { StageStepper } from "@portal/components/procurement/StageStepper"; - -/** - * Enterprise-only gate for free/pro buyers. Shows the journey as a greyed - * preview behind an upgrade prompt so the buyer understands what the - * commercial track looks like before they talk to sales. - */ -export function LockedState({ - journey, - onTalkToSales, -}: { - journey: JourneyStep[]; - onTalkToSales: () => void; -}) { - const { t } = useTranslation(); - return ( -
- - {t("portal.procurement.locked.talkToSales")} - - } - /> - - - -
- ); -} diff --git a/frontend/editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx b/frontend/editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx index a6ca8dfcf5..fd47d867c6 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx @@ -69,10 +69,9 @@ const meta: Meta = { args: { quote, busy: false, - downloading: false, onAgree: () => {}, - onDownload: () => {}, - onEdit: () => {}, + onRequestChanges: () => {}, + onClose: () => {}, }, }; export default meta; @@ -81,12 +80,7 @@ type Story = StoryObj; export const Default: Story = {}; -// Agreeing: the primary CTA shows its loading state while the accept call is in flight. -export const Agreeing: Story = { +// Signing: the primary CTA shows its loading state while the accept call is in flight. +export const Signing: Story = { args: { busy: true }, }; - -// Downloading: the secondary action shows its loading state instead. -export const Downloading: Story = { - args: { downloading: true }, -}; diff --git a/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx b/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx index 336b6af5cb..318f23aca5 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx @@ -1,152 +1,267 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Card } from "@app/ui"; -import type { QuoteResult } from "@portal/api/procurement"; -import { money } from "@portal/components/procurement/format"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Button } from "@app/ui"; +import { + fetchAgreementDocument, + fetchAgreementPdf, + recordAgreementSignature, + type QuoteResult, +} from "@portal/api/procurement"; +import { DownloadIcon } from "@portal/components/icons"; +import { StepModalHeader } from "@portal/components/shared/StepModalHeader"; +import { useAsync } from "@portal/hooks/useAsync"; import "@portal/views/Procurement.css"; /** - * The agreement (security) step: a single combined Stirling Enterprise Agreement — Master Service - * Agreement + Order Form (from the issued quote) + EULA + Data Processing Agreement — that the buyer - * reviews and agrees to before it's accepted into a subscription. No e-signature for now: an explicit - * "I agree" click stands in (the terms reference the accepted quote). Document body is static legal - * copy; the surrounding UI is translated. + * The agreement (security) step: the buyer reviews the full Stirling Enterprise Agreement — Master + * Services Agreement + Order Form (from the quote) + Data Processing Addendum, one signature — then + * signs it. The document body is served by the backend from the versioned legal registry (static + * legal copy, English only); this component renders it, gates signing behind a scroll-through, and + * captures the typed legal name, signatory, title, and authority. On sign it records the signature + * (pinned to the exact document version + a hash) and then accepts the quote into a subscription. + * + * Presented as the document itself rather than a card about it: this step names the agreement in the + * dialog's own header and carries its download there, so the terms are read on paper-like stock + * instead of in app chrome. That is why it draws its own header — see ProcurementFlow. */ export function ProcurementAgreement({ quote, busy, - downloading, onAgree, - onDownload, - onEdit, + onRequestChanges, + onClose, }: { quote: QuoteResult; busy: boolean; - downloading: boolean; - /** Accept the quote straight into a committed subscription (this is also the agreement). */ + /** Accept the quote straight into a committed subscription (runs after the signature is saved). */ onAgree: () => void; - onDownload: () => void; - onEdit: () => void; + /** Hand the buyer to their SE to negotiate terms: closes this and opens scheduling. */ + onRequestChanges: () => void; + /** This step draws the dialog's header, so it carries the close too. */ + onClose?: () => void; }) { const { t } = useTranslation(); - const [checked, setChecked] = useState(false); - const annual = money(quote.annualNetMinor, quote.currency); - const tcv = money(quote.tcvMinor, quote.currency); - const renewal = money(quote.renewalAnnualNetMinor, quote.currency); - const years = quote.config.termYears; + const { data: doc, loading } = useAsync(fetchAgreementDocument, []); + + const [legalName, setLegalName] = useState(quote.config.businessName ?? ""); + const [signatory, setSignatory] = useState(quote.config.contactName ?? ""); + const [title, setTitle] = useState(""); + const [confirmed, setConfirmed] = useState(false); + const [scrolledToEnd, setScrolledToEnd] = useState(false); + const [signing, setSigning] = useState(false); + const [downloadingMsa, setDownloadingMsa] = useState(false); + const [error, setError] = useState(false); + const [downloadError, setDownloadError] = useState(false); + const docRef = useRef(null); + + const downloadMsa = async () => { + setDownloadingMsa(true); + setDownloadError(false); + try { + const blob = await fetchAgreementPdf(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "stirling-enterprise-agreement.pdf"; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 60_000); + } catch { + // Surface the failure — the PDF is rendered server-side, so a failure here means the + // render service is unavailable rather than something the buyer can retry around. + setDownloadError(true); + } finally { + setDownloadingMsa(false); + } + }; + + const onScroll = () => { + const el = docRef.current; + if (!el) return; + if (el.scrollTop + el.clientHeight >= el.scrollHeight - 24) { + setScrolledToEnd(true); + } + }; + + const ready = + scrolledToEnd && + confirmed && + legalName.trim().length > 0 && + signatory.trim().length > 0; + + const sign = async () => { + setError(false); + setSigning(true); + try { + await recordAgreementSignature({ + customerLegalName: legalName.trim(), + signatoryName: signatory.trim(), + signatoryTitle: title.trim(), + authorityConfirmed: confirmed, + }); + onAgree(); // proceed into the committed subscription + } catch { + setError(true); + } finally { + // In `finally`, not only on failure: accepting can fail after the signature is recorded, and + // the controller deliberately keeps this dialog open on failure so the error is readable. With + // the flag left set, the button span the rest of the session and there was no way to retry. + setSigning(false); + } + }; return ( - - - {t("portal.procurement.agreement.eyebrow")} - -

- {t("portal.procurement.agreement.title")} -

-

- {t("portal.procurement.agreement.intro")} -

+
+ + {/* On the document, like the quote's: it downloads what is on screen. */} + + {/* Redlines are a conversation, not a form: this hands the buyer to their SE rather than + pretending the terms can be amended in the app. */} + +
+ } + /> -
-

1. Master Service Agreement

-

- This Stirling Enterprise Agreement ("Agreement") is entered into - between Stirling PDF Inc. ("Stirling") and the customer identified on - the Order Form ("Customer"). It governs Customer's access to and use - of the Stirling enterprise platform and related services (the - "Service"). Stirling will provide the Service with commercially - reasonable skill and care and in accordance with the service levels - set out in the Order Form. -

+ {/* The tray scrolls, not the paper. The paper is its natural height inside it, so mid-document it + runs flush to the footer with no grey beneath, and the tray's bottom padding only comes into + view once the buyer reaches the end — the page ending is what shows they got there. */} +
+
+ {loading &&

{t("portal.procurement.agreement.loading")}

} + {!loading && !doc && ( +

{t("portal.procurement.agreement.loadError")}

+ )} + {doc && ( + <> + {/* Letterhead: the reference ties the terms to the quote they price, and the version + label pins what was signed. The document's own heading follows, so this adds a + masthead rather than repeating the title. */} +
+ + {t("portal.procurement.agreement.confidential")} + + + {t("portal.procurement.agreement.ref", { + ref: quote.quoteNumber, + version: doc.versionLabel, + })} + +
+
+ {doc.markdown} +
+ + )} +
+
-

2. Order Form

-

- Quote {quote.quoteNumber} forms the Order Form for - this Agreement. Customer commits to a {years}-year term at{" "} - {annual} per year (total contract value{" "} - {tcv}), billed annually in advance by invoice. Fees - are exclusive of taxes. The committed volume, service level, and - add-ons are itemised below: + {error && ( +

+ {t("portal.procurement.agreement.signError")}

-
    - {quote.lineItems.map((li) => ( -
  • - {li.label} - - {li.kind === "INCLUDED" - ? t("portal.procurement.builder.included") - : money(li.amountMinor, quote.currency)} + )} + {downloadError && ( +

    + {t("portal.procurement.agreement.downloadDraftError")} +

    + )} + + {/* The signature block: who is bound, who signs, and the act of signing, on one line — the + shape of a paper signature block rather than a form above a button. The consent sits + directly under the fields it qualifies, with no rule between them: it is part of signing, + not a separate section, and boxing it cost the document a quarter of its height. */} +
    +
    +
    +
  • - ))} -
+ setLegalName(e.target.value)} + /> + + + +
+ +
-

3. Term, renewal and annual fee adjustment

-

- This Agreement runs for the committed {years}-year term set out in the - Order Form. It then renews automatically for successive one-year terms - unless either party gives written notice of non-renewal at least 30 - days before the end of the then-current term. On each renewal the - annual fee increases by {quote.cpiRatePct}%, a fixed CPI adjustment. - Based on this quote, the first renewal year would be approximately{" "} - {renewal} per year; the committed term above is - billed at the rate in the Order Form and is not affected. -

- -

4. End-User License Agreement

-

- Subject to the terms of this Agreement, Stirling grants Customer a - non-exclusive, non-transferable right to use the Service for its - internal business purposes during the term. Customer is responsible - for its users' compliance and for the content it processes. The - Service, and all intellectual property in it, remains Stirling's. -

- -

5. Data Processing Agreement

-

- Where Stirling processes personal data on Customer's behalf, it does - so only on Customer's documented instructions and applies appropriate - technical and organisational measures. Sub-processors, international - transfers, and security commitments are as described in Stirling's - Data Processing Agreement and Trust Center, incorporated here by - reference. -

- -

6. Acceptance

-

- By agreeing below, Customer accepts this Agreement and the Order Form. - On acceptance, Stirling will issue the committed annual subscription - and its first invoice. This preview stands in for e-signature during - the pilot. -

+ {/* Consent under the fields it qualifies; the gate's state under the button it gates, so the + reason signing is unavailable sits beside the unavailable thing. */} +
+ + {doc && !scrolledToEnd && ( + + {t("portal.procurement.agreement.scrollHint")} + + )} +
- - - -
- - - -
- + ); } diff --git a/frontend/editor/src/portal/components/procurement/ProcurementBanner.stories.tsx b/frontend/editor/src/portal/components/procurement/ProcurementBanner.stories.tsx deleted file mode 100644 index 28e9d2f99a..0000000000 --- a/frontend/editor/src/portal/components/procurement/ProcurementBanner.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ProcurementBanner } from "@portal/components/procurement/ProcurementBanner"; -import type { ProcurementController } from "@portal/components/procurement/useProcurement"; -import type { ProcurementSnapshot } from "@portal/api/procurement"; - -const snapshot: ProcurementSnapshot = { - dealId: 1, - stage: "trial", - deployment: "cloud", - seats: 250, - trialStartedAt: "2026-06-25T00:00:00Z", - trialEndsAt: "2026-07-09T00:00:00Z", - trialExtensionsUsed: 0, - licensed: false, - licenseKey: null, - latestQuote: null, -}; - -function makeController( - overrides: Partial = {}, -): ProcurementController { - return { - isLinked: true, - loading: false, - data: null, - started: false, - stage: undefined, - latest: null, - isIssued: false, - isDraft: true, - busy: false, - downloading: false, - downloadingLicense: false, - error: null, - setError: () => {}, - open: false, - setOpen: () => {}, - editing: false, - setEditing: () => {}, - extra: null, - setExtra: () => {}, - invoicePdf: null, - onStartTrial: () => {}, - onConfirmSetup: () => {}, - onExtendTrial: () => {}, - onReset: () => {}, - onGenerate: () => {}, - onAgree: () => {}, - onDownloadPdf: async () => {}, - onDownloadOfflineLicense: async () => {}, - ...overrides, - }; -} - -/** Deal-status hero once a deal is underway, otherwise the enterprise on-ramp. */ -const meta: Meta = { - title: "Portal/Procurement/ProcurementBanner", - component: ProcurementBanner, - parameters: { layout: "padded" }, -}; -export default meta; - -type Story = StoryObj; - -/** No deal yet: the enterprise on-ramp upsell. */ -export const Upsell: Story = { - args: { controller: makeController() }, -}; - -/** A deal is underway: the wired deal-status hero. */ -export const DealUnderway: Story = { - args: { - controller: makeController({ - started: true, - data: snapshot, - stage: snapshot.stage, - }), - }, -}; diff --git a/frontend/editor/src/portal/components/procurement/ProcurementBanner.tsx b/frontend/editor/src/portal/components/procurement/ProcurementBanner.tsx index d47ffa0b48..626cc8e9fb 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementBanner.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementBanner.tsx @@ -1,13 +1,10 @@ -import { useTranslation } from "react-i18next"; -import { Button, Card } from "@app/ui"; import { useView } from "@portal/contexts/ViewContext"; import { DealStatusHero } from "@portal/components/procurement/DealStatusHero"; import type { ProcurementController } from "@portal/components/procurement/useProcurement"; /** - * The deal-status hero, wired to a shared ProcurementController. Rendered both - * standalone (the /procurement route) and as the Home hero card's footer once a - * deal is underway. Assumes an active deal (controller.data present). + * The deal-status hero, wired to a shared ProcurementController. Rendered as the Home hero card's + * footer once a deal is underway; assumes an active deal (controller.data present). */ export function ControlledDealStatusHero({ controller, @@ -21,60 +18,18 @@ export function ControlledDealStatusHero({ snapshot={controller.data} busy={controller.busy} canSchedule={controller.isLinked} - onExpand={() => controller.setOpen(true)} + onExpand={() => + // Exploring has no journey to expand into yet — its ask is to set the trial up. + controller.stage === "exploring" + ? controller.onStartTrial() + : controller.setOpen(true) + } + onAcceptQuote={() => void controller.onAcceptQuote()} onLicense={() => controller.setExtra("license")} onInvite={() => setActiveView("users")} onSchedule={() => controller.setExtra("schedule")} onManageTrial={() => controller.setExtra("trial")} - onNavigate={setActiveView} + onDocuments={() => controller.setExtra("documents")} /> ); } - -/** - * Enterprise on-ramp shown when no deal exists yet. Only used on the dedicated - * /procurement route — on Home the setup checklist's Enterprise rung owns the - * on-ramp, so this doesn't render there. - */ -export function ProcurementUpsell({ - controller, -}: { - controller: ProcurementController; -}) { - const { t } = useTranslation(); - return ( - -
- - {t("portal.procurement.upsell.homeBadge")} - -

- {t("portal.procurement.upsell.homeHeadline")} - {t("portal.procurement.upsell.homeBody")} -

-
- -
- ); -} - -/** Deal-status hero when a deal is underway, otherwise the enterprise on-ramp. */ -export function ProcurementBanner({ - controller, -}: { - controller: ProcurementController; -}) { - return controller.isLinked && controller.started && controller.data ? ( - - ) : ( - - ); -} diff --git a/frontend/editor/src/portal/components/procurement/ProcurementExtras.stories.tsx b/frontend/editor/src/portal/components/procurement/ProcurementExtras.stories.tsx index 06361da844..7a6c6ffb18 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementExtras.stories.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementExtras.stories.tsx @@ -31,6 +31,10 @@ const SNAPSHOT: ProcurementSnapshot = { trialExtensionsUsed: 0, licensed: false, licenseKey: null, + agreementSignedVersion: null, + businessName: null, + contactName: null, + contactEmail: null, latestQuote: null, }; @@ -72,13 +76,14 @@ export const ScheduleCall: Story = { ), }; -// Deployment + seat count captured before the trial starts. +// Two steps before the trial starts: how they'll run it, then who is buying. export const TrialSetup: Story = { render: () => ( {}} busy={false} + onScheduleCall={() => {}} onConfirm={() => {}} /> ), diff --git a/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx b/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx index 44bbea1264..10ea20ed0c 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx @@ -1,11 +1,20 @@ import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { Button } from "@app/ui"; -import type { ProcurementSnapshot } from "@portal/api/procurement"; +import { + fetchLegalDocument, + recordLegalConsent, + type ProcurementSnapshot, + type TrialSetupDetails, +} from "@portal/api/procurement"; +import { StepModalHeader } from "@portal/components/shared/StepModalHeader"; import { CalendlyInline } from "@portal/components/procurement/CalendlyInline"; import { LicensePanel } from "@portal/components/procurement/ProcurementStages"; -import { useFocusTrap } from "@portal/components/procurement/ProcurementModal"; +import { FlowModal } from "@portal/components/shared/FlowModal"; +import { useAsync } from "@portal/hooks/useAsync"; +import { openApiUrl } from "@portal/api/externalUrl"; import "@portal/views/Procurement.css"; /** @@ -14,6 +23,8 @@ import "@portal/views/Procurement.css"; * scheduler. The shells and wiring are real so the hero behaves like the marketing prototype. */ +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + function SideModal({ open, onClose, @@ -21,56 +32,243 @@ function SideModal({ subtitle, children, footer, + headerAside, wide = false, }: { open: boolean; onClose: () => void; title: string; subtitle?: string; + /** Sits on the title row, before the close button (e.g. a "Step 1 of 2" badge). */ + headerAside?: React.ReactNode; children: React.ReactNode; footer?: React.ReactNode; wide?: boolean; }) { - const { t } = useTranslation(); - const trapRef = useFocusTrap(open); - - useEffect(() => { - if (!open) return; - const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [open, onClose]); - - if (!open) return null; - return createPortal( -
e.target === e.currentTarget && onClose()} - > -
- -
-

{title}

+ return ( + +
+

{title}

+ {headerAside} +
{subtitle &&

{subtitle}

} + + } + > + {children} +
+ ); +} + +/** + * Reader for a versioned legal document (EULA, SLA exhibit, subprocessors), fetched from the + * backend registry and rendered as markdown. Open when {@code docId} is set. Drafts are badged. + */ +export function LegalDocumentModal({ + docId, + onClose, +}: { + docId: string | null; + onClose: () => void; +}) { + const { t } = useTranslation(); + const { data, loading } = useAsync( + () => (docId ? fetchLegalDocument(docId) : Promise.resolve(null)), + [docId], + ); + return ( + + {loading && ( +

{t("portal.legal.loading")}

+ )} + {!loading && !data && ( +

{t("portal.legal.loadError")}

+ )} + {data && ( +
+ {data.markdown}
-
{children}
- {footer &&
{footer}
} + )} +
+ ); +} + +// ── Documents ──────────────────────────────────────────────────────────────── +/** + * The deal's paperwork in one place, reachable throughout the journey (not tied to the current + * stage): the enterprise agreement, the quote, the invoice, and the reference documents (EULA, SLA + * exhibit, subprocessors). Each row downloads or views the real artifact when it's available, and + * reads as "available later" until then. The per-stage download buttons remain the primary path; + * this is the secondary, always-on reference. + */ +export function DocumentsModal({ + open, + onClose, + agreementVersion, + downloadingAgreement, + onDownloadAgreement, + onViewAgreement, + quoteAvailable, + downloadingQuote, + onDownloadQuote, + invoiceUrl, + invoicePdf, +}: { + open: boolean; + onClose: () => void; + agreementVersion?: string | null; + downloadingAgreement?: boolean; + onDownloadAgreement: () => void; + /** Jump to the agreement/sign stage in the flow (used before it's signed). */ + onViewAgreement: () => void; + quoteAvailable: boolean; + downloadingQuote?: boolean; + onDownloadQuote: () => void; + invoiceUrl?: string | null; + invoicePdf?: string | null; +}) { + const { t } = useTranslation(); + const [legalDoc, setLegalDoc] = useState(null); + const invoice = invoiceUrl || invoicePdf || null; + + return ( + <> + +
    + + + openApiUrl(invoice), + } + : { + unavailable: t("portal.procurement.documents.laterInvoice"), + } + } + /> + setLegalDoc("eula"), + }} + /> + setLegalDoc("sla"), + }} + /> + setLegalDoc("subprocessors"), + }} + /> +
+
+ setLegalDoc(null)} /> + + ); +} + +/** One row in the Documents list: name + sub on the left, an action button or a muted note. */ +function DocItem({ + name, + sub, + action, +}: { + name: string; + sub: string; + action: + | { label: string; onClick: () => void; loading?: boolean } + | { unavailable: string }; +}) { + return ( +
  • +
    + {name} + {sub}
    -
  • , - document.body, + {"unavailable" in action ? ( + {action.unavailable} + ) : ( + + )} + ); } @@ -154,82 +352,233 @@ export function TrialSetupModal({ open, onClose, busy, + email, + onScheduleCall, onConfirm, }: { open: boolean; onClose: () => void; busy: boolean; - onConfirm: (deployment: string, seats: number) => void; + /** Linked-account email, prefilled as the work email on the details step. */ + email?: string; + /** Open the scheduler — the step-1 escape hatch for buyers who want to talk first. */ + onScheduleCall: () => void; + onConfirm: ( + deployment: string, + seats: number, + details: TrialSetupDetails, + ) => void; }) { const { t } = useTranslation(); + const [step, setStep] = useState(0); const [deployment, setDeployment] = useState("cloud"); const [seats, setSeats] = useState(""); + const [contactName, setContactName] = useState(""); + const [businessName, setBusinessName] = useState(""); + const [contactEmail, setContactEmail] = useState(""); + const [inviteEmails, setInviteEmails] = useState(""); + const [eula, setEula] = useState(false); + const [legalDoc, setLegalDoc] = useState(null); // Reset to defaults each time the dialog opens, so a cancelled setup doesn't linger. useEffect(() => { if (open) { + setStep(0); setDeployment("cloud"); setSeats(""); + setContactName(""); + setBusinessName(""); + setContactEmail(email ?? ""); + setInviteEmails(""); + setEula(false); } - }, [open]); + }, [open, email]); + + // The buying entity is what the quote and agreement are drawn against, so it is required here + // rather than deferred to the quote; invites are genuinely optional. + const detailsValid = + contactName.trim().length > 0 && + businessName.trim().length > 0 && + EMAIL_RE.test(contactEmail.trim()); + + const confirm = () => { + void recordLegalConsent("eula", "trial"); // clickwrap consent, best-effort + onConfirm(deployment, Math.max(0, Number(seats) || 0), { + businessName: businessName.trim(), + contactName: contactName.trim(), + contactEmail: contactEmail.trim(), + inviteEmails: inviteEmails.trim(), + }); + }; return ( - onConfirm(deployment, Math.max(0, Number(seats) || 0))} - > - {t("portal.procurement.setup.start")} - - } - > - + + + ) : ( + <> + + + + ) + } + > + - -

    - {t("portal.procurement.setup.seatsHint")} -

    -
    + {step === 0 && ( + <> + + + + )} + + {step === 1 && ( + <> +
    + + +
    + + + + + + )} + + setLegalDoc(null)} /> + ); } @@ -280,7 +629,6 @@ export function TrialManageModal({ } @@ -103,46 +113,63 @@ export function ProcurementFlow({ {isLinked && started && ( <> -
    - -
    - - {(editing || - (isDraft && (stage === "trial" || stage === "quote"))) && ( + {builderShowing && ( setOpen(false)} onGenerate={onGenerate} + // Null while re-editing: the buyer asked for the form, not the paper they just left. + issued={!editing && isIssued ? latest : null} + downloading={downloading} + onDownload={onDownloadPdf} /> )} - {/* Quote + agreement are one step: review the itemised quote and the agreement, then - accept straight into a committed subscription. Once accepted you can't go back. - ("security" is the retired agreement stage — still handled so an older deal that - stopped there isn't left blank.) */} - {!editing && - isIssued && - (stage === "quote" || stage === "security") && - latest && ( - setEditing(true)} - /> - )} + {/* Agreement step: review and sign the enterprise agreement. Signing accepts the quote + into a committed subscription (Stripe). */} + {agreementShowing && latest && ( + { + setOpen(false); + setExtra("schedule"); + }} + onClose={() => setOpen(false)} + /> + )} {!editing && stage === "procurement" && latest && ( )} - {!editing && stage === "active" && } + {!editing && stage === "active" && ( + + )} )} @@ -151,6 +178,8 @@ export function ProcurementFlow({ open={extra === "setup"} onClose={() => setExtra(null)} busy={busy} + email={scheduleEmail ?? undefined} + onScheduleCall={() => setExtra("schedule")} onConfirm={onConfirmSetup} /> {data?.licenseKey && ( @@ -185,6 +214,23 @@ export function ProcurementFlow({ }} /> )} + setExtra(null)} + agreementVersion={data?.agreementSignedVersion} + downloadingAgreement={downloadingAgreement} + onDownloadAgreement={onDownloadSignedAgreement} + onViewAgreement={() => { + setExtra(null); + setEditing(false); + setOpen(true); + }} + quoteAvailable={!!latest?.stripeQuoteId} + downloadingQuote={downloading} + onDownloadQuote={onDownloadPdf} + invoiceUrl={latest?.invoiceUrl} + invoicePdf={latest?.invoicePdf ?? invoicePdf} + /> ); } diff --git a/frontend/editor/src/portal/components/procurement/ProcurementHome.stories.tsx b/frontend/editor/src/portal/components/procurement/ProcurementHome.stories.tsx deleted file mode 100644 index ae424922ad..0000000000 --- a/frontend/editor/src/portal/components/procurement/ProcurementHome.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ProcurementHome } from "@portal/components/procurement/ProcurementHome"; - -/** - * The end-to-end procurement experience (Home hero + takeover modal), driven by the `procurementSaas` - * MSW handlers: start trial → build quote → generate (issue Stripe Quote) → milestone (download PDF / - * accept). `autoOpen` opens the modal so the flow is immediately clickable. - */ -const meta: Meta = { - title: "Portal/Procurement/ProcurementHome", - component: ProcurementHome, - parameters: { layout: "fullscreen" }, -}; -export default meta; - -type Story = StoryObj; - -export const Default: Story = { args: { autoOpen: true } }; diff --git a/frontend/editor/src/portal/components/procurement/ProcurementHome.tsx b/frontend/editor/src/portal/components/procurement/ProcurementHome.tsx deleted file mode 100644 index 1877e0bc3c..0000000000 --- a/frontend/editor/src/portal/components/procurement/ProcurementHome.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ProcurementBanner } from "@portal/components/procurement/ProcurementBanner"; -import { ProcurementFlow } from "@portal/components/procurement/ProcurementFlow"; -import { useProcurement } from "@portal/components/procurement/useProcurement"; -import "@portal/views/Procurement.css"; - -/** - * The standalone procurement experience: a deal-status hero (or enterprise - * on-ramp when no deal exists) above the full-screen takeover flow that holds - * the journey — build + issue a quote, review + agree to the enterprise - * agreement, then accept into a committed subscription. Rendered at - * /procurement (autoOpen). On Home the deal-status hero instead attaches to the - * tier hero card's footer (see HomeHero) so this component isn't used there. - */ -export function ProcurementHome({ autoOpen = false }: { autoOpen?: boolean }) { - const controller = useProcurement(autoOpen); - return ( - <> - - - - ); -} diff --git a/frontend/editor/src/portal/components/procurement/ProcurementModal.stories.tsx b/frontend/editor/src/portal/components/procurement/ProcurementModal.stories.tsx index 7fb8b865f2..14934905d6 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementModal.stories.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementModal.stories.tsx @@ -31,9 +31,7 @@ export const Open: Story = { contract and go live.

    - +
    diff --git a/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx b/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx index 852c4e68c4..881bb778ce 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx @@ -1,104 +1,49 @@ -import { useEffect, useRef } from "react"; -import { createPortal } from "react-dom"; -import { useTranslation } from "react-i18next"; +import type { ReactNode } from "react"; +import { FlowModal } from "@portal/components/shared/FlowModal"; import "@portal/views/Procurement.css"; -/** Keep keyboard focus inside an open dialog: focus it on open and wrap Tab at the edges. */ -export function useFocusTrap(open: boolean) { - const ref = useRef(null); - useEffect(() => { - if (!open) return; - const panel = ref.current; - if (!panel) return; - const prev = document.activeElement as HTMLElement | null; - const focusables = () => - Array.from( - panel.querySelectorAll( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', - ), - ).filter((el) => !el.hasAttribute("disabled")); - (focusables()[0] ?? panel).focus(); - const onKey = (e: KeyboardEvent) => { - if (e.key !== "Tab") return; - const items = focusables(); - if (items.length === 0) return; - const first = items[0]; - const last = items[items.length - 1]; - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - }; - panel.addEventListener("keydown", onKey); - return () => { - panel.removeEventListener("keydown", onKey); - prev?.focus?.(); - }; - }, [open]); - return ref; -} - /** - * Full-screen takeover modal for the procurement flow, copying the prototype's modal design - * (portaled to body, dimmed + blurred backdrop, rounded panel, close button). The Home deal-status - * hero expands into this. + * The procurement takeover: the shared {@link FlowModal} at takeover width. Chrome and copy only — + * the shell (portal, focus trap, Escape, close, header/body bands) is shared, so this dialog cannot + * drift from the trial and licence dialogs the way two hand-rolled shells did. */ export function ProcurementModal({ open, onClose, title, subtitle, + headerless = false, children, }: { open: boolean; onClose: () => void; + /** Dialog label. Omit `subtitle` (and pass `headerless`) when the step renders its own heading. */ title: string; subtitle?: string; - children: React.ReactNode; + /** + * Skip the title block, which takes the shell's close with it: the step inside supplies the + * heading, step badge and its own close (see StepModalHeader), so the shell would otherwise stack + * a second header and leave a stray close above it. Escape and the backdrop still dismiss. + */ + headerless?: boolean; + children: ReactNode; }) { - const { t } = useTranslation(); - const trapRef = useFocusTrap(open); - - useEffect(() => { - if (!open) return; - const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [open, onClose]); - - if (!open) return null; - - return createPortal( -
    e.target === e.currentTarget && onClose()} + return ( + +

    {title}

    + {subtitle &&

    {subtitle}

    } + + ) + } > -
    - -
    -

    {title}

    - {subtitle &&

    {subtitle}

    } -
    -
    {children}
    -
    -
    , - document.body, + {children} + ); } diff --git a/frontend/editor/src/portal/components/procurement/ProcurementStages.tsx b/frontend/editor/src/portal/components/procurement/ProcurementStages.tsx index 35460a7ba5..fcf926f481 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementStages.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementStages.tsx @@ -1,61 +1,92 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Card } from "@app/ui"; +import { Button } from "@app/ui"; +import { openApiUrl } from "@portal/api/externalUrl"; import "@portal/views/Procurement.css"; /** - * The stage-specific cards shown inside the procurement takeover modal once a quote exists: the - * issued-quote milestone, the subscription-created payment step, and the live confirmation. Each is - * a pure presentational view driven by props; ProcurementHome owns the state and the actions. + * The stage-specific views shown inside the procurement takeover modal once the agreement is signed: + * the subscription-created payment step and the live confirmation. Each is a pure presentational view + * driven by props; the controller owns the state and the actions. + * + * Neither is wrapped in a Card: the dialog is already the surface, and a card inside it drew a second + * border around content that filled it. Both wear the same eyebrow/title/description stack and put + * their actions in the flow's footer bar, so the last two steps of the journey read like the ones + * before them rather than like panels that wandered in. */ -/** The subscription-created step: pay or download the first invoice. */ +/** The subscription-created step: pay or download the first invoice, and the signed agreement. */ export function PaymentStageCard({ invoiceUrl, invoicePdf, + signedAgreementVersion, + downloadingAgreement, + onDownloadSignedAgreement, }: { invoiceUrl?: string | null; invoicePdf?: string | null; + /** Version label of the signed agreement PDF, if one is available to download. */ + signedAgreementVersion?: string | null; + downloadingAgreement?: boolean; + onDownloadSignedAgreement?: () => void; }) { const { t } = useTranslation(); return ( - +
    + + {t("portal.procurement.payment.eyebrow")} +

    {t("portal.procurement.payment.title")}

    {t("portal.procurement.payment.description")}

    - {(invoiceUrl || invoicePdf) && ( -
    - {invoiceUrl && ( - - )} - {invoicePdf && ( - - )} + {(invoiceUrl || invoicePdf || signedAgreementVersion) && ( +
    +
    + {signedAgreementVersion && onDownloadSignedAgreement && ( + + )} + {invoicePdf && ( + + )} + {invoiceUrl && ( + + )} +
    )} - +
    ); } /** The live confirmation once the deal is active. */ -export function LiveStageCard() { +export function LiveStageCard({ + signedAgreementVersion, + downloadingAgreement, + onDownloadSignedAgreement, +}: { + signedAgreementVersion?: string | null; + downloadingAgreement?: boolean; + onDownloadSignedAgreement?: () => void; +} = {}) { const { t } = useTranslation(); return ( - +
    {t("portal.procurement.live.eyebrow")} @@ -65,7 +96,20 @@ export function LiveStageCard() {

    {t("portal.procurement.live.description")}

    - + {signedAgreementVersion && onDownloadSignedAgreement && ( +
    +
    + +
    +
    + )} +
    ); } diff --git a/frontend/editor/src/portal/components/procurement/QuoteBuilder.tsx b/frontend/editor/src/portal/components/procurement/QuoteBuilder.tsx index b392247563..98fcbd4f04 100644 --- a/frontend/editor/src/portal/components/procurement/QuoteBuilder.tsx +++ b/frontend/editor/src/portal/components/procurement/QuoteBuilder.tsx @@ -3,18 +3,24 @@ import { useTranslation } from "react-i18next"; import { Button } from "@app/ui"; import { DocumentsIcon, + DownloadIcon, PoliciesIcon, UsersIcon, } from "@portal/components/icons"; import { money } from "@portal/components/procurement/format"; import { buildQuote, + recordLegalConsent, type QuoteConfigInput, type QuoteResult, } from "@portal/api/procurement"; +import { LegalDocumentModal } from "@portal/components/procurement/ProcurementExtras"; +import { StepModalHeader } from "@portal/components/shared/StepModalHeader"; import "@portal/views/Procurement.css"; -const STEPS = ["volume", "plan", "details"] as const; +const STEPS = ["volume", "plan", "details", "review"] as const; +const DETAILS_STEP = 2; +const REVIEW_STEP = 3; const TERM_DISCOUNT = [0, 0.03, 0.05, 0.06, 0.07]; // 1..5 years — meter-only discount (D71) // Governance posture: the intensity (runs per PDF) fed to the committed-volume curve. const POSTURES = [ @@ -30,22 +36,57 @@ const SIZE_TIERS = [ ] as const; /** - * The enterprise quote builder — volume → commitment & service → details. A client-side preview - * drives the live footer total; the backend is authoritative. Completing the form generates the - * quote directly (build + issue in one step) — the issued quote is then shown as the milestone, so - * there's no redundant in-builder preview. + * The enterprise quote builder — volume → commitment & service → details → review. A client-side + * preview drives the live footer total; the backend is authoritative. Generating builds and issues in + * one go, and the issued quote comes back as the fourth step: the buyer reads the real itemised paper + * and can download it, but does not accept here. Accepting is a decision taken from the deal card, + * deliberately, so circulating the quote internally is not a dead end in a modal. */ export function QuoteBuilder({ deployment, seats = 0, + email, + onClose, + dealDetails, initial, + eulaAlreadyAgreed = false, onGenerate, + issued, + downloading = false, + onDownload, }: { deployment: string; /** Seat count from the trial setup; seeds the users field + volume estimate on a fresh quote. */ seats?: number; + /** Linked-account email; prefills the contact email on a fresh quote's details step. */ + email?: string | null; + /** Dismiss the dialog. The builder draws its own header, so it carries the close too. */ + onClose?: () => void; + /** + * The buying entity captured at trial setup. Seeds a fresh quote's details step so it confirms + * what is already known rather than asking twice; the buyer can still correct it here, since a + * deal can change hands between trial and quote. + */ + dealDetails?: { + businessName?: string | null; + contactName?: string | null; + contactEmail?: string | null; + }; /** Seed the builder from an existing quote's config (re-editing a quote). */ initial?: QuoteConfigInput; + /** + * The issued quote, which is what the review step shows. Its arrival is also what opens that step: + * the parent issues the quote and it lands by snapshot refresh, so there is no synchronous result + * to advance on. Null while re-editing, so editing reopens the form rather than the paper. + */ + issued?: QuoteResult | null; + downloading?: boolean; + onDownload?: () => void; + /** + * The buyer already accepted the EULA (e.g. at trial start). When true, the EULA clickwrap is + * hidden here and no consent is recorded at quote time — it's only collected once. + */ + eulaAlreadyAgreed?: boolean; /** Called with the priced DRAFT quote; the parent issues it as a Stripe Quote. */ onGenerate: (quote: QuoteResult) => void; }) { @@ -65,9 +106,9 @@ export function QuoteBuilder({ indemnification: false, training: false, qbr: false, - businessName: "", - contactName: "", - contactEmail: "", + businessName: dealDetails?.businessName ?? "", + contactName: dealDetails?.contactName ?? "", + contactEmail: dealDetails?.contactEmail ?? email ?? "", addressLine1: "", addressLine2: "", city: "", @@ -79,29 +120,81 @@ export function QuoteBuilder({ ); // A seeded quote carries a volume but no user count, so treat it as manually set. const [manualVolume, setManualVolume] = useState(initial != null); - const [eula, setEula] = useState(initial != null); + // Never pre-ticked, even when re-editing a quote: a consent the buyer did not tick in this session + // is not a consent, and recordLegalConsent would have logged one as though they had. + const [eula, setEula] = useState(false); + const [legalDoc, setLegalDoc] = useState(null); const [busy, setBusy] = useState(false); + // Only surface field errors once the buyer tries to generate — no red fields on first sight. + const [showErrors, setShowErrors] = useState(false); function set(k: K, v: QuoteConfigInput[K]) { setCfg((c) => ({ ...c, [k]: v })); } - // Re-editing an existing quote: everything is seeded, so jump to the last step (details) with the + // Required buyer details before a quote can be generated (Order Form / invoice need these). + const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( + (cfg.contactEmail ?? "").trim(), + ); + const valid = { + businessName: cfg.businessName.trim().length > 0, + contactName: (cfg.contactName ?? "").trim().length > 0, + contactEmail: emailOk, + addressLine1: (cfg.addressLine1 ?? "").trim().length > 0, + city: (cfg.city ?? "").trim().length > 0, + region: (cfg.region ?? "").trim().length > 0, + postalCode: (cfg.postalCode ?? "").trim().length > 0, + }; + const detailsValid = Object.values(valid).every(Boolean); + const eulaOk = eulaAlreadyAgreed || eula; + const canGenerate = detailsValid && eulaOk; + + // Re-editing an existing quote: everything is seeded, so jump to the details step with the // agreement pre-accepted — one click re-generates, or Back to change a field. No walking from step 1. // Mount-only: seed the step from `initial` once (deliberately no deps). useEffect(() => { - if (initial) setStep(STEPS.length - 1); + if (initial) setStep(DETAILS_STEP); }, []); + // Issuing lands by snapshot refresh rather than as a return value, so the arrival of the issued + // quote is what opens the review step. Keyed on the quote's id, not the object: React Query hands + // back a fresh object on every refetch, which would yank a buyer who had walked Back to the form. + useEffect(() => { + if (issued) setStep(REVIEW_STEP); + }, [issued?.quoteId]); + const preview = previewAnnualMinor(cfg); const tcvPreview = preview * cfg.termYears + (cfg.training ? 750_000 : 0); + // On the review step the footer quotes the issued figures rather than the client-side preview, so + // the running total never disagrees with the paper directly above it. + const onPaper = issued != null && step === REVIEW_STEP; + const running = onPaper + ? { + annual: money(issued.annualNetMinor, issued.currency), + years: issued.config.termYears, + tcv: money(issued.tcvMinor, issued.currency), + } + : { + annual: money(preview), + years: cfg.termYears, + tcv: money(tcvPreview), + }; + // Fully filled → price + hand the draft to the parent to issue as a Stripe Quote (which then shows // as the milestone). No separate in-builder preview step. async function generate() { + if (!canGenerate) { + setShowErrors(true); + return; + } setBusy(true); try { - onGenerate(await buildQuote(cfg)); + const quote = await buildQuote(cfg); + // Record the EULA clickwrap only when it's collected here — i.e. the buyer didn't already + // accept it at trial start. Best-effort. + if (!eulaAlreadyAgreed) void recordLegalConsent("eula", "quote"); + onGenerate(quote); } finally { setBusy(false); } @@ -109,22 +202,16 @@ export function QuoteBuilder({ return (
    -
    -

    - {t("portal.procurement.builder.title")} -

    - - {t("portal.procurement.builder.stepOf", { - n: step + 1, - total: STEPS.length, - })} - -
    -
    - {STEPS.map((s, i) => ( - - ))} -
    +
    {step === 0 && ( @@ -171,15 +258,6 @@ export function QuoteBuilder({ ? t("portal.procurement.builder.volManual") : t("portal.procurement.builder.volNoUsers")}

    - - )} - - {step === 1 && ( - } - title={t("portal.procurement.builder.s2Title")} - sub={t("portal.procurement.builder.s2Sub")} - >
    {POSTURES.map((p) => ( @@ -209,7 +287,15 @@ export function QuoteBuilder({ ))}
    +
    + )} + {step === 1 && ( + } + title={t("portal.procurement.builder.s2Title")} + sub={t("portal.procurement.builder.s2Sub")} + >
    {[1, 2, 3, 4, 5].map((y) => ( @@ -287,7 +373,11 @@ export function QuoteBuilder({ sub={t("portal.procurement.builder.s3Sub")} >
    - + set("businessName", e.target.value)} /> - +
    - + set("contactEmail", e.target.value)} /> - +
    - + set("city", e.target.value)} /> - + set("region", e.target.value)} /> - +
    - + {!eulaAlreadyAgreed && ( + + )} + {showErrors && !canGenerate && ( +

    + {t("portal.procurement.builder.completeRequired")} +

    + )} )} + + {/* No step heading here, unlike the form steps: the quote is the content, and a heading over + it only repeats what the paper already says. The real issued figures, not the footer's + client-side preview — this is the document the buyer circulates, so it has to match the + PDF and the Stripe quote exactly. */} + {step === REVIEW_STEP && issued && ( +
    +
    +
    +
    +
    Stirling PDF
    +
    + {t("portal.procurement.builder.paperEyebrow")} +
    +
    +
    +
    + {issued.quoteNumber} +
    + {issued.validUntil && ( +
    + {t("portal.procurement.review.validUntil", { + date: new Date(issued.validUntil).toLocaleDateString(), + })} +
    + )} + {/* On the document rather than in the footer: it downloads this paper, so it + belongs to it, and the footer stays the flow's own Back/Done. */} + +
    +
    + + {issued.config.businessName?.trim() && ( +
    +
    + {t("portal.procurement.builder.paperFor")} +
    +
    + {issued.config.businessName} +
    +
    + )} + +
      + {issued.lineItems.map((li) => ( +
    • + {li.label} + {money(li.amountMinor, issued.currency)} +
    • + ))} +
    + +
    +
    +
    + {t("portal.procurement.review.annual")} +
    +
    + {t("portal.procurement.review.tcv", { + years: issued.config.termYears, + tcv: money(issued.tcvMinor, issued.currency), + })} +
    +
    + {t("portal.procurement.review.renewal", { + amount: money( + issued.renewalAnnualNetMinor, + issued.currency, + ), + pct: issued.cpiRatePct, + })} +
    + {issued.config.poNumber?.trim() && ( +
    + {t("portal.procurement.review.poNumber", { + po: issued.config.poNumber.trim(), + })} +
    + )} +
    +
    + {money(issued.annualNetMinor, issued.currency)} +
    +
    +
    +
    + )}
    - {t("portal.procurement.builder.running", { - annual: money(preview), - years: cfg.termYears, - tcv: money(tcvPreview), - })} + {t("portal.procurement.builder.running", running)}
    {step > 0 && ( @@ -408,7 +630,6 @@ export function QuoteBuilder({ {step === 0 && ( )} {step === 1 && ( - )} - {step === 2 && ( - )} + {/* No Accept here: the review step ends on the deal card, where accepting is one of two + deliberate choices rather than the only way out of a modal. Download lives on the + document itself. */} + {step === REVIEW_STEP && ( + + )}
    + setLegalDoc(null)} />
    ); } @@ -470,14 +690,25 @@ function Step({ function Field({ label, + required, + invalid, children, }: { label: string; + required?: boolean; + invalid?: boolean; children: React.ReactNode; }) { return ( -
    ); diff --git a/frontend/editor/src/portal/views/Procurement.css b/frontend/editor/src/portal/views/Procurement.css index e26fd74a31..b721768952 100644 --- a/frontend/editor/src/portal/views/Procurement.css +++ b/frontend/editor/src/portal/views/Procurement.css @@ -1,34 +1,10 @@ -.portal-proc { - display: flex; - flex-direction: column; - gap: 1.25rem; - padding: 1.5rem; - max-width: 84rem; - margin: 0 auto; -} - -/* Page header */ -.portal-proc__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; -} - -.portal-proc__title { - margin: 0; - font-size: 1.375rem; - font-weight: 600; - color: var(--c-text); -} - .portal-proc__subtitle { margin: 0.25rem 0 0; font-size: 0.8125rem; color: var(--c-text-subtle); } -/* Eyebrow label shared by the journey header + SE block */ +/* Eyebrow label above a stage panel's heading */ .portal-proc__eyebrow { display: block; font-size: 0.6875rem; @@ -38,613 +14,33 @@ color: var(--c-text-subtle); margin-bottom: 0.25rem; } - -/* ── Journey card ─────────────────────────────────────────────────────── */ -/* Stacked, border-divided sections: header / stepper / trial / next step. */ -.portal-proc__journey-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - padding: 1.25rem 1.5rem; - border-bottom: 1px solid var(--c-border-subtle); - flex-wrap: wrap; -} - -.portal-proc__journey-title { - margin: 0; - font-size: 1.0625rem; - font-weight: 700; - color: var(--c-text); -} - -.portal-proc__journey-sub { - margin: 0.25rem 0 0; - max-width: 36rem; - font-size: 0.8125rem; - line-height: 1.5; - color: var(--c-text-subtle); -} - -.portal-proc__se { - display: flex; - flex-direction: column; - text-align: right; - flex-shrink: 0; -} - -.portal-proc__se .portal-proc__eyebrow { - margin-bottom: 0.25rem; -} - -.portal-proc__se-name { - font-size: 0.8125rem; - font-weight: 600; - color: var(--c-text); -} - -.portal-proc__se-email { - font-size: 0.75rem; - color: var(--c-primary); - text-decoration: none; -} - -.portal-proc__se-email:hover { - text-decoration: underline; -} - -/* Stepper band */ -.portal-proc__journey-stepper { - padding: 1.125rem 1.5rem; - border-bottom: 1px solid var(--c-border-subtle); -} - -.portal-proc__steps { - display: flex; - align-items: flex-start; -} - -.portal-proc__steps--locked { - opacity: 0.55; - filter: grayscale(0.4); - pointer-events: none; -} - -.portal-proc__step { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.4rem; - min-width: 4rem; -} - -.portal-proc__step-dot { - width: 0.875rem; - height: 0.875rem; - border-radius: 50%; - background: var(--c-border); -} - -.portal-proc__step--complete .portal-proc__step-dot { - background: var(--color-green); -} - -.portal-proc__step--current .portal-proc__step-dot { - background: var(--color-purple); - box-shadow: 0 0 0 4px var(--color-purple-light); -} - -.portal-proc__step-label { - font-size: 0.6875rem; - font-weight: 500; - text-align: center; - white-space: nowrap; - color: var(--c-text-subtle); -} - -.portal-proc__step--complete .portal-proc__step-label { - color: var(--c-text-subtle); -} - -.portal-proc__step--current .portal-proc__step-label { - font-weight: 700; - color: var(--c-text); -} - -/* Connector aligns with the 0.875rem dots: (14px − 2px) / 2 = 6px down */ -.portal-proc__step-line { - flex: 1; - height: 2px; - margin: 0.375rem 0.375rem 0; - background: var(--c-border-subtle); -} - -.portal-proc__step-line[data-filled="true"] { - background: var(--color-green); -} - -/* Trial status strip (shown while evaluating) */ -.portal-proc__trial { - display: flex; - align-items: center; - gap: 0.625rem; - flex-wrap: wrap; - padding: 0.75rem 1.5rem; - border-bottom: 1px solid var(--c-border-subtle); - background: var(--color-bg-code); -} - -.portal-proc__trial-title { - font-size: 0.8125rem; - font-weight: 600; - color: var(--c-text); -} - -.portal-proc__trial-dim { - font-size: 0.78125rem; - color: var(--c-text-subtle); -} - -.portal-proc__trial-key { - font-family: - ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; - font-size: 0.6875rem; - color: var(--c-text-subtle); - background: var(--c-surface); - border: 1px solid var(--c-border); - border-radius: 0.375rem; - padding: 0.1875rem 0.5rem; -} - -/* Next-step row: one primary action at a time */ -.portal-proc__next { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; - padding: 1rem 1.5rem; -} - -.portal-proc__next-label { - display: flex; - align-items: center; - gap: 0.625rem; - font-size: 0.84375rem; - font-weight: 600; - color: var(--c-text); -} - -.portal-proc__next-dot { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--color-amber); - flex-shrink: 0; -} - -.portal-proc__next-dot[data-live="true"] { - background: var(--color-green); -} - -/* ── Documents card ───────────────────────────────────────────────────── */ -.portal-proc__docs-head { - padding: 1.125rem 1.5rem; - border-bottom: 1px solid var(--c-border-subtle); -} - -.portal-proc__docs-title { - margin: 0; - font-size: 0.9375rem; - font-weight: 700; - color: var(--c-text); -} - -.portal-proc__docs-sub { - margin: 0.25rem 0 0; - font-size: 0.78125rem; - color: var(--c-text-subtle); -} - -.portal-proc__docs-body { - padding: 0.375rem 1.5rem 1.125rem; -} - -/* Accordion spacing — the disclosure chrome itself lives in shared Collapsible - (.sui-collapsible); here we only space the stacked sections. */ -.portal-proc__docs-body .sui-collapsible { - margin-top: 0.875rem; -} - -/* Stage header bits */ -.portal-proc__stage-dot { - width: 0.4375rem; - height: 0.4375rem; - border-radius: 50%; - flex-shrink: 0; -} - -.portal-proc__stage-dot[data-state="done"] { - background: var(--color-green); -} - -.portal-proc__stage-dot[data-state="current"] { - background: var(--color-purple); -} - -.portal-proc__stage-dot[data-state="upcoming"] { - background: var(--c-border); -} - -.portal-proc__stage-label { - font-size: 0.75rem; - font-weight: 700; - letter-spacing: 0.03em; - text-transform: uppercase; - color: var(--c-text-subtle); -} - -.portal-proc__stage-label[data-current] { - color: var(--c-text); -} - -.portal-proc__stage-hint { - font-size: 0.71875rem; - color: var(--c-text-subtle); -} - -.portal-proc__stage-count { - font-size: 0.6875rem; - color: var(--c-text-subtle); -} - -/* Document lists inside the accordion */ -.portal-proc__doc-list { - border-top: 1px solid var(--c-border-subtle); -} - -.portal-proc__doc-list--boxed { - border: 1px solid var(--c-border-subtle); - border-radius: 0.5rem; - overflow: hidden; -} - -/* Supporting section — extra separation from the stage accordion above it. - Scoped to match the general .sui-collapsible spacing rule's specificity. */ -.portal-proc__docs-body .portal-proc__supporting-acc { - margin-top: 1.5rem; -} - -.portal-proc__supporting-head { - display: flex; - flex-direction: column; - gap: 0.125rem; - min-width: 0; -} - -.portal-proc__supporting-sub { - font-size: 0.71875rem; - font-weight: 400; - line-height: 1.45; - color: var(--c-text-subtle); -} - -.portal-proc__acc-toggle-label { - font-size: 0.75rem; - font-weight: 600; - color: var(--c-primary); -} - -.portal-proc__supporting-groups { - display: flex; - flex-direction: column; - gap: 1rem; - border-top: 1px solid var(--c-border-subtle); - padding: 0.875rem; -} - -.portal-proc__group-label { - font-size: 0.6875rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--c-text-subtle); - margin-bottom: 0.5rem; -} - -/* ── Document rows (ledger + supporting) ──────────────────────────────── */ -.portal-proc__doc { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.75rem 0.875rem; - border-bottom: 1px solid var(--c-border-subtle); -} - -.portal-proc__doc:last-child { - border-bottom: none; -} - -.portal-proc__doc[data-locked] { - opacity: 0.6; -} - -.portal-proc__doc-text { - min-width: 0; -} - -.portal-proc__doc-name-row { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.portal-proc__doc-name { - font-size: 0.84375rem; - font-weight: 600; - color: var(--c-text); -} - -.portal-proc__doc-sub { - margin: 0.0625rem 0 0; - font-size: 0.71875rem; - line-height: 1.4; - color: var(--c-text-subtle); -} - -.portal-proc__doc-actions { - display: flex; - align-items: center; - gap: 0.75rem; - flex-shrink: 0; -} - -/* ── Locked state ─────────────────────────────────────────────────────── */ -.portal-proc__locked { - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* ── Action modal ─────────────────────────────────────────────────────── */ -.portal-proc__modal-body { - margin: 0 0 1rem; - font-size: 0.875rem; - line-height: 1.55; - color: var(--c-text-muted); -} - -.portal-proc__modal-actions { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 0.625rem; -} - -.portal-proc__upload { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.portal-proc__upload-input { - display: none; -} - -.portal-proc__upload-name { - font-size: 0.75rem; - color: var(--c-text-subtle); -} - -@media (max-width: 48rem) { - .portal-proc__journey-head { - flex-direction: column; - } - - .portal-proc__se { - text-align: left; - } - - .portal-proc__steps { - overflow-x: auto; - } -} - -/* ── Enterprise upsell CTA (Home / Usage on-ramp) ─────────────────────────── */ -.portal-proc__upsell { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; -} -.portal-proc__upsell-badge { - display: inline-block; - font-size: 0.625rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--c-primary); - background: var(--c-primary-subtle); - padding: 0.15rem 0.5rem; - border-radius: 0.375rem; - margin-bottom: 0.4rem; -} -.portal-proc__upsell-copy { - margin: 0; - font-size: 0.8125rem; - line-height: 1.45; - color: var(--c-text-subtle); - max-width: 44rem; -} -.portal-proc__upsell-copy strong { - color: var(--c-text); -} - -/* ── Quote builder ────────────────────────────────────────────────────────── */ -.portal-proc__builder-head { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: 1rem; -} .portal-proc__builder-title { margin: 0; font-size: 1rem; font-weight: 650; color: var(--c-text); } -.portal-proc__builder-step { - font-size: 0.75rem; - color: var(--c-text-subtle); -} -.portal-proc__builder-body { - display: flex; - flex-direction: column; - gap: 0.85rem; -} -.portal-proc__field { - display: flex; - flex-direction: column; - gap: 0.3rem; - font-size: 0.8125rem; - color: var(--c-text-subtle); -} -.portal-proc__field input, -.portal-proc__field select { - padding: 0.45rem 0.6rem; - border: 1px solid var(--c-border); - border-radius: 0.5rem; - font-size: 0.875rem; - background: var(--c-input-bg); - color: var(--c-text); -} -.portal-proc__builder-addons { - display: flex; - flex-direction: column; - gap: 0.4rem; - font-size: 0.8125rem; - color: var(--c-text-muted); -} -.portal-proc__builder-addons label { - display: flex; - align-items: center; - gap: 0.5rem; -} -.portal-proc__builder-actions { - display: flex; - justify-content: flex-end; - gap: 0.6rem; - margin-top: 0.5rem; -} -.portal-proc__quote-head { - display: flex; - align-items: baseline; - justify-content: space-between; - border-bottom: 1px solid var(--c-border); - padding-bottom: 0.5rem; -} -.portal-proc__quote-number { - font-weight: 650; - color: var(--c-text); -} -.portal-proc__quote-valid { - font-size: 0.75rem; - color: var(--c-text-subtle); -} -.portal-proc__quote-lines { - list-style: none; - margin: 0; - padding: 0; -} -.portal-proc__quote-lines li { - display: flex; - justify-content: space-between; - padding: 0.4rem 0; - font-size: 0.8125rem; - color: var(--c-text-muted); - border-bottom: 1px solid var(--c-border-subtle); -} -.portal-proc__quote-lines li[data-kind="DISCOUNT"] { - color: var(--c-success); -} -.portal-proc__quote-total { - display: flex; - justify-content: space-between; - align-items: baseline; - padding: 0.6rem 0 0.2rem; - font-size: 0.9375rem; -} -.portal-proc__quote-total strong { - font-size: 1.25rem; - color: var(--c-text); -} -.portal-proc__quote-tcv { - font-size: 0.75rem; - color: var(--c-text-subtle); -} /* ══ Quote builder — copied from the marketing prototype (tight density) ═════ */ .portal-qb { - background: var(--c-surface); - border: 1px solid var(--c-border); - border-radius: 14px; - box-shadow: inset 0 0 0 1px var(--c-border-subtle); - overflow: hidden; -} -.portal-qb__head { display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 24px 14px; - border-bottom: 1px solid var(--c-border-subtle); - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--c-primary) 5.5%, transparent) 0%, - transparent 100% - ); -} -.portal-qb__title { - margin: 0; - font-size: 16px; - font-weight: 700; - color: var(--c-text); -} -.portal-qb__stepchip { - font-size: 11px; - font-weight: 700; - color: var(--c-text-subtle); - background: var(--c-surface-sunken); - padding: 3px 10px; - border-radius: 999px; -} -.portal-qb__progress { - display: flex; - gap: 6px; - padding: 12px 24px 0; -} -.portal-qb__progress span { - flex: 1; - height: 6px; - border-radius: 999px; - background: var(--c-surface-sunken); - transition: background 0.3s; -} -.portal-qb__progress span[data-on] { - background: var(--c-primary); + flex-direction: column; } +/* No padding or scroll of its own: FlowModal's panel supplies both, and nesting a second scroll + container inside a scrolling panel gave the builder two scrollbars. */ +/* This gap is the builder's only vertical rhythm: the blocks inside carry no bottom margins of their + own, so spacing cannot double up the way a margin plus a gap did. The top margin is the breathing + room under the stepped header, which deliberately has no bottom margin so its host sets this. */ .portal-qb__body { - padding: 20px 24px; - max-height: 56vh; - overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 1.05rem; } .portal-qb__intro { display: flex; align-items: center; gap: 12px; - margin-bottom: 18px; } .portal-qb__intro-icon { width: 38px; @@ -667,9 +63,10 @@ color: var(--c-text-subtle); margin-top: 1px; } +/* No bottom margin: inside .portal-qb__body the gap spaces these, and inside .portal-qb__row a + margin only reserved dead space under the inputs. */ .portal-qb__field { display: block; - margin-bottom: 18px; } .portal-qb__field-label { display: block; @@ -691,6 +88,18 @@ background: var(--c-input-bg); color: var(--c-text); } +.portal-qb__req { + color: var(--c-danger); +} +.portal-qb__field[data-invalid] input, +.portal-qb__field[data-invalid] select { + border-color: var(--c-danger); +} +.portal-qb__error { + margin: 10px 0 0; + font-size: 12px; + color: var(--c-danger); +} .portal-qb__row { display: flex; gap: 14px; @@ -701,7 +110,7 @@ min-width: 190px; } .portal-qb__hint { - margin: 7px 0 0; + margin: 0; font-size: 11.5px; color: var(--c-text-subtle); line-height: 1.4; @@ -736,11 +145,22 @@ gap: 10px; flex-wrap: wrap; } +/* Short-label options that should sit across one row rather than wrapping 2 + 1. Tighter padding + and label size so each caption fits on a single line at three-across. Deliberately no `nowrap`: + a longer translation should wrap rather than clip or push the card out of the row. */ +.portal-qb__opts--across .portal-qb__opt { + min-width: 0; + padding: 11px 12px; +} +.portal-qb__opts--across .portal-qb__opt-sub { + font-size: 11px; + line-height: 1.35; +} .portal-qb__opt { text-align: left; flex: 1; min-width: 150px; - padding: 12px 14px; + padding: 9px 11px; border-radius: 9px; border: 1px solid var(--c-border); background: var(--c-surface); @@ -834,7 +254,11 @@ align-items: center; justify-content: space-between; gap: 12px; - padding: 14px 24px; + /* Bleeds to the panel's edges by reading the shell's own inset, so changing FlowModal's padding + can no longer leave this footer stopping short of them. */ + margin: 0.85rem calc(-1 * var(--flowmodal-inset)) + calc(-1 * var(--flowmodal-body-end)); + padding: 0.8rem var(--flowmodal-inset) 0.9rem; border-top: 1px solid var(--c-border-subtle); flex-wrap: wrap; } @@ -846,13 +270,13 @@ display: flex; gap: 10px; } -/* Step 4 — the itemised quote paper */ +/* Step 4 — the itemised quote paper, on a sunken tray that runs to the panel's edges. No scroll of + its own: the dialog body already scrolls, and nesting a second scroller gave the builder two + scrollbars. The bleed reads the shell's inset rather than hard-coding a copy of it. */ .portal-qb__papertray { background: var(--c-surface-sunken); - padding: 18px; - max-height: 56vh; - overflow-y: auto; - margin: -20px -24px; + padding: 14px var(--flowmodal-inset); + margin: 0 calc(-1 * var(--flowmodal-inset)); } .portal-qb__paper { background: var(--c-surface); @@ -880,6 +304,11 @@ .portal-qb__paper-meta { text-align: right; } +/* Reads as a link on the document rather than a button in a toolbar: right-aligned under the quote's + own metadata, with the row's padding trimmed so it sits tight to the date above it. */ +.portal-qb__paper-download { + margin: 0.2rem -0.5rem -0.25rem 0; +} .portal-qb__quote-number { font-size: 12.5px; font-weight: 700; @@ -955,27 +384,24 @@ margin-top: 3px; } -/* ── Enterprise upsell text wrapper (Home on-ramp) ────────────────────────── */ -.portal-proc__upsell-text { - flex: 1 1 20rem; -} - /* ── Deal-status hero (Home, active deal) ─────────────────────────────────── */ .portal-hero { border: 1px solid var(--c-border); border-radius: 12px; padding: 1.1rem 1.25rem; - background: - radial-gradient( - 120% 140% at 100% 0%, - color-mix(in srgb, var(--c-hue-violet) 8%, transparent), - transparent 55% - ), - var(--c-surface); + background: var(--c-surface); display: flex; flex-direction: column; gap: 1rem; } + +/* Attached under the editor rail the two read as one card, so the hero drops its standalone frame + and lets the footer's top border be the only seam. It keeps the frame on the procurement view, + where it stands alone. */ +.portal-editor-hero__footer .portal-hero { + border: none; + border-radius: 0; +} .portal-hero__top { display: flex; align-items: flex-start; @@ -983,25 +409,56 @@ gap: 1rem; flex-wrap: wrap; } +.portal-hero__ident { + flex: 1; + min-width: 0; +} .portal-hero__eyebrow { display: block; font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; - color: var(--c-primary); + color: var(--c-text-subtle); + margin-bottom: 0.55rem; } -.portal-hero__company { - display: block; - font-size: 1.0625rem; - font-weight: 650; +/* The journey band: one segment per stage, filled through the current one. A progress indicator, + so it lives inside the block whose progress it reports and deliberately does not pulse. */ +.portal-hero__bar { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 0.625rem; +} +.portal-hero__bar span { + flex: 1; + height: 6px; + border-radius: 999px; + background: var(--c-hover); + transition: background 0.3s ease; +} +.portal-hero__bar span[data-on] { + background: var(--c-primary); +} +/* The hero's one-line status: bold stage · what it asks, then Next: … */ +.portal-hero__sentence { + margin: 0; + font-size: 0.84375rem; + line-height: 1.5; + color: var(--c-text-muted); +} +.portal-hero__sentence strong { + font-weight: 700; color: var(--c-text); - margin-top: 0.2rem; +} +.portal-hero__sentence-next { + color: var(--c-text-subtle); } .portal-hero__chips { display: flex; gap: 0.4rem; flex-wrap: wrap; + margin-top: 0.5rem; } .portal-hero__chip { font-size: 0.6875rem; @@ -1011,88 +468,74 @@ border-radius: 999px; padding: 0.2rem 0.6rem; } -.portal-hero__stepper { - overflow-x: auto; -} -.portal-hero__next { +/* One action row: the stage's primary CTA leads, quiet icon actions sit beside it. No dividers. */ +.portal-hero__cta { display: flex; align-items: center; - justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; - padding-top: 0.85rem; - border-top: 1px solid var(--c-border-subtle); } -.portal-hero__next-label { +.portal-hero__icons { + display: flex; + align-items: center; + gap: 0.5rem; +} +.portal-hero__iconbtn { + width: 34px; + height: 34px; display: inline-flex; align-items: center; - gap: 0.45rem; - font-size: 0.8125rem; - font-weight: 600; - color: var(--c-text-muted); -} -.portal-hero__next-dot { - width: 0.5rem; - height: 0.5rem; - border-radius: 999px; - background: var(--c-primary); - box-shadow: 0 0 0 3px var(--c-primary-subtle); -} - -/* ── Full-screen takeover modal (procurement flow) ────────────────────────── */ -.portal-procmodal { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: flex-start; justify-content: center; - padding: clamp(0.5rem, 4vh, 3rem) 1rem; - overflow-y: auto; - background: var(--c-overlay); - backdrop-filter: blur(6px) saturate(160%); - -webkit-backdrop-filter: blur(6px) saturate(160%); - animation: portal-procmodal-fade 0.15s ease-out; -} -@keyframes portal-procmodal-fade { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -.portal-procmodal__panel { - position: relative; - width: 100%; - max-width: 62rem; - background: var(--c-surface); + border-radius: 9px; border: 1px solid var(--c-border); - border-radius: 14px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28); - padding: 1.5rem 1.5rem 1.75rem; -} -.portal-procmodal__close { - position: absolute; - top: 0.85rem; - right: 0.85rem; - border: none; - background: var(--c-border-subtle); - color: var(--c-text-subtle); - width: 1.9rem; - height: 1.9rem; - border-radius: 8px; - font-size: 0.85rem; + background: var(--c-surface); + color: var(--c-text-muted); cursor: pointer; + transition: + background 0.15s ease, + border-color 0.15s ease, + color 0.15s ease; } -.portal-procmodal__close:hover { - background: var(--c-border); +.portal-hero__iconbtn:hover { + background: var(--c-hover); + border-color: var(--c-border-strong); color: var(--c-text); } -.portal-procmodal__header { - margin-bottom: 1.25rem; - padding-right: 2.5rem; +/* Terminal state: the deal is done, so the row reports rather than asks. */ +.portal-hero__live { + display: flex; + align-items: center; + gap: 0.75rem; } +.portal-hero__live-tile { + width: 40px; + height: 40px; + flex-shrink: 0; + border-radius: 11px; + display: flex; + align-items: center; + justify-content: center; + /* Holds a CheckIcon, which takes its stroke from `color` and its size from its own prop. */ + color: var(--c-success); + background: var(--c-success-subtle); +} +.portal-hero__live-text { + display: flex; + flex-direction: column; + min-width: 0; +} +.portal-hero__live-title { + font-size: 0.90625rem; + font-weight: 700; + color: var(--c-text); +} +.portal-hero__live-sub { + font-size: 0.78125rem; + color: var(--c-text-subtle); + margin-top: 1px; +} + +/* ── Procurement takeover: heading type only, the shell is the shared Modal ── */ .portal-procmodal__title { margin: 0; font-size: 1.35rem; @@ -1104,15 +547,6 @@ font-size: 0.875rem; color: var(--c-text-subtle); } -.portal-procmodal__body { - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -.portal-proc__modal-stepper { - overflow-x: auto; -} .portal-proc__payment-actions { display: flex; gap: 0.6rem; @@ -1124,7 +558,7 @@ padding: 1rem; border: 1px solid var(--c-border); border-radius: 0.6rem; - background: var(--color-surface-2, rgba(0, 0, 0, 0.02)); + background: var(--c-surface-sunken); } .portal-proc__license-label { display: block; @@ -1139,7 +573,7 @@ margin-top: 0.4rem; padding: 0.55rem 0.7rem; border-radius: 0.4rem; - background: var(--color-surface-3, rgba(0, 0, 0, 0.05)); + background: var(--c-surface-raised); font-family: var(--font-mono, monospace); font-size: 0.85rem; word-break: break-all; @@ -1150,45 +584,9 @@ font-size: 0.75rem; color: var(--c-text-subtle, var(--c-text-muted)); } -.portal-proc__milestone-for { - margin: 0.15rem 0 0; - font-size: 0.8125rem; - font-weight: 600; - color: var(--c-text-muted); -} -.portal-proc__milestone-lines { - margin: 0.85rem 0 0.5rem; -} -.portal-proc__milestone-totals { - display: flex; - align-items: baseline; - gap: 1rem; - flex-wrap: wrap; - margin: 0.75rem 0 0.25rem; -} -.portal-proc__milestone-annual { - font-size: 1.75rem; - font-weight: 700; - color: var(--c-text); -} -.portal-proc__milestone-annual small { - font-size: 0.8125rem; - font-weight: 500; - color: var(--c-text-subtle); -} -.portal-proc__milestone-tcv { - font-size: 0.8125rem; - color: var(--c-text-subtle); -} /* Hero next-step action row (primary CTA + optional extend-trial). */ -.portal-hero__next-actions { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -/* Hero quick-action chips (clickable pills next to the company name). */ +/* Hero quick-action chips (the trial countdown pill under the stage sentence). */ .portal-hero__chip--action { border: 1px solid var(--c-border); cursor: pointer; @@ -1203,101 +601,8 @@ transform: translateY(-1px); } -/* Hero rollout checklist (trial): the "do this now" setup steps. */ -.portal-hero__checklist { - list-style: none; - margin: 0; - padding: 0; - border-top: 1px solid var(--c-border-subtle); -} -.portal-hero__checklist li { - border-bottom: 1px solid var(--c-border-subtle); -} -.portal-hero__checklist button { - display: flex; - align-items: center; - gap: 0.85rem; - width: 100%; - padding: 0.7rem 0.25rem; - background: none; - border: none; - cursor: pointer; - text-align: left; -} -.portal-hero__checklist button:hover { - background: var(--c-hover, var(--c-border-subtle)); -} -.portal-hero__check-dot { - width: 0.5rem; - height: 0.5rem; - border-radius: 999px; - background: var(--c-border); - flex-shrink: 0; -} -.portal-hero__check-text { - flex: 1; - min-width: 0; -} -.portal-hero__check-title { - display: block; - font-size: 0.85rem; - font-weight: 600; - color: var(--c-text); -} -.portal-hero__check-sub { - display: block; - font-size: 0.75rem; - color: var(--c-text-subtle); - margin-top: 0.05rem; -} -.portal-hero__check-pill { - font-size: 0.6875rem; - font-weight: 600; - color: var(--c-text-subtle); - background: var(--c-border-subtle); - border-radius: 999px; - padding: 0.15rem 0.55rem; - flex-shrink: 0; -} - /* ── Side dialogs (Key documents / Schedule a call / Trial) ───────────────── */ -.portal-sidemodal { - position: fixed; - inset: 0; - z-index: 1100; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - overflow-y: auto; - background: var(--c-overlay); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - animation: portal-procmodal-fade 0.15s ease-out; - /* Portaled to , outside .portal-scope, so set the portal UI font explicitly. */ - font-family: var(--font-sans); -} -.portal-sidemodal__panel { - position: relative; - width: 100%; - max-width: 30rem; - background: var(--c-surface); - border: 1px solid var(--c-border); - border-radius: 14px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28); - padding: 1.35rem 1.4rem 1.4rem; - max-height: 86vh; - overflow-y: auto; -} -/* Wide enough for Calendly's two-pane layout (its single-column layout below ~680px inner width is - tall and scrolls); paired with the embed's taller fixed height so the time view needs no scroll. */ -.portal-sidemodal__panel--wide { - max-width: 52rem; -} -.portal-sidemodal__header { - margin-bottom: 1rem; - padding-right: 2rem; -} +/* Chrome and width come from the shared Modal via FlowModal; only type and content live here. */ .portal-sidemodal__title { margin: 0; font-size: 1.05rem; @@ -1316,15 +621,6 @@ color: var(--c-text-subtle); line-height: 1.55; } -.portal-sidemodal__footer { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - margin-top: 1.1rem; - padding-top: 0.9rem; - border-top: 1px solid var(--c-border-subtle); -} .portal-sidemodal__ghost { border: none; background: none; @@ -1336,63 +632,6 @@ color: var(--c-text-muted); } -/* Key documents ledger. */ -.portal-docs__group + .portal-docs__group { - margin-top: 1rem; -} -.portal-docs__group-title { - font-size: 0.6875rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--c-text-subtle); - margin-bottom: 0.4rem; -} -.portal-docs__list { - list-style: none; - margin: 0; - padding: 0; -} -.portal-docs__row { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.55rem 0; - border-top: 1px solid var(--c-border-subtle); -} -.portal-docs__row-text { - flex: 1; - min-width: 0; -} -.portal-docs__row-name { - display: block; - font-size: 0.8125rem; - font-weight: 600; - color: var(--c-text); -} -.portal-docs__row-sub { - display: block; - font-size: 0.72rem; - color: var(--c-text-subtle); - margin-top: 0.05rem; -} -.portal-docs__row-action { - font-size: 0.6875rem; - font-weight: 600; - border-radius: 999px; - padding: 0.2rem 0.6rem; - flex-shrink: 0; - color: var(--c-text-subtle); - background: var(--c-border-subtle); -} -.portal-docs__row-action[data-status="action"] { - color: var(--c-primary); - background: var(--c-primary-subtle); -} -.portal-docs__row-action[data-status="request"] { - color: var(--c-text-subtle); -} - /* Calendly scheduler embed (Schedule a call). The widget is always light (Calendly renders inputs on white), so give it a white surface — it reads as a clean card even inside the dark-mode modal. */ .portal-calendly { @@ -1418,17 +657,173 @@ } /* ── Agreement (security) step ────────────────────────────────────────────── */ -.portal-agreement__doc { - margin: 1rem 0; - max-height: 22rem; - overflow-y: auto; - padding: 1rem 1.1rem; - border: 1px solid var(--c-border); - border-radius: 10px; - background: var(--color-bg-subtle, var(--c-bg)); - font-size: 0.8125rem; - line-height: 1.55; +/* The terminal steps (payment, live): a stacked eyebrow/title/description with the flow's own footer + bar beneath. No card of their own — the dialog is already the surface. */ +.portal-procstage { + display: flex; + flex-direction: column; + gap: 0.4rem; +} +/* Nothing to report on the left of these, so the actions take the whole bar. */ +.portal-procstage__foot { + justify-content: flex-end; +} + +/* The two document actions read as a pair, close together and set apart from the close beside them. */ +.portal-agreement__actions { + display: flex; + align-items: center; + gap: 0.1rem; +} + +/* The signature block: the three fields that name the bound party and its signatory, on the same line + as the act of signing, with the consent directly beneath them and no rule between. Fields shrink + below the row's usual floor so all three plus the button hold one line at the takeover's width; + they wrap rather than clip if a translation runs long. */ +/* No rule above it: the tray's grey ends where the signature block begins, which is boundary enough, + and a border there cut the document off from the page it sits on. */ +.portal-agreement__signbar { + flex-direction: column; + align-items: stretch; + gap: 0.6rem; + border-top: none; +} +/* Bottom-aligned: a field is a label above an input, so aligning to the block's centre or its top + leaves the button off the input row. Sharing the input's bottom edge puts its centre on theirs, both + being 37px. The gate note is deliberately NOT in this row — hanging it off the button would make the + column taller than the fields and drag the button back up off the row. */ +.portal-agreement__signrow { + display: flex; + align-items: flex-end; + gap: 0.9rem; +} +/* Consent on the left, the gate's state on the right so it lands under the button it explains. */ +.portal-agreement__signfoot { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} +.portal-agreement__gate { + flex: 0 0 auto; + font-size: 0.6875rem; color: var(--c-text-subtle); + white-space: nowrap; +} +.portal-agreement__signrow .portal-agreement__signfields { + flex: 1; + gap: 0.6rem; + min-width: 0; +} +.portal-agreement__signrow .portal-qb__field { + min-width: 8.5rem; +} +/* Unboxed: a line of small print under the fields, not a panel competing with the document. */ +.portal-agreement__accept { + display: flex; + align-items: flex-start; + gap: 0.5rem; + font-size: 0.75rem; + line-height: 1.45; + color: var(--c-text-subtle); + cursor: pointer; +} +.portal-agreement__accept input { + margin: 0.1rem 0 0; + flex: 0 0 auto; +} + +/* The agreement is presented as paper, not as app copy in a box: a sunken tray running to the panel's + edges, with the terms on white stock in a serif face. Signing is the most consequential thing a + buyer does here, so the document should read like the document it is. */ +/* The document takes every pixel the dialog can spare: it is what the buyer is here to read, and a + fixed height left it a small white box floating in a tall panel. The chain has to carry the fill — + each link needs min-height:0 or a flex child refuses to shrink below its content. */ +.portal-agreement { + display: flex; + flex-direction: column; + gap: 0.75rem; + flex: 1 1 auto; + min-height: 0; +} +/* The tray is the scroll container, so the paper inside it behaves like a page being scrolled past a + window: flush to the footer while there is more to read, and revealing the tray's bottom padding + only at the end. Scrolling the paper instead left a permanent grey band under it, which read as a + box with contents rather than a document. */ +/* `scroll`, not `auto`, and an explicitly styled bar: signing is gated on reaching the end of the + document, so the buyer has to be able to see there is more of it and how far through they are. + Overlay scrollbars fade out when idle, which hid both. Styling the bar also opts Chromium out of + overlay behaviour, so it stays put. Matches the treatment on the files page. */ +.portal-agreement__tray { + background: var(--c-surface-sunken); + padding: 14px var(--flowmodal-inset); + margin: 0 calc(-1 * var(--flowmodal-inset)); + flex: 1 1 auto; + min-height: 0; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--c-text-subtle) var(--c-border-subtle); +} +.portal-agreement__tray::-webkit-scrollbar { + width: 0.625rem; +} +.portal-agreement__tray::-webkit-scrollbar-track { + background: var(--c-border-subtle); + border-radius: 999px; +} +.portal-agreement__tray::-webkit-scrollbar-thumb { + background: var(--c-text-subtle); + border-radius: 999px; + border: 2px solid transparent; + background-clip: content-box; +} +.portal-agreement__tray::-webkit-scrollbar-thumb:hover { + background: var(--c-text-muted); + background-clip: content-box; +} +.portal-agreement__doc { + /* A floor so a short document still reads as a page; no ceiling, so a long one runs on and the tray + does the scrolling. */ + min-height: 16rem; + padding: 1.35rem 1.6rem; + border: 1px solid var(--c-border-subtle); + border-radius: 12px; + background: var(--c-surface); + box-shadow: var(--shadow-sm); + /* No serif token exists in the theme — the serif is specific to rendering legal terms as paper. */ + font-family: Georgia, "Times New Roman", "Liberation Serif", serif; + font-size: 0.84rem; + line-height: 1.62; + color: var(--c-text-muted); +} +/* Masthead above the document's own heading: confidentiality on the left, the quote reference and + signed version on the right, over the rule that opens the terms. */ +.portal-agreement__letterhead { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + padding-bottom: 0.6rem; + margin-bottom: 1rem; + border-bottom: 2px solid var(--c-text); + font-family: var(--font-sans); + font-size: 0.6875rem; + letter-spacing: 0.04em; + color: var(--c-text-subtle); +} +.portal-agreement__confidential { + font-weight: 700; + text-transform: uppercase; +} +/* The document's own title, centred like an executed agreement's. */ +.portal-agreement__md > h1:first-child, +.portal-agreement__md > h2:first-child { + text-align: center; + font-size: 1.05rem; + letter-spacing: 0.02em; + text-transform: uppercase; + margin-bottom: 1rem; } .portal-agreement__doc h4 { margin: 1rem 0 0.35rem; @@ -1445,29 +840,129 @@ .portal-agreement__doc strong { color: var(--c-text); } -.portal-agreement__accept { - margin-top: 0.25rem; +.portal-agreement__md h1 { + font-size: 0.95rem; + font-weight: 700; + color: var(--c-text); + margin: 1.1rem 0 0.5rem; } -.portal-agreement__lines { - margin: 0.4rem 0 0.6rem; +.portal-agreement__md h2 { + font-size: 0.875rem; + font-weight: 650; + color: var(--c-text); + margin: 1rem 0 0.4rem; } -.portal-proc__reset { - display: flex; - justify-content: center; - padding-top: 0.5rem; +.portal-agreement__md h3 { + font-size: 0.8125rem; + font-weight: 650; + color: var(--c-text); + margin: 0.9rem 0 0.3rem; } -.portal-proc__reset button { +.portal-agreement__md h1:first-child, +.portal-agreement__md h2:first-child { + margin-top: 0; +} +.portal-agreement__md p { + margin: 0 0 0.55rem; +} +.portal-agreement__md strong { + color: var(--c-text); +} +.portal-agreement__md ul { + margin: 0 0 0.6rem; + padding-left: 1.1rem; +} +.portal-agreement__md li { + margin-bottom: 0.2rem; +} +.portal-agreement__md table { + border-collapse: collapse; + width: 100%; + margin: 0.4rem 0 0.8rem; + font-size: 0.78rem; +} +.portal-agreement__md th, +.portal-agreement__md td { + border: 1px solid var(--c-border); + padding: 0.35rem 0.5rem; + text-align: left; + vertical-align: top; +} +.portal-agreement__md th { + background: var(--c-bg); + font-weight: 650; + color: var(--c-text); +} +.portal-agreement__signfields { + margin-top: 0.75rem; +} +.portal-proc__error { + color: var(--c-danger); + font-size: 0.8125rem; + margin: 0.5rem 0 0; +} +.portal-legal__link { border: none; background: none; + padding: 0; + font: inherit; + color: var(--c-text); + text-decoration: underline; + cursor: pointer; +} +.portal-legal__link:hover { + color: var(--c-text); +} +.portal-docmodal { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} +.portal-docmodal__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem 0; + border-bottom: 1px solid var(--c-border); +} +.portal-docmodal__row:last-child { + border-bottom: none; +} +.portal-docmodal__text { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; +} +.portal-docmodal__name { + font-weight: 650; + font-size: 0.875rem; + color: var(--c-text); +} +.portal-docmodal__sub { + font-size: 0.75rem; + color: var(--c-text-muted); +} +.portal-docmodal__later { + flex-shrink: 0; font-size: 0.75rem; color: var(--c-text-subtle); - cursor: pointer; - text-decoration: underline; + white-space: nowrap; } -.portal-proc__reset button:hover:not(:disabled) { + +/* Title row: the step badge rides beside the heading, not on a line of its own. */ +.portal-sidemodal__title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +/* Quiet hint sat opposite the primary action in a dialog footer. */ +.portal-sidemodal__foot-hint { + font-size: 0.75rem; color: var(--c-text-subtle); } -.portal-proc__reset button:disabled { - opacity: 0.5; - cursor: default; -} diff --git a/frontend/editor/src/portal/views/Procurement.tsx b/frontend/editor/src/portal/views/Procurement.tsx deleted file mode 100644 index 1f6e2329a9..0000000000 --- a/frontend/editor/src/portal/views/Procurement.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ProcurementHome } from "@portal/components/procurement/ProcurementHome"; -import "@portal/views/Procurement.css"; - -/** - * /procurement — procurement is no longer a nav tab; it lives on Home as the deal-status hero. - * This route is kept for deep links (and the Usage "Build your quote" CTA): it renders the same - * surface, opening the takeover modal once a deal is underway. - */ -export function Procurement() { - return ( -
    - -
    - ); -} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index d2853d8f1e..256bdf640a 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -258,12 +258,7 @@ export default defineConfig( // can't represent. Exempt ONLY the raw- ); diff --git a/frontend/editor/src/core/components/shared/FileSidebar.tsx b/frontend/editor/src/core/components/shared/FileSidebar.tsx index 7d09261226..742334015c 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebar.tsx @@ -81,9 +81,10 @@ const EXPANDED_WIDTH = "16.25rem"; // ~260px const WATCHED_FOLDER_VIEW_ID = "watchedFolder"; const WATCHED_FOLDER_WORKBENCH_ID = "custom:watchedFolder"; -// Stable empty props for rows without folders, so the memoized FileItem -// isn't re-rendered by a fresh `?? []` identity on every list render. +// Stable empty props for rows without folders/policies, so the memoized +// FileItem isn't re-rendered by a fresh `?? []` identity on every list render. const NO_FOLDERS: never[] = []; +const NO_POLICIES: never[] = []; /** Only surface the "Adding files…" progress row for drops big enough that the * pre-dispatch scan is user-visible; small adds finish before it would paint. */ @@ -790,7 +791,7 @@ const FileSidebar = forwardRef( onDragStart={handleWatchedFolderDragStart} folders={memberFolders} onFolderClick={openWatchedFolder} - policies={policyFileBadges.get(stub.id as string) ?? []} + policies={policyFileBadges.get(stub.id as string) ?? NO_POLICIES} onDelete={isWatchedFoldersActive ? undefined : handleSidebarDelete} onSaveToCloud={isWatchedFoldersActive ? undefined : handleSaveToCloud} canSaveToCloud={storageEnabled && fileOrigin !== "shared-with-me"} diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx index a289a245f1..c1a93594c5 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx @@ -167,7 +167,9 @@ export interface FileItemProps { const MAX_VISIBLE_FOLDER_TAGS = 2; -export function FileItem({ +// Memoized: sidebar rows bail out unless THEIR props change, so one file's +// update (e.g. a new version landing) re-renders one row, not the whole list. +export const FileItem = React.memo(function FileItem({ fileId, name, size, @@ -509,4 +511,4 @@ export function FileItem({ )} ); -} +}); diff --git a/frontend/editor/src/core/components/shared/PolicyBadges.css b/frontend/editor/src/core/components/shared/PolicyBadges.css index 3a919e24c9..8526de44c6 100644 --- a/frontend/editor/src/core/components/shared/PolicyBadges.css +++ b/frontend/editor/src/core/components/shared/PolicyBadges.css @@ -41,22 +41,3 @@ transform: rotate(360deg); } } - -.policy-badge--recent { - animation: policy-badge-pulse 4.5s ease-in-out forwards; -} -@keyframes policy-badge-pulse { - 0%, - 24%, - 48% { - box-shadow: 0 0 0 0 transparent; - } - 12%, - 36% { - box-shadow: 0 0 5px 2px currentColor; - } - 60%, - 100% { - box-shadow: 0 0 0 0 transparent; - } -} diff --git a/frontend/editor/src/core/components/shared/PolicyBadges.stories.tsx b/frontend/editor/src/core/components/shared/PolicyBadges.stories.tsx index 81969bf615..c3646f774a 100644 --- a/frontend/editor/src/core/components/shared/PolicyBadges.stories.tsx +++ b/frontend/editor/src/core/components/shared/PolicyBadges.stories.tsx @@ -2,10 +2,14 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { PolicyBadges } from "@app/components/shared/PolicyBadges"; import type { FileItemPolicyRef } from "@app/components/shared/PolicyBadges"; +// Real catalog category ids, so each badge renders its own shared glyph +// (policyCategoryIcon) rather than the unknown-category fallback. Accents mirror +// policyAccentVar's mapping — that lives in the proprietary layer, which a core +// story can't import. const mockPolicies: FileItemPolicyRef[] = [ - { id: "policy-1", name: "Redact PII", accentColor: "#e03131", recent: true }, - { id: "policy-2", name: "Sanitize", accentColor: "#2f9e44", recent: false }, - { id: "policy-3", name: "Watermark", accentColor: "#4263eb", recent: false }, + { id: "security", name: "Redact PII", accentColor: "var(--color-purple)" }, + { id: "compliance", name: "Sanitize", accentColor: "var(--color-green)" }, + { id: "ingestion", name: "Watermark", accentColor: "var(--color-blue)" }, ]; const meta = { @@ -22,15 +26,25 @@ export const Default: Story = { }, }; +/** A blocking policy mid-run: spinner, and the file's exit points are gated. */ export const Enforcing: Story = { + args: { + policies: [ + { ...mockPolicies[0], enforcing: true }, + ...mockPolicies.slice(1), + ], + }, +}; + +/** A non-blocking run (classification tagging): same spinner, nothing gated. */ +export const Background: Story = { args: { policies: [ { - id: "policy-1", - name: "Redact PII", - accentColor: "#e03131", - recent: false, - enforcing: true, + id: "classification", + name: "Classification", + accentColor: "var(--color-orange)", + background: true, }, ...mockPolicies.slice(1), ], diff --git a/frontend/editor/src/core/components/shared/PolicyBadges.tsx b/frontend/editor/src/core/components/shared/PolicyBadges.tsx index f8661b9176..1fa57ca70a 100644 --- a/frontend/editor/src/core/components/shared/PolicyBadges.tsx +++ b/frontend/editor/src/core/components/shared/PolicyBadges.tsx @@ -1,6 +1,6 @@ import { Tooltip } from "@mantine/core"; -import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; import AutorenewIcon from "@mui/icons-material/Autorenew"; +import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon"; import { useTranslation } from "react-i18next"; import "@app/components/shared/PolicyBadges.css"; @@ -10,20 +10,20 @@ export interface FileItemPolicyRef { name: string; /** CSS colour for the badge (matches the policy's accent). */ accentColor: string; - /** True only just after the policy was applied — drives the one-off glow, so - * it doesn't replay on every reload of an already-enforced file. */ - recent: boolean; - /** True while the policy run is actively in-flight on this file. */ + /** True while a BLOCKING policy run is in-flight on this file (gates actions). */ enforcing?: boolean; + /** True while a non-blocking run (e.g. classification) is in-flight — shows + * the same spinner but never gates anything. */ + background?: boolean; } const MAX_VISIBLE = 3; /** - * The canonical policy badge row: one accent-tinted shield per policy that has - * run on a file, spinning while a run is in flight, glowing briefly after it - * lands. Every surface that shows per-file policy badges (file sidebar, file - * editor thumbnails, files page) renders this so they stay identical. + * The canonical policy badge row: one accent-tinted category icon per policy + * that has run on a file, spinning while a run is in flight. Every surface that + * shows per-file policy badges (file sidebar, file editor thumbnails, files + * page) renders this so they stay identical. */ export function PolicyBadges({ policies, @@ -40,33 +40,40 @@ export function PolicyBadges({ className={`policy-badges${className ? ` ${className}` : ""}`} data-no-select > - {policies.slice(0, MAX_VISIBLE).map((policy) => ( - - { + const running = policy.enforcing || policy.background; + return ( + - {policy.enforcing ? ( - - ) : ( - - )} - - - ))} + + {running ? ( + + ) : ( + policyCategoryIcon(policy.id, { fontSize: "0.7rem" }) + )} + + + ); + })} ); } diff --git a/frontend/editor/src/core/components/shared/PolicyEnforcingOverlay.tsx b/frontend/editor/src/core/components/shared/PolicyEnforcingOverlay.tsx index cb9e931e80..9d5d2475b3 100644 --- a/frontend/editor/src/core/components/shared/PolicyEnforcingOverlay.tsx +++ b/frontend/editor/src/core/components/shared/PolicyEnforcingOverlay.tsx @@ -4,6 +4,8 @@ export function PolicyEnforcingOverlay(_props: { zIndex?: number; /** CSS colour var for the enforcing policy's accent; tints the icon/spinner. */ accentVar?: string; + /** Category of the enforcing policy — picks its icon in the real overlay. */ + categoryId?: string; }) { return null; } diff --git a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx index 5cc1e6cabd..55ca01d307 100644 --- a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx +++ b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx @@ -19,7 +19,8 @@ import { } from "@app/components/filesPage/filesPageReturnRoute"; import { useWorkbenchBar } from "@app/contexts/WorkbenchBarContext"; import { - useFileState, + useAllFiles, + useFileSelectors, useFileSelection, useFileActions, } from "@app/contexts/FileContext"; @@ -121,10 +122,10 @@ export default function WorkbenchBar({ const { sharingEnabled } = useSharingEnabled(); const viewerContext = React.useContext(ViewerContext); - const { selectors } = useFileState(); + const selectors = useFileSelectors(); const { selectedFiles, selectedFileIds } = useFileSelection(); const { actions: fileActions } = useFileActions(); - const activeFiles = selectors.getFiles(); + const { files: activeFiles } = useAllFiles(); const { activeFileId, setActiveFileId } = useViewer(); const policyFileBadges = usePolicyFileBadges(); // Block print/export while any file the export would touch is under active diff --git a/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx index cc33ccbbb4..fe4a6f8e48 100644 --- a/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx @@ -11,7 +11,7 @@ import { } from "@app/utils/convertUtils"; import { getConversionEndpoints } from "@app/data/toolsTaxonomy"; import { useFileSelection } from "@app/contexts/FileContext"; -import { useFileState } from "@app/contexts/FileContext"; +import { useFileSelector, useFileSelectors } from "@app/contexts/FileContext"; import { detectFileExtension } from "@app/utils/fileUtils"; import { usePreferences } from "@app/contexts/PreferencesContext"; import { useConversionCloudStatus } from "@app/hooks/useConversionCloudStatus"; @@ -62,8 +62,8 @@ const ConvertSettings = ({ const { t } = useTranslation(); const theme = useMantineTheme(); const { setSelectedFiles } = useFileSelection(); - const { state, selectors } = useFileState(); - const activeFiles = state.files.ids; + const selectors = useFileSelectors(); + const activeFiles = useFileSelector((s) => s.files.ids); const { preferences } = usePreferences(); const allEndpoints = useMemo(() => { diff --git a/frontend/editor/src/core/components/tools/shared/ReviewToolStep.tsx b/frontend/editor/src/core/components/tools/shared/ReviewToolStep.tsx index 8c89029913..7918bac7ca 100644 --- a/frontend/editor/src/core/components/tools/shared/ReviewToolStep.tsx +++ b/frontend/editor/src/core/components/tools/shared/ReviewToolStep.tsx @@ -11,7 +11,7 @@ import { Tooltip } from "@app/components/shared/Tooltip"; import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; import { saveOperationResults } from "@app/services/operationResultsSaveService"; -import { useFileActions, useFileState } from "@app/contexts/FileContext"; +import { useFileActions, useFileSelectors } from "@app/contexts/FileContext"; import { FileId } from "@app/types/fileContext"; import i18n from "@app/i18n"; @@ -40,7 +40,7 @@ function ReviewStepContent({ const DownloadIcon = icons.download; const stepRef = useRef(null); const { actions: fileActions } = useFileActions(); - const { selectors } = useFileState(); + const selectors = useFileSelectors(); const handleUndo = async () => { try { diff --git a/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx index cc4a04598e..be4a4a9971 100644 --- a/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx @@ -12,7 +12,12 @@ import { ActionIcon } from "@app/ui/ActionIcon"; import CloseIcon from "@mui/icons-material/Close"; import LockIcon from "@mui/icons-material/Lock"; -import { useFileState, useFileActions } from "@app/contexts/FileContext"; +import { + useAllFiles, + useFileSelector, + useFileSelectors, + useFileActions, +} from "@app/contexts/FileContext"; import { useFileWithUrl } from "@app/hooks/useFileWithUrl"; import { useViewer } from "@app/contexts/ViewerContext"; import { LocalEmbedPDF } from "@app/components/viewer/LocalEmbedPDF"; @@ -259,9 +264,9 @@ const EmbedPdfViewerContent = ({ const redactionTrackerRef = useRef(null); // Get current file from FileContext - const { selectors } = useFileState(); + const selectors = useFileSelectors(); const { actions } = useFileActions(); - const activeFiles = selectors.getFiles(); + const { files: activeFiles } = useAllFiles(); const activeFilesRef = useRef(activeFiles); activeFilesRef.current = activeFiles; const activeFileIds = activeFiles.map((f) => f.fileId); @@ -392,11 +397,11 @@ const EmbedPdfViewerContent = ({ }, [previewFile, fileWithUrl]); // Check if the current file is encrypted (gate the viewer to prevent PDFium crash) - const isCurrentFileEncrypted = React.useMemo(() => { - if (!currentFile || !isStirlingFile(currentFile)) return false; - const stub = selectors.getStirlingFileStub(currentFile.fileId); - return stub?.processedFile?.isEncrypted === true; - }, [currentFile, selectors]); + const isCurrentFileEncrypted = useFileSelector((s) => + currentFile && isStirlingFile(currentFile) + ? s.files.byId[currentFile.fileId]?.processedFile?.isEncrypted === true + : false, + ); const bookmarkCacheKey = React.useMemo(() => { if (currentFile && isStirlingFile(currentFile)) { diff --git a/frontend/editor/src/core/components/viewer/NonPdfViewer.tsx b/frontend/editor/src/core/components/viewer/NonPdfViewer.tsx index ed4a2e2263..bd2704963a 100644 --- a/frontend/editor/src/core/components/viewer/NonPdfViewer.tsx +++ b/frontend/editor/src/core/components/viewer/NonPdfViewer.tsx @@ -4,7 +4,7 @@ import { Button } from "@app/ui/Button"; import ArticleIcon from "@mui/icons-material/Article"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; -import { useFileState } from "@app/contexts/FileContext"; +import { useAllFiles } from "@app/contexts/FileContext"; import { useViewer } from "@app/contexts/ViewerContext"; import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; import { @@ -126,8 +126,7 @@ export function NonPdfViewer({ file }: NonPdfViewerProps) { // ─── Wrapper that resolves the active file from FileContext ─────────────────── export function NonPdfViewerWrapper(props: ViewerProps) { - const { selectors } = useFileState(); - const activeFiles = selectors.getFiles(); + const { files: activeFiles } = useAllFiles(); const { activeFileIndex } = useViewer(); const file = diff --git a/frontend/editor/src/core/components/viewer/Viewer.tsx b/frontend/editor/src/core/components/viewer/Viewer.tsx index 08103ca1f5..36a1080dbb 100644 --- a/frontend/editor/src/core/components/viewer/Viewer.tsx +++ b/frontend/editor/src/core/components/viewer/Viewer.tsx @@ -5,7 +5,7 @@ import { NonPdfViewerWrapper, type ViewerProps, } from "@app/components/viewer/NonPdfViewer"; -import { useFileState } from "@app/contexts/FileContext"; +import { useAllFiles } from "@app/contexts/FileContext"; import { useViewer } from "@app/contexts/ViewerContext"; import { isStirlingFile } from "@app/types/fileContext"; import { isPdfFile } from "@app/utils/fileUtils"; @@ -26,8 +26,7 @@ type SignatureOverlayPassThrough = Pick< >; const Viewer = (props: ViewerProps & SignatureOverlayPassThrough) => { - const { selectors } = useFileState(); - const activeFiles = selectors.getFiles(); + const { files: activeFiles } = useAllFiles(); const { activeFileId } = useViewer(); // Determine the active file — previewFile takes priority, then look up by stable ID diff --git a/frontend/editor/src/core/components/viewer/ViewerAnnotationControls.tsx b/frontend/editor/src/core/components/viewer/ViewerAnnotationControls.tsx index 32b4b1ce11..fda6c2d2a5 100644 --- a/frontend/editor/src/core/components/viewer/ViewerAnnotationControls.tsx +++ b/frontend/editor/src/core/components/viewer/ViewerAnnotationControls.tsx @@ -5,7 +5,11 @@ import { ActionIcon } from "@app/ui/ActionIcon"; import { Tooltip } from "@app/components/shared/Tooltip"; import { ViewerContext } from "@app/contexts/ViewerContext"; import { useSignature } from "@app/contexts/SignatureContext"; -import { useFileState, useFileContext } from "@app/contexts/FileContext"; +import { + useAllFiles, + useFileSelectors, + useFileContext, +} from "@app/contexts/FileContext"; import { createStirlingFilesAndStubs } from "@app/services/fileStubHelpers"; import { useNavigationState, @@ -39,9 +43,9 @@ export default function ViewerAnnotationControls({ const { historyApiRef, isPlacementMode } = useSignature(); // File state for save functionality - const { state, selectors } = useFileState(); + const selectors = useFileSelectors(); + const { files: activeFiles, fileIds } = useAllFiles(); const { actions: fileActions } = useFileContext(); - const activeFiles = selectors.getFiles(); // Check if we're in sign mode or redaction mode const { selectedTool } = useNavigationState(); @@ -83,7 +87,7 @@ export default function ViewerAnnotationControls({ !historyApiRef?.current?.canUndo() ) return; - if (activeFiles.length === 0 || state.files.ids.length === 0) return; + if (activeFiles.length === 0 || fileIds.length === 0) return; try { const arrayBuffer = await viewerContext.exportActions.saveAsCopy(); @@ -92,7 +96,7 @@ export default function ViewerAnnotationControls({ const file = new File([new Blob([arrayBuffer])], activeFiles[0].name, { type: "application/pdf", }); - const parentStub = selectors.getStirlingFileStub(state.files.ids[0]); + const parentStub = selectors.getStirlingFileStub(fileIds[0]); if (!parentStub) return; const { stirlingFiles, stubs } = await createStirlingFilesAndStubs( @@ -100,11 +104,7 @@ export default function ViewerAnnotationControls({ parentStub, "redact", ); - await fileActions.consumeFiles( - [state.files.ids[0]], - stirlingFiles, - stubs, - ); + await fileActions.consumeFiles([fileIds[0]], stirlingFiles, stubs); // Clear unsaved changes flags after successful save setHasUnsavedChanges(false); diff --git a/frontend/editor/src/core/components/viewer/ViewerShareButton.tsx b/frontend/editor/src/core/components/viewer/ViewerShareButton.tsx index f063cbffe2..4ef4b30cda 100644 --- a/frontend/editor/src/core/components/viewer/ViewerShareButton.tsx +++ b/frontend/editor/src/core/components/viewer/ViewerShareButton.tsx @@ -9,7 +9,7 @@ import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import { Tooltip } from "@app/components/shared/Tooltip"; import ShareManagementModal from "@app/components/shared/ShareManagementModal"; import { useViewer } from "@app/contexts/ViewerContext"; -import { useFileState, useFileActions } from "@app/contexts/FileContext"; +import { useAllFiles, useFileActions } from "@app/contexts/FileContext"; import { uploadHistoryChain } from "@app/services/serverStorageUpload"; import { fileStorage } from "@app/services/fileStorage"; import { alert } from "@app/components/toast"; @@ -39,7 +39,7 @@ export default function ViewerShareButton({ }: ViewerShareButtonProps) { const { t } = useTranslation(); const { activeFileId } = useViewer(); - const { selectors } = useFileState(); + const { fileStubs } = useAllFiles(); const { actions } = useFileActions(); const [confirmOpen, setConfirmOpen] = useState(false); const [saving, setSaving] = useState(false); @@ -49,7 +49,7 @@ export default function ViewerShareButton({ // Resolve strictly to the file shown in the viewer. Never fall back to an // arbitrary file — sharing the wrong document would be worse than not // sharing. If there's no active file, the button is disabled (see isDisabled). - const stubs = selectors.getStirlingFileStubs(); + const stubs = fileStubs; const stub = activeFileId ? stubs.find((s) => s.id === activeFileId) : undefined; diff --git a/frontend/editor/src/core/components/viewer/ZoomAPIBridge.tsx b/frontend/editor/src/core/components/viewer/ZoomAPIBridge.tsx index 2501ddfb79..0cb11837cc 100644 --- a/frontend/editor/src/core/components/viewer/ZoomAPIBridge.tsx +++ b/frontend/editor/src/core/components/viewer/ZoomAPIBridge.tsx @@ -3,7 +3,7 @@ import { useZoom, ZoomMode } from "@embedpdf/plugin-zoom/react"; import { useSpread, SpreadMode } from "@embedpdf/plugin-spread/react"; import { useViewer } from "@app/contexts/ViewerContext"; import { useActiveDocumentId } from "@app/components/viewer/useActiveDocumentId"; -import { useFileState } from "@app/contexts/FileContext"; +import { useAllFiles } from "@app/contexts/FileContext"; import { determineAutoZoom, DEFAULT_FALLBACK_ZOOM, @@ -36,7 +36,7 @@ function ZoomAPIBridgeInner({ documentId }: { documentId: string }) { const { provides: zoom, state: zoomState } = useZoom(documentId); const { spreadMode } = useSpread(documentId); const { registerBridge, triggerImmediateZoomUpdate } = useViewer(); - const { selectors } = useFileState(); + const { fileStubs } = useAllFiles(); const hasSetInitialZoom = useRef(false); const lastSpreadMode = useRef(spreadMode ?? SpreadMode.None); @@ -62,7 +62,7 @@ function ZoomAPIBridgeInner({ documentId }: { documentId: string }) { } }, []); - const stubs = selectors.getStirlingFileStubs(); + const stubs = fileStubs; const firstFileStub = stubs[0]; const firstFileId = firstFileStub?.id; diff --git a/frontend/editor/src/core/components/viewer/useViewerReadAloud.ts b/frontend/editor/src/core/components/viewer/useViewerReadAloud.ts index c2ccdec1be..5352ffc748 100644 --- a/frontend/editor/src/core/components/viewer/useViewerReadAloud.ts +++ b/frontend/editor/src/core/components/viewer/useViewerReadAloud.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { computeReadAloudHighlightRect } from "@app/components/viewer/readAloudHighlight"; -import { useFileState } from "@app/contexts/FileContext"; +import { useFileSelectors } from "@app/contexts/FileContext"; import { useViewer } from "@app/contexts/ViewerContext"; import { useStopReadAloudOnNavigation } from "@app/components/viewer/useStopReadAloudOnNavigation"; import { pdfWorkerManager } from "@app/services/pdfWorkerManager"; @@ -60,7 +60,7 @@ function createHighlightElement( export function useViewerReadAloud(defaultLanguage?: string) { const viewer = useViewer(); - const { selectors } = useFileState(); + const selectors = useFileSelectors(); const [isReadingAloud, setIsReadingAloud] = useState(false); const [speechRate, setSpeechRate] = useState(1); diff --git a/frontend/editor/src/core/contexts/FileContext.tsx b/frontend/editor/src/core/contexts/FileContext.tsx index d0808665cd..a982c347ca 100644 --- a/frontend/editor/src/core/contexts/FileContext.tsx +++ b/frontend/editor/src/core/contexts/FileContext.tsx @@ -16,6 +16,7 @@ import { useReducer, useCallback, useEffect, + useLayoutEffect, useRef, useMemo, useState, @@ -23,7 +24,6 @@ import { import { FileContextProviderProps, FileContextSelectors, - FileContextStateValue, FileContextActionsValue, FileContextActions, FileId, @@ -36,6 +36,7 @@ import { import { fileContextReducer, initialFileContextState, + withReducerIdentityGuard, } from "@app/contexts/file/FileReducer"; import { createFileSelectors } from "@app/contexts/file/fileSelectors"; import { @@ -49,8 +50,9 @@ import { } from "@app/contexts/file/fileActions"; import { FileLifecycleManager } from "@app/contexts/file/lifecycle"; import { - FileStateContext, + FileStoreContext, FileActionsContext, + type FileStateStore, } from "@app/contexts/file/contexts"; import { IndexedDBProvider, @@ -75,10 +77,13 @@ function FileContextInner({ children, enablePersistence = true, }: FileContextProviderProps) { - const [state, dispatch] = useReducer( - fileContextReducer, - initialFileContextState, + // Guarded in dev: warns if a reducer case reallocates a slice without changing + // it, which would silently defeat the selector-subscription bail-out. + const guardedReducer = useMemo( + () => withReducerIdentityGuard(fileContextReducer), + [], ); + const [state, dispatch] = useReducer(guardedReducer, initialFileContextState); // Always call the hook unconditionally to satisfy React's rules of hooks. // IndexedDB context is only used when enablePersistence is true. @@ -657,14 +662,28 @@ function FileContextInner({ ], ); - // Split context values to minimize re-renders - const stateValue = useMemo( + // Subscription store bridge: the context value is STABLE, so consumers only + // re-render when the slice they select (via useFileSelector) changes — not on + // every state change. Listeners are notified after each committed state. + const listenersRef = useRef void>>(new Set()); + const store = useMemo( () => ({ - state, + getState: () => stateRef.current, + subscribe: (listener) => { + listenersRef.current.add(listener); + return () => { + listenersRef.current.delete(listener); + }; + }, selectors, }), - [state, selectors], + [selectors], ); + // Layout effect (not passive): subscribers re-render before the browser + // paints, so a state change can never show a frame with stale consumers. + useLayoutEffect(() => { + for (const listener of listenersRef.current) listener(); + }, [state]); const actionsValue = useMemo( () => ({ @@ -698,7 +717,7 @@ function FileContextInner({ }, [lifecycleManager]); return ( - + {children} - + ); } @@ -758,6 +777,10 @@ export function FileContextProvider({ export { useFileState, useFileActions, + useFileSelector, + useFileSelectors, + useFileIndex, + shallowEqual, useCurrentFile, useFileSelection, useFileManagement, diff --git a/frontend/editor/src/core/contexts/ViewerContext.tsx b/frontend/editor/src/core/contexts/ViewerContext.tsx index 22d95606c1..5e5d08dbb4 100644 --- a/frontend/editor/src/core/contexts/ViewerContext.tsx +++ b/frontend/editor/src/core/contexts/ViewerContext.tsx @@ -9,7 +9,11 @@ import React, { useCallback, } from "react"; import { useNavigation } from "@app/contexts/NavigationContext"; -import { useFileState } from "@app/contexts/FileContext"; +import { + useFileIndex, + useFileSelector, + useFileSelectors, +} from "@app/contexts/FileContext"; import { isStirlingFile } from "@app/types/fileContext"; import type { FileId } from "@app/types/file"; import { enforceExportPolicies } from "@app/services/policyExport"; @@ -244,25 +248,21 @@ export const ViewerProvider: React.FC = ({ children }) => { const [activeFileId, setActiveFileId] = useState(null); // activeFileIndex is derived from activeFileId so they can never desync. - // ViewerProvider sits inside FileContextProvider so useFileState is valid here. - const { selectors, state } = useFileState(); + // ViewerProvider sits inside FileContextProvider so these hooks are valid here. + const selectors = useFileSelectors(); + const fileIds = useFileSelector((s) => s.files.ids); // Clear activeFileId when its file is removed from the workbench. // Dep on state.files.ids so the effect re-runs on every add/remove. useEffect(() => { if (!activeFileId) return; - const stillInWorkbench = state.files.ids.some( + const stillInWorkbench = fileIds.some( (id) => (id as string) === activeFileId, ); if (!stillInWorkbench) setActiveFileId(null); - }, [activeFileId, state.files.ids]); + }, [activeFileId, fileIds]); - const activeFileIndex = useMemo(() => { - if (!activeFileId) return 0; - const files = selectors.getFiles(); - const idx = files.findIndex((f) => f.fileId === activeFileId); - return idx >= 0 ? idx : 0; - }, [activeFileId, selectors]); + const activeFileIndex = useFileIndex(activeFileId); const setActiveFileIndex = useCallback( (index: number) => { const files = selectors.getFiles(); diff --git a/frontend/editor/src/core/contexts/file/FileReducer.ts b/frontend/editor/src/core/contexts/file/FileReducer.ts index 91bd62cf4d..2697974823 100644 --- a/frontend/editor/src/core/contexts/file/FileReducer.ts +++ b/frontend/editor/src/core/contexts/file/FileReducer.ts @@ -425,3 +425,83 @@ export function fileContextReducer( return state; } } + +// ── Dev-only structural-sharing guard ────────────────────────────────────── +// +// The file hooks bail a consumer out of re-rendering when the slice it selects +// keeps its object identity across a dispatch. That optimisation silently +// breaks if a reducer case returns a NEW identity for a slice it didn't +// actually change (e.g. an unnecessary `{ ...state.files }`): every consumer of +// that slice re-renders for nothing, with no test failure. This wrapper warns +// when that happens. No-op in production. + +function idsUnchanged(a: FileId[], b: FileId[]): boolean { + return a.length === b.length && a.every((id, i) => id === b[i]); +} + +function byIdUnchanged( + a: Record, + b: Record, +): boolean { + const keysA = Object.keys(a); + return ( + keysA.length === Object.keys(b).length && + keysA.every((id) => a[id as FileId] === b[id as FileId]) + ); +} + +function uiUnchanged( + a: FileContextState["ui"], + b: FileContextState["ui"], +): boolean { + return (Object.keys(a) as Array).every( + (k) => a[k] === b[k], + ); +} + +function setUnchanged(a: Set, b: Set): boolean { + if (a.size !== b.size) return false; + for (const v of a) if (!b.has(v)) return false; + return true; +} + +/** + * Wrap a reducer so, outside production, it warns when an action reallocates a + * top-level state slice without changing its contents — which would defeat the + * selector-subscription bail-out in the file hooks. + */ +export function withReducerIdentityGuard( + reducer: (s: FileContextState, a: FileContextAction) => FileContextState, +): (s: FileContextState, a: FileContextAction) => FileContextState { + if (process.env.NODE_ENV === "production") return reducer; + return (state, action) => { + const next = reducer(state, action); + if (next === state) return next; + if ( + next.files !== state.files && + idsUnchanged(next.files.ids, state.files.ids) && + byIdUnchanged(next.files.byId, state.files.byId) + ) { + console.error( + `[FileReducer] '${action.type}' reallocated state.files without changing it — ` + + "this re-renders every file consumer for nothing. Return the existing slice unchanged.", + ); + } + if (next.ui !== state.ui && uiUnchanged(next.ui, state.ui)) { + console.error( + `[FileReducer] '${action.type}' reallocated state.ui without changing it — ` + + "this re-renders every UI consumer for nothing. Return the existing slice unchanged.", + ); + } + if ( + next.pinnedFiles !== state.pinnedFiles && + setUnchanged(next.pinnedFiles, state.pinnedFiles) + ) { + console.error( + `[FileReducer] '${action.type}' reallocated state.pinnedFiles without changing it — ` + + "this re-renders every pinned-files consumer for nothing.", + ); + } + return next; + }; +} diff --git a/frontend/editor/src/core/contexts/file/classificationToolRace.test.ts b/frontend/editor/src/core/contexts/file/classificationToolRace.test.ts new file mode 100644 index 0000000000..faf6501a0c --- /dev/null +++ b/frontend/editor/src/core/contexts/file/classificationToolRace.test.ts @@ -0,0 +1,167 @@ +import { describe, it, expect } from "vitest"; +import { fileContextReducer } from "@app/contexts/file/FileReducer"; +import type { + FileContextAction, + FileContextState, + StirlingFileStub, +} from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +/** + * Classification is non-blocking: while it runs, the user can manually run a + * tool on the same file. Classification's only write is a metadata-only, + * shallow-merged UPDATE_FILE_RECORD stamping `classificationLabels`; a manual + * tool run produces a NEW document via CONSUME_FILES (new id + version). These + * tests drive the REAL reducer through every interleaving (classification lands + * before / during / after the tool run) and prove the invariant the design + * relies on: the tool's output document is byte-for-byte what the tool produced, + * regardless of when classification lands. (Label PLACEMENT in the mid-run race + * is the orchestration's job — usePolicyAutoRun resolves targets at write time; + * see usePolicyAutoRun.race.test.tsx. Here we lock the reducer backstop.) + */ + +const stub = ( + id: string, + extra: Partial = {}, +): StirlingFileStub => + ({ + id: id as FileId, + name: "doc.pdf", + versionNumber: 1, + ...extra, + }) as StirlingFileStub; + +function stateWith(...stubs: StirlingFileStub[]): FileContextState { + return { + files: { + ids: stubs.map((s) => s.id), + byId: Object.fromEntries(stubs.map((s) => [s.id, s])) as Record< + FileId, + StirlingFileStub + >, + }, + pinnedFiles: new Set(), + ui: { + selectedFileIds: [], + selectedPageNumbers: [], + isProcessing: false, + processingProgress: 0, + hasUnsavedChanges: false, + errorFileIds: [], + }, + }; +} + +const LABELS = ["Invoice"]; + +// A manual tool run on `inputId` producing a new versioned document `outputId`. +// Mirrors what useToolOperation dispatches: the reducer stamps provenance +// (derivedFromTool, sourceFileIds) and inherits labels itself. +const toolRun = (inputId: string, outputId: string): FileContextAction => ({ + type: "CONSUME_FILES", + payload: { + inputFileIds: [inputId as FileId], + outputStirlingFileStubs: [stub(outputId, { versionNumber: 2 })], + silent: false, + }, +}); + +// Classification stamping labels onto a target id (the reducer merges shallowly). +const classify = (targetId: string): FileContextAction => ({ + type: "UPDATE_FILE_RECORD", + payload: { + id: targetId as FileId, + updates: { classificationLabels: LABELS }, + }, +}); + +describe("classification landing vs a manually-run tool", () => { + it("PRE: classification lands first — tool output is correct AND inherits the label", () => { + let s = stateWith(stub("orig")); + s = fileContextReducer(s, classify("orig")); + s = fileContextReducer(s, toolRun("orig", "out")); + + const out = s.files.byId["out" as FileId]; + expect(out).toBeDefined(); + expect(out.versionNumber).toBe(2); // the document the tool produced + expect(s.files.byId["orig" as FileId]).toBeUndefined(); // input consumed + // Label carried forward onto the tool's new version. + expect(out.classificationLabels).toEqual(LABELS); + }); + + it("POST: classification lands after the tool run, targeting the new leaf — output untouched, label applied, nothing else clobbered", () => { + let s = stateWith(stub("orig")); + s = fileContextReducer(s, toolRun("orig", "out")); + + const before = s.files.byId["out" as FileId]; + // classificationLabelTargets resolves the run's descendants: "out" matches + // because its sourceFileIds includes "orig". + expect(before.sourceFileIds).toContain("orig" as FileId); + + s = fileContextReducer(s, classify("out")); + const after = s.files.byId["out" as FileId]; + + // The label write is a shallow merge: ONLY classificationLabels changes. + expect(after.classificationLabels).toEqual(LABELS); + expect({ ...after, classificationLabels: undefined }).toEqual({ + ...before, + classificationLabels: undefined, + }); + expect(after.versionNumber).toBe(2); + }); + + it("MID (the race): a label write aimed at an already-consumed id no-ops — output document is CORRECT, nothing is resurrected", () => { + // In production this stale-id write no longer happens: usePolicyAutoRun + // resolves the label targets AT WRITE TIME, so the labels land on the live + // leaf instead (see usePolicyAutoRun.race.test.tsx). This test locks the + // reducer-level BACKSTOP behind that: even if a stale id does get written, + // it cannot corrupt or resurrect anything. + let s = stateWith(stub("orig")); + + const staleTargetId = "orig"; + + // During that window the user runs a tool: orig -> out. orig had no labels + // yet, so the new leaf inherits none. + s = fileContextReducer(s, toolRun("orig", "out")); + const out = s.files.byId["out" as FileId]; + expect(out.versionNumber).toBe(2); + expect(out.classificationLabels).toBeUndefined(); + + // Classification's write finally lands — on the now-consumed snapshot id. + const beforeWrite = s; + s = fileContextReducer(s, classify(staleTargetId)); + + // No-op on a missing record: reducer returns the SAME state reference, so no + // zombie "orig" record is resurrected and nothing is corrupted. + expect(s).toBe(beforeWrite); + expect(s.files.byId["orig" as FileId]).toBeUndefined(); + + // The tool's output document is intact and exactly what the tool produced. + const finalOut = s.files.byId["out" as FileId]; + expect(finalOut.versionNumber).toBe(2); + expect(finalOut.sourceFileIds).toContain("orig" as FileId); + // At the reducer level the stale write leaves the leaf unlabelled — which + // is why the orchestration resolves targets at write time instead. The + // DOCUMENT is unaffected either way. + expect(finalOut.classificationLabels).toBeUndefined(); + }); + + it("classification can never overwrite a tool output's document fields (only the label)", () => { + // Tool output already carries its own state; classification must not disturb it. + let s = stateWith( + stub("out", { + versionNumber: 7, + thumbnailUrl: "blob:thumb", + isPinned: true, + } as Partial), + ); + + s = fileContextReducer(s, classify("out")); + const after = s.files.byId["out" as FileId]; + + expect(after.versionNumber).toBe(7); + expect(after.thumbnailUrl).toBe("blob:thumb"); + expect((after as { isPinned?: boolean }).isPinned).toBe(true); + expect(after.classificationLabels).toEqual(LABELS); + }); +}); diff --git a/frontend/editor/src/core/contexts/file/contexts.ts b/frontend/editor/src/core/contexts/file/contexts.ts index c17a178043..6bc74e11ae 100644 --- a/frontend/editor/src/core/contexts/file/contexts.ts +++ b/frontend/editor/src/core/contexts/file/contexts.ts @@ -4,14 +4,28 @@ import { createContext } from "react"; import { + FileContextState, + FileContextSelectors, FileContextStateValue, FileContextActionsValue, } from "@app/types/fileContext"; -// Split contexts for performance -export const FileStateContext = createContext< - FileContextStateValue | undefined ->(undefined); +/** + * Subscription store for file state. The context VALUE is stable — consumers + * subscribe and select slices (see useFileSelector), re-rendering only when + * their selected slice changes, instead of on every state change. + */ +export interface FileStateStore { + getState: () => FileContextState; + subscribe: (listener: () => void) => () => void; + /** Stable selector API (reads live state via refs). */ + selectors: FileContextSelectors; +} + +export const FileStoreContext = createContext( + undefined, +); + export const FileActionsContext = createContext< FileContextActionsValue | undefined >(undefined); diff --git a/frontend/editor/src/core/contexts/file/fileActions.ts b/frontend/editor/src/core/contexts/file/fileActions.ts index 77447003f6..db99e1c673 100644 --- a/frontend/editor/src/core/contexts/file/fileActions.ts +++ b/frontend/editor/src/core/contexts/file/fileActions.ts @@ -370,32 +370,57 @@ export async function addFiles( // Collect hydrations to schedule after dispatch so updateStirlingFileStub finds files in state. const pendingHydrations: Array<() => Promise> = []; + // Per-chunk persistence promises (kicked off as chunks flush, awaited before + // return). See flushChunk — we stream writes instead of one batch at the end. + const persistPromises: Array> = []; - // Stream the batch into the workspace in chunks. The per-file pre-scan below - // (dedupe, encryption sniff — which reads each PDF's bytes) takes real time - // for a big folder drop; a single end-of-loop dispatch would leave the UI - // frozen-looking for seconds and then dump hundreds of rows in one render. - // Chunked dispatch keeps rows (and their thumbnail hydrations) streaming in, - // and the progress store drives the sidebar's "Adding files…" indicator. - const DISPATCH_CHUNK = 25; + // Dispatch stubs in chunks so rows (and thumbnail hydrations) stream in + // rather than dumping the whole drop in one render. + const DISPATCH_CHUNK = 5; let flushedStubs = 0; let flushedHydrations = 0; - const flushChunk = () => { - if ( - !options.skipWorkspaceDispatch && - stirlingFileStubs.length > flushedStubs - ) { - dispatch({ - type: "ADD_FILES", - payload: { stirlingFileStubs: stirlingFileStubs.slice(flushedStubs) }, - }); + // Flushes the pending chunk and returns this chunk's persistence promises, + // so the caller can await the writes (see the loop's yield) before the policy + // auto-run tries to read the file back from storage. + const flushChunk = (): Array> => { + const chunkWrites: Array> = []; + if (stirlingFileStubs.length > flushedStubs) { + const from = flushedStubs; + const newStubs = stirlingFileStubs.slice(from); flushedStubs = stirlingFileStubs.length; + if (!options.skipWorkspaceDispatch) { + dispatch({ + type: "ADD_FILES", + payload: { stirlingFileStubs: newStubs }, + }); + } + // Persist each chunk as it flushes, not one batch at the end: the policy + // auto-run reads files from IndexedDB with no in-memory fallback. + if (enablePersistence) { + const newFiles = stirlingFiles.slice(from); + for (let i = 0; i < newFiles.length; i++) { + const sf = newFiles[i]; + const stub = newStubs[i]; + const write = fileStorage + .storeStirlingFile(sf, stub) + .catch((error) => { + console.error( + "Failed to persist file to storage:", + sf.name, + error, + ); + }); + chunkWrites.push(write); + persistPromises.push(write); + } + } } // Hydrations only after their chunk is dispatched, so // updateStirlingFileStub finds the files in state. while (flushedHydrations < pendingHydrations.length) { scheduleMetadataHydration(pendingHydrations[flushedHydrations++]); } + return chunkWrites; }; reportBulkAddProgress(0, filesToProcess.length); @@ -554,38 +579,25 @@ export async function addFiles( reportBulkAddProgress(++scannedCount, filesToProcess.length); if (stirlingFileStubs.length - flushedStubs >= DISPATCH_CHUNK) { - flushChunk(); + const chunkWrites = flushChunk(); + // Yield a MACROTASK so React commits this chunk and runs its effects + // (incl. the policy-enforcement dispatch) before the next chunk scans. + // The per-file awaits above are only microtasks, which don't give React + // a turn — without this, all dispatches batch and processing can't begin + // until the whole drop is scanned. Awaiting the chunk's writes first means + // the auto-run finds each file's bytes already committed in storage. + await Promise.all(chunkWrites); + await new Promise((resolve) => setTimeout(resolve)); } } // Flush the remainder (also the sole dispatch for small batches). flushChunk(); - // Persist to storage if enabled using fileStorage service - if (enablePersistence && stirlingFiles.length > 0) { - await Promise.all( - stirlingFiles.map(async (stirlingFile, index) => { - try { - // Get corresponding stub with all metadata - const fileStub = stirlingFileStubs[index]; - - // Store using the cleaner signature - pass StirlingFile + StirlingFileStub directly - await fileStorage.storeStirlingFile(stirlingFile, fileStub); - - if (DEBUG) - console.log( - `📄 addFiles: Stored file ${stirlingFile.name} with metadata:`, - fileStub, - ); - } catch (error) { - console.error( - "Failed to persist file to storage:", - stirlingFile.name, - error, - ); - } - }), - ); + // Wait for the per-chunk writes (streamed in flushChunk) to commit, so + // addFiles only resolves once every file is durably stored. + if (enablePersistence && persistPromises.length > 0) { + await Promise.all(persistPromises); } if (!options.skipUploadTracking && stirlingFiles.length > 0) { diff --git a/frontend/editor/src/core/contexts/file/fileHooks.selector.test.tsx b/frontend/editor/src/core/contexts/file/fileHooks.selector.test.tsx new file mode 100644 index 0000000000..5a8f3243f9 --- /dev/null +++ b/frontend/editor/src/core/contexts/file/fileHooks.selector.test.tsx @@ -0,0 +1,223 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, act } from "@testing-library/react"; +import { useEffect } from "react"; +import { MantineProvider } from "@mantine/core"; +import { FileContextProvider } from "@app/contexts/FileContext"; +import { + useAllFiles, + useFileContext, + useFileSelection, + useFileSelectors, + useStirlingFileStub, + useFileActions, +} from "@app/contexts/file/fileHooks"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; +import type { FileContextAction } from "@app/types/fileContext"; + +/** + * Proves the selector-subscription contract: a consumer re-renders only when + * the slice it selects changes — a single file's update doesn't re-render + * other files' consumers, and selection changes don't re-render list consumers. + */ + +const stub = (id: string): StirlingFileStub => + ({ + id: id as FileId, + name: `${id}.pdf`, + type: "application/pdf", + size: 1, + lastModified: 0, + }) as StirlingFileStub; + +const renders: Record = {}; +let dispatchRef: React.Dispatch | null = null; + +function Controller() { + const { dispatch } = useFileActions(); + dispatchRef = dispatch; + return null; +} + +function StubWatcher({ fileId }: { fileId: string }) { + useStirlingFileStub(fileId as FileId); + renders[`stub-${fileId}`] = (renders[`stub-${fileId}`] ?? 0) + 1; + return null; +} + +function ListWatcher() { + useAllFiles(); + renders.list = (renders.list ?? 0) + 1; + return null; +} + +function SelectionWatcher() { + useFileSelection(); + renders.selection = (renders.selection ?? 0) + 1; + return null; +} + +function setup() { + for (const key of Object.keys(renders)) delete renders[key]; + dispatchRef = null; + render( + + + + + + + + + , + ); + act(() => { + dispatchRef!({ + type: "ADD_FILES", + payload: { stirlingFileStubs: [stub("a"), stub("b")] }, + }); + }); + return { ...renders }; +} + +describe("file hooks — selector subscriptions", () => { + it("updating one file re-renders that file's consumer, not the other's", () => { + const before = setup(); + act(() => { + dispatchRef!({ + type: "UPDATE_FILE_RECORD", + payload: { id: "b" as FileId, updates: { name: "renamed.pdf" } }, + }); + }); + expect(renders["stub-b"]).toBeGreaterThan(before["stub-b"]); + expect(renders["stub-a"]).toBe(before["stub-a"]); + }); + + it("selection changes don't re-render file-list or per-file consumers", () => { + const before = setup(); + act(() => { + dispatchRef!({ + type: "SET_SELECTED_FILES", + payload: { fileIds: ["a" as FileId] }, + }); + }); + expect(renders.selection).toBeGreaterThan(before.selection); + expect(renders.list).toBe(before.list); + expect(renders["stub-a"]).toBe(before["stub-a"]); + expect(renders["stub-b"]).toBe(before["stub-b"]); + }); + + it("file-list changes don't re-render selection-only consumers", () => { + const before = setup(); + act(() => { + dispatchRef!({ + type: "UPDATE_FILE_RECORD", + payload: { id: "a" as FileId, updates: { name: "x.pdf" } }, + }); + }); + expect(renders.selection).toBe(before.selection); + }); +}); + +describe("useFileSelectors — render-phase misuse guard", () => { + const guardErrors = (spy: ReturnType) => + spy.mock.calls.filter((args) => + String(args[0]).includes("[useFileSelectors]"), + ); + + function RenderTimeMisuse() { + const selectors = useFileSelectors(); + selectors.getAllFileIds(); // during render — must be flagged + return null; + } + + function EffectTimeUse() { + const selectors = useFileSelectors(); + useEffect(() => { + selectors.getAllFileIds(); // after commit — legitimate + }, [selectors]); + return null; + } + + it("flags a selector invoked during render", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + render( + + + + + , + ); + expect(guardErrors(spy).length).toBeGreaterThan(0); + spy.mockRestore(); + }); + + it("does not flag selector reads from effects", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + render( + + + + + , + ); + expect(guardErrors(spy)).toHaveLength(0); + spy.mockRestore(); + }); +}); + +describe("useFileContext — render-phase misuse guard", () => { + // useFileContext subscribes to files + pinnedFiles only, so a render-time read + // of the SELECTION slice through its exposed selectors would silently go + // stale. The guard covers exactly those selectors and nothing else. + const guardErrors = (spy: ReturnType) => + spy.mock.calls.filter((args) => + String(args[0]).includes("[useFileContext]"), + ); + + const renderWithGuard = (node: React.ReactNode) => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + render( + + {node} + , + ); + const errors = guardErrors(spy); + spy.mockRestore(); + return errors; + }; + + function SelectionReadDuringRender() { + const { selectors } = useFileContext(); + selectors.getSelectedFiles(); // unsubscribed slice — must be flagged + return null; + } + + function FilesReadDuringRender() { + const { selectors } = useFileContext(); + selectors.getStirlingFileStubs(); // files slice IS subscribed — legitimate + return null; + } + + function SelectionReadFromEffect() { + const { selectors } = useFileContext(); + useEffect(() => { + selectors.getSelectedFiles(); // after commit — legitimate + }, [selectors]); + return null; + } + + it("flags a selection read during render", () => { + expect( + renderWithGuard().length, + ).toBeGreaterThan(0); + }); + + it("does not flag reads of a slice it subscribes to", () => { + expect(renderWithGuard()).toHaveLength(0); + }); + + it("does not flag selection reads from effects", () => { + expect(renderWithGuard()).toHaveLength(0); + }); +}); diff --git a/frontend/editor/src/core/contexts/file/fileHooks.ts b/frontend/editor/src/core/contexts/file/fileHooks.ts index fcd3ba7ef7..f5c1bfc460 100644 --- a/frontend/editor/src/core/contexts/file/fileHooks.ts +++ b/frontend/editor/src/core/contexts/file/fileHooks.ts @@ -1,27 +1,187 @@ /** - * Performant file hooks - Clean API using FileContext + * Performant file hooks — selector subscriptions over the FileStateStore. + * Each hook re-renders its consumer only when the slice it selects changes, + * not on every file-state change. */ -import { useContext, useMemo } from "react"; +import { useContext, useLayoutEffect, useMemo, useRef } from "react"; +import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector.js"; import { - FileStateContext, + FileStoreContext, FileActionsContext, + FileStateStore, FileContextStateValue, FileContextActionsValue, } from "@app/contexts/file/contexts"; -import { StirlingFileStub, StirlingFile } from "@app/types/fileContext"; +import { + StirlingFileStub, + StirlingFile, + FileContextState, + FileContextSelectors, +} from "@app/types/fileContext"; import { FileId } from "@app/types/file"; +const GUARD_MISUSE = process.env.NODE_ENV !== "production"; + +/** Shallow equality over object/array slices assembled by selectors. */ +export function shallowEqual(a: unknown, b: unknown): boolean { + if (Object.is(a, b)) return true; + if ( + typeof a !== "object" || + a === null || + typeof b !== "object" || + b === null + ) { + return false; + } + const keysA = Object.keys(a); + if (keysA.length !== Object.keys(b).length) return false; + return keysA.every((key) => + Object.is( + (a as Record)[key], + (b as Record)[key], + ), + ); +} + +function useFileStore(): FileStateStore { + const store = useContext(FileStoreContext); + if (!store) { + throw new Error("File hooks must be used within a FileContextProvider"); + } + return store; +} + +/** + * Subscribe to a slice of file state. The component re-renders only when the + * selected value changes (Object.is by default; pass shallowEqual for slices + * assembled into fresh objects/arrays). + */ +export function useFileSelector( + selector: (state: FileContextState) => T, + isEqual?: (a: T, b: T) => boolean, +): T { + const store = useFileStore(); + return useSyncExternalStoreWithSelector( + store.subscribe, + store.getState, + store.getState, + selector, + isEqual, + ); +} + +/** Selectors that read `ui.selectedFileIds`. A hook that doesn't subscribe to + * that slice must not let consumers call these during render. */ +const SELECTION_SELECTORS: ReadonlyArray = [ + "getSelectedFiles", + "getSelectedStirlingFileStubs", +]; + +/** Wrap selectors so a call made during render logs loudly (dev/test only). + * Render-time vs event-time isn't statically lintable, so this is the guard. + * `keys` limits the wrap to the selectors whose slice the calling hook does NOT + * subscribe to — the rest are safe to read during render and pass through. */ +function guardSelectors( + selectors: FileContextSelectors, + isRendering: () => boolean, + hookName: string, + keys?: ReadonlyArray, +): FileContextSelectors { + const guardedKeys = + keys ?? (Object.keys(selectors) as Array); + const guarded: Record = { ...selectors }; + for (const key of guardedKeys) { + const original = selectors[key] as unknown as ( + ...args: unknown[] + ) => unknown; + guarded[key] = (...args: unknown[]) => { + if (isRendering()) { + console.error( + `[${hookName}] ${key}() was called during render. This read doesn't ` + + "subscribe to the state it depends on, so the UI can go stale — use " + + "useFileSelector / useFileSelection / useAllFiles for render-time data.", + ); + } + return original(...args); + }; + } + return guarded as unknown as FileContextSelectors; +} + +/** + * Wrap a hook's exposed selectors in the render-phase misuse guard (no-op in + * production). `keys` names the selectors the calling hook doesn't subscribe to; + * omit it to guard every selector (for hooks that subscribe to nothing). + */ +function useGuardedSelectors( + selectors: FileContextSelectors, + hookName: string, + keys?: ReadonlyArray, +): FileContextSelectors { + // True exactly while this consumer is rendering: set on every render, cleared + // by the layout effect once that render commits. + const renderPhase = useRef(false); + renderPhase.current = GUARD_MISUSE; + useLayoutEffect(() => { + renderPhase.current = false; + }); + return useMemo( + () => + GUARD_MISUSE + ? guardSelectors(selectors, () => renderPhase.current, hookName, keys) + : selectors, + [selectors, hookName, keys], + ); +} + +/** + * Stable selector API with NO state subscription — never re-renders. For + * event-time reads (callbacks/effects), which see live state when invoked. + * Render-time reads need a reactive hook (useAllFiles/useFileSelector) or + * they go stale — calling one during render logs an error outside production. + */ +export function useFileSelectors(): FileContextSelectors { + const { selectors } = useFileStore(); + return useGuardedSelectors(selectors, "useFileSelectors"); +} + +/** + * Position of `fileId` in the resolved file list — the SAME array useAllFiles() + * returns, which drops ids whose bytes haven't hydrated into memory yet, so the + * index lines up with what consumers actually index into. 0 when unset/absent. + * + * Selects a NUMBER, so the consumer re-renders only when the index actually + * moves. useAllFiles() would do the job too, but it re-renders on every + * unrelated stub update (thumbnail hydration, labels, …) — too costly for a + * high-level provider whose context value isn't memoized. + */ +export function useFileIndex(fileId: string | null | undefined): number { + // Raw (unguarded) selectors: the read below runs inside the subscription + // selector, so it IS reactive and the render-phase guard doesn't apply. + const { selectors } = useFileStore(); + return useFileSelector((s) => { + if (!fileId) return 0; + const index = selectors + .getFiles(s.files.ids) + .findIndex((file) => file.fileId === fileId); + return index >= 0 ? index : 0; + }); +} + /** * Hook for accessing file state (will re-render on any state change) * Use individual selector hooks below for better performance */ export function useFileState(): FileContextStateValue { - const context = useContext(FileStateContext); - if (!context) { - throw new Error("useFileState must be used within a FileContextProvider"); - } - return context; + const store = useFileStore(); + const state = useFileSelector((s) => s); + // Selectors are exposed unguarded on purpose: this hook subscribes to the + // WHOLE state, so a render-time selector read can't go stale. + return useMemo( + () => ({ state, selectors: store.selectors }), + [state, store.selectors], + ); } /** @@ -39,21 +199,21 @@ export function useFileActions(): FileContextActionsValue { * Hook for current/primary file (first in list) */ export function useCurrentFile(): { file?: File; record?: StirlingFileStub } { - const { state, selectors } = useFileState(); - - const primaryFileId = state.files.ids[0]; - const primaryFileRecord = primaryFileId - ? state.files.byId[primaryFileId] - : undefined; + const { selectors } = useFileStore(); + const { primaryFileId, record } = useFileSelector( + (s) => ({ + primaryFileId: s.files.ids[0], + record: s.files.ids[0] ? s.files.byId[s.files.ids[0]] : undefined, + }), + shallowEqual, + ); return useMemo( () => ({ file: primaryFileId ? selectors.getFile(primaryFileId) : undefined, - record: primaryFileId - ? selectors.getStirlingFileStub(primaryFileId) - : undefined, + record, }), - [primaryFileId, primaryFileRecord, selectors], + [primaryFileId, record, selectors], ); } @@ -61,27 +221,35 @@ export function useCurrentFile(): { file?: File; record?: StirlingFileStub } { * Hook for file selection state and actions */ export function useFileSelection() { - const { state, selectors } = useFileState(); + const { selectors } = useFileStore(); const { actions } = useFileActions(); + const selectedFileIds = useFileSelector((s) => s.ui.selectedFileIds); + const selectedPageNumbers = useFileSelector((s) => s.ui.selectedPageNumbers); + // Only the SELECTED files' records — an unrelated file's update never + // re-renders selection consumers. + const selectedStubs = useFileSelector( + (s) => s.ui.selectedFileIds.map((id) => s.files.byId[id]), + shallowEqual, + ); // Memoize selected files to avoid recreating arrays const selectedFiles = useMemo(() => { return selectors.getSelectedFiles(); - }, [state.ui.selectedFileIds, state.files.byId, selectors]); + }, [selectedFileIds, selectedStubs, selectors]); return useMemo( () => ({ selectedFiles, - selectedFileIds: state.ui.selectedFileIds, - selectedPageNumbers: state.ui.selectedPageNumbers, + selectedFileIds, + selectedPageNumbers, setSelectedFiles: actions.setSelectedFiles, setSelectedPages: actions.setSelectedPages, clearSelections: actions.clearSelections, }), [ selectedFiles, - state.ui.selectedFileIds, - state.ui.selectedPageNumbers, + selectedFileIds, + selectedPageNumbers, actions.setSelectedFiles, actions.setSelectedPages, actions.clearSelections, @@ -111,57 +279,64 @@ export function useFileManagement() { * Hook for UI state */ export function useFileUI() { - const { state } = useFileState(); const { actions } = useFileActions(); + const ui = useFileSelector( + (s) => ({ + isProcessing: s.ui.isProcessing, + processingProgress: s.ui.processingProgress, + hasUnsavedChanges: s.ui.hasUnsavedChanges, + }), + shallowEqual, + ); return useMemo( () => ({ - isProcessing: state.ui.isProcessing, - processingProgress: state.ui.processingProgress, - hasUnsavedChanges: state.ui.hasUnsavedChanges, + ...ui, setProcessing: actions.setProcessing, setUnsavedChanges: actions.setHasUnsavedChanges, }), - [state.ui, actions], + [ui, actions], ); } /** - * Hook for specific file by ID (optimized for individual file access) + * Hook for specific file by ID (optimized for individual file access): + * re-renders only when THAT file's record changes. */ export function useStirlingFileStub(fileId: FileId): { file?: File; record?: StirlingFileStub; } { - const { state, selectors } = useFileState(); - const fileRecord = state.files.byId[fileId]; + const { selectors } = useFileStore(); + const record = useFileSelector((s) => s.files.byId[fileId]); return useMemo( () => ({ file: selectors.getFile(fileId), - record: selectors.getStirlingFileStub(fileId), + record, }), - [fileId, fileRecord, selectors], + [fileId, record, selectors], ); } /** - * Hook for all files (use sparingly - causes re-renders on file list changes) + * Hook for all files: re-renders on file-list changes only (not selection/UI). */ export function useAllFiles(): { files: StirlingFile[]; fileStubs: StirlingFileStub[]; fileIds: FileId[]; } { - const { state, selectors } = useFileState(); + const { selectors } = useFileStore(); + const files = useFileSelector((s) => s.files); return useMemo( () => ({ - files: selectors.getFiles(), - fileStubs: selectors.getStirlingFileStubs(), - fileIds: state.files.ids, + files: selectors.getFiles(files.ids), + fileStubs: selectors.getStirlingFileStubs(files.ids), + fileIds: files.ids, }), - [state.files.ids, state.files.byId, selectors], + [files, selectors], ); } @@ -173,30 +348,47 @@ export function useSelectedFiles(): { selectedFileStubs: StirlingFileStub[]; selectedFileIds: FileId[]; } { - const { state, selectors } = useFileState(); + const { selectors } = useFileStore(); + const selectedFileIds = useFileSelector((s) => s.ui.selectedFileIds); + // Only the SELECTED files' records — see useFileSelection. + const selectedStubs = useFileSelector( + (s) => s.ui.selectedFileIds.map((id) => s.files.byId[id]), + shallowEqual, + ); return useMemo( () => ({ selectedFiles: selectors.getSelectedFiles(), selectedFileStubs: selectors.getSelectedStirlingFileStubs(), - selectedFileIds: state.ui.selectedFileIds, + selectedFileIds, }), - [state.ui.selectedFileIds, state.files.byId, selectors], + [selectedFileIds, selectedStubs, selectors], ); } -// Navigation management removed - moved to NavigationContext - /** - * Primary API hook for file context operations - * Used by tools for core file context functionality + * Primary API hook for file context operations. Used by tools for core file + * context functionality. Re-renders only when the slices it exposes reactively + * (files, pinned files) change — not on selection/UI changes. */ export function useFileContext() { - const { state, selectors } = useFileState(); + const store = useFileStore(); const { actions } = useFileActions(); + const { files, pinnedFiles } = useFileSelector( + (s) => ({ files: s.files, pinnedFiles: s.pinnedFiles }), + shallowEqual, + ); + // This hook subscribes to files + pinnedFiles, so those selectors are safe to + // read during render; the SELECTION ones aren't (no subscription to + // ui.selectedFileIds), so they carry the misuse guard. + const selectors = useGuardedSelectors( + store.selectors, + "useFileContext", + SELECTION_SELECTORS, + ); - return useMemo( - () => ({ + return useMemo(() => { + return { // Lifecycle management trackBlobUrl: actions.trackBlobUrl, scheduleCleanup: actions.scheduleCleanup, @@ -213,10 +405,11 @@ export function useFileContext() { _operationId: string, _error: string, ) => {}, // Operation tracking not implemented - // File ID lookup + // File ID lookup (reads live state at call time) findFileId: (file: File) => { - return state.files.ids.find((id) => { - const record = state.files.byId[id]; + const { files: liveFiles } = store.getState(); + return liveFiles.ids.find((id) => { + const record = liveFiles.byId[id]; return ( record && record.name === file.name && @@ -227,19 +420,18 @@ export function useFileContext() { }, // Pinned files - pinnedFiles: state.pinnedFiles, + pinnedFiles, pinFile: actions.pinFile, unpinFile: actions.unpinFile, isFilePinned: selectors.isFilePinned, // Active files - activeFiles: selectors.getFiles(), + activeFiles: selectors.getFiles(files.ids), openEncryptedUnlockPrompt: actions.openEncryptedUnlockPrompt, // Direct access to actions and selectors (for advanced use cases) actions, selectors, - }), - [state, selectors, actions], - ); + }; + }, [files, pinnedFiles, actions, store, selectors]); } diff --git a/frontend/editor/src/core/contexts/file/reducerIdentityGuard.test.ts b/frontend/editor/src/core/contexts/file/reducerIdentityGuard.test.ts new file mode 100644 index 0000000000..c250c3e4dc --- /dev/null +++ b/frontend/editor/src/core/contexts/file/reducerIdentityGuard.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { withReducerIdentityGuard } from "@app/contexts/file/FileReducer"; +import type { + FileContextState, + FileContextAction, + StirlingFileStub, +} from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const stub = (id: string): StirlingFileStub => + ({ id: id as FileId, name: `${id}.pdf` }) as StirlingFileStub; + +function baseState(): FileContextState { + return { + files: { ids: ["a" as FileId], byId: { ["a" as FileId]: stub("a") } }, + pinnedFiles: new Set(), + ui: { + selectedFileIds: [], + selectedPageNumbers: [], + isProcessing: false, + processingProgress: 0, + hasUnsavedChanges: false, + errorFileIds: [], + }, + }; +} + +const guardErrors = (spy: ReturnType) => + spy.mock.calls.filter((a) => String(a[0]).includes("[FileReducer]")); + +afterEach(() => vi.restoreAllMocks()); + +describe("withReducerIdentityGuard", () => { + it("warns when a slice is reallocated but unchanged", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + // Bad reducer: rebuilds `files` (new ref) with identical contents. + const guarded = withReducerIdentityGuard((s) => ({ + ...s, + files: { ids: [...s.files.ids], byId: { ...s.files.byId } }, + })); + guarded(baseState(), { type: "NOOP" } as unknown as FileContextAction); + expect(guardErrors(spy)).toHaveLength(1); + expect(String(guardErrors(spy)[0][0])).toContain("state.files"); + }); + + it("stays quiet when a slice genuinely changes", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + const guarded = withReducerIdentityGuard((s) => ({ + ...s, + files: { + ids: [...s.files.ids, "b" as FileId], + byId: { ...s.files.byId, ["b" as FileId]: stub("b") }, + }, + })); + guarded(baseState(), { type: "NOOP" } as unknown as FileContextAction); + expect(guardErrors(spy)).toHaveLength(0); + }); + + it("stays quiet when the reducer returns the same state reference", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + const guarded = withReducerIdentityGuard((s) => s); + const state = baseState(); + expect( + guarded(state, { type: "NOOP" } as unknown as FileContextAction), + ).toBe(state); + expect(guardErrors(spy)).toHaveLength(0); + }); + + it("flags a needless ui reallocation", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + const guarded = withReducerIdentityGuard((s) => ({ + ...s, + ui: { ...s.ui }, + })); + guarded(baseState(), { type: "NOOP" } as unknown as FileContextAction); + expect(String(guardErrors(spy)[0][0])).toContain("state.ui"); + }); +}); diff --git a/frontend/editor/src/core/contexts/file/useFileIndex.test.tsx b/frontend/editor/src/core/contexts/file/useFileIndex.test.tsx new file mode 100644 index 0000000000..e10bfed517 --- /dev/null +++ b/frontend/editor/src/core/contexts/file/useFileIndex.test.tsx @@ -0,0 +1,128 @@ +import { describe, it, expect } from "vitest"; +import { render, act } from "@testing-library/react"; +import { + FileStoreContext, + type FileStateStore, +} from "@app/contexts/file/contexts"; +import { useFileIndex } from "@app/contexts/file/fileHooks"; +import type { + FileContextSelectors, + FileContextState, +} from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +/** + * useFileIndex replaced a render-time `selectors.getFiles()` read in + * ViewerContext, which never re-subscribed and so survived a file-list change + * that moved the active file. These tests drive a hand-built store (the real one + * needs IndexedDB to populate its File map) and lock the two properties the fix + * depends on: the index tracks the RESOLVED file list, and the consumer + * re-renders only when the index actually moves. + */ + +function makeStore(ids: string[], resolved: string[]) { + let state: FileContextState = { + files: { ids: ids as FileId[], byId: {} }, + } as FileContextState; + let resolvedIds = new Set(resolved); + const listeners = new Set<() => void>(); + + // Mirrors createFileSelectors.getFiles: maps ids through the in-memory File + // map and DROPS the ones whose bytes haven't landed yet. + const selectors = { + getFiles: (requested?: FileId[]) => + (requested ?? state.files.ids) + .filter((id) => resolvedIds.has(id as string)) + .map((id) => ({ fileId: id })), + } as unknown as FileContextSelectors; + + const store: FileStateStore = { + getState: () => state, + subscribe: (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + selectors, + }; + + const update = (nextIds: string[], nextResolved: string[] = nextIds) => { + act(() => { + state = { + files: { ids: nextIds as FileId[], byId: {} }, + } as FileContextState; + resolvedIds = new Set(nextResolved); + listeners.forEach((listener) => listener()); + }); + }; + + return { store, update }; +} + +function setup(ids: string[], resolved: string[], fileId: string | null) { + const { store, update } = makeStore(ids, resolved); + let renders = 0; + let index = -1; + + function Probe() { + index = useFileIndex(fileId); + renders++; + return null; + } + + render( + + + , + ); + + return { update, get: () => index, renderCount: () => renders }; +} + +describe("useFileIndex", () => { + it("reports the active file's position in the resolved list", () => { + const { get } = setup(["a", "b", "c"], ["a", "b", "c"], "c"); + expect(get()).toBe(2); + }); + + it("skips ids whose bytes haven't hydrated, matching what consumers index into", () => { + // "a" has no File yet, so getFiles() yields [b, c] — "c" sits at 1, not 2. + const { get } = setup(["a", "b", "c"], ["b", "c"], "c"); + expect(get()).toBe(1); + }); + + it("updates when the list reorders under a stable active file", () => { + // The regression this fixes: activeFileId never changed, so the old + // useMemo kept returning the pre-reorder index. + const { get, update } = setup(["a", "b", "c"], ["a", "b", "c"], "c"); + expect(get()).toBe(2); + update(["c", "a", "b"]); + expect(get()).toBe(0); + }); + + it("updates when a file ahead of the active one is removed", () => { + const { get, update } = setup(["a", "b", "c"], ["a", "b", "c"], "c"); + update(["b", "c"]); + expect(get()).toBe(1); + }); + + it("falls back to 0 when the active file leaves the list", () => { + const { get, update } = setup(["a", "b"], ["a", "b"], "b"); + update(["a"]); + expect(get()).toBe(0); + }); + + it("returns 0 with no active file", () => { + const { get } = setup(["a", "b"], ["a", "b"], null); + expect(get()).toBe(0); + }); + + it("does not re-render when a store change leaves the index alone", () => { + // Selecting a NUMBER is the point: appending after the active file, or any + // unrelated stub churn, must not re-render the consumer. + const { get, update, renderCount } = setup(["a", "b"], ["a", "b"], "a"); + const before = renderCount(); + update(["a", "b", "c"]); + expect(get()).toBe(0); + expect(renderCount()).toBe(before); + }); +}); diff --git a/frontend/editor/src/core/tools/Convert.tsx b/frontend/editor/src/core/tools/Convert.tsx index de29e4ae94..b1d214f1a6 100644 --- a/frontend/editor/src/core/tools/Convert.tsx +++ b/frontend/editor/src/core/tools/Convert.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; -import { useFileState } from "@app/contexts/FileContext"; +import { useAllFiles } from "@app/contexts/FileContext"; import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles"; import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; @@ -14,8 +14,7 @@ import { BaseToolProps, ToolComponent } from "@app/types/tool"; const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); - const { selectors } = useFileState(); - const activeFiles = selectors.getFiles(); + const { files: activeFiles } = useAllFiles(); const selectedFiles = useViewScopedFiles(); const scrollContainerRef = useRef(null); diff --git a/frontend/editor/src/desktop/hooks/useExitWarning.ts b/frontend/editor/src/desktop/hooks/useExitWarning.ts index 1e6b4423fc..4a5b16fb8f 100644 --- a/frontend/editor/src/desktop/hooks/useExitWarning.ts +++ b/frontend/editor/src/desktop/hooks/useExitWarning.ts @@ -1,14 +1,14 @@ import { useEffect, useRef } from "react"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { message } from "@tauri-apps/plugin-dialog"; -import { useFileState, useFileActions } from "@app/contexts/FileContext"; +import { useFileSelectors, useFileActions } from "@app/contexts/FileContext"; import { downloadFile } from "@app/services/downloadService"; import type { StirlingFileStub } from "@app/types/fileContext"; import { useTranslation } from "react-i18next"; export function useExitWarning() { const { t } = useTranslation(); - const { selectors } = useFileState(); + const selectors = useFileSelectors(); const { actions: fileActions } = useFileActions(); const selectorsRef = useRef(selectors); const isClosingRef = useRef(false); diff --git a/frontend/editor/src/desktop/hooks/useSaveShortcut.ts b/frontend/editor/src/desktop/hooks/useSaveShortcut.ts index 927d5f06cf..b3faa64bb7 100644 --- a/frontend/editor/src/desktop/hooks/useSaveShortcut.ts +++ b/frontend/editor/src/desktop/hooks/useSaveShortcut.ts @@ -1,5 +1,9 @@ import { useEffect } from "react"; -import { useFileState, useFileActions } from "@app/contexts/FileContext"; +import { + useFileSelector, + useFileSelectors, + useFileActions, +} from "@app/contexts/FileContext"; // Save through the export gateway so a "run on export" policy enforces before // the file is written out (no-op when no such policy is active). import { downloadFileWithPolicy as downloadFile } from "@app/services/exportWithPolicy"; @@ -10,7 +14,8 @@ import { downloadFileWithPolicy as downloadFile } from "@app/services/exportWith * Matches WorkbenchBar button behavior: saves selected files if any, otherwise all files */ export function useSaveShortcut() { - const { selectors, state } = useFileState(); + const selectors = useFileSelectors(); + const currentSelectedFileIds = useFileSelector((s) => s.ui.selectedFileIds); const { actions: fileActions } = useFileActions(); useEffect(() => { @@ -20,7 +25,7 @@ export function useSaveShortcut() { event.preventDefault(); // Get selected files or all files if nothing selected - const selectedFileIds = state.ui.selectedFileIds; + const selectedFileIds = currentSelectedFileIds; const filesToSave = selectedFileIds.length > 0 ? selectors.getFiles(selectedFileIds) @@ -63,5 +68,5 @@ export function useSaveShortcut() { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [selectors, state.ui.selectedFileIds, fileActions]); + }, [selectors, currentSelectedFileIds, fileActions]); } diff --git a/frontend/editor/src/proprietary/components/policies/classificationLabelTargets.test.ts b/frontend/editor/src/proprietary/components/policies/classificationLabelTargets.test.ts new file mode 100644 index 0000000000..74baf59006 --- /dev/null +++ b/frontend/editor/src/proprietary/components/policies/classificationLabelTargets.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { classificationLabelTargetStubs } from "@app/components/policies/usePolicyAutoRun"; +import type { StirlingFileStub } from "@app/types/fileContext"; + +// Loosely-typed builder: FileId is a branded string, so accept plain string ids +// in tests and cast — classificationLabelTargetStubs only reads id/parent/sources. +const stub = (s: { + id: string; + parentFileId?: string; + sourceFileIds?: string[]; +}): StirlingFileStub => s as unknown as StirlingFileStub; + +const ids = (stubs: StirlingFileStub[]) => stubs.map((s) => s.id as string); + +describe("classificationLabelTargetStubs", () => { + it("targets the run's own file when it's still the leaf", () => { + const stubs = [stub({ id: "a" }), stub({ id: "b" })]; + expect(ids(classificationLabelTargetStubs("a", stubs))).toEqual(["a"]); + }); + + it("targets a descendant leaf when the file was edited during the run", () => { + // "a" was consumed into leaf "a2" (edit forked a new version mid-run). + const stubs = [stub({ id: "a2", sourceFileIds: ["a"] })]; + expect(ids(classificationLabelTargetStubs("a", stubs))).toEqual(["a2"]); + }); + + it("targets a direct child via parentFileId", () => { + const stubs = [stub({ id: "a2", parentFileId: "a" })]; + expect(ids(classificationLabelTargetStubs("a", stubs))).toEqual(["a2"]); + }); + + it("returns the stubs themselves, so the caller can see what's already tagged", () => { + const target = stub({ id: "a" }); + expect(classificationLabelTargetStubs("a", [target])[0]).toBe(target); + }); + + it("is empty when the document has left the workspace (file closed)", () => { + // No fallback to the run's own id: stamping a consumed id would no-op + // anyway, and an empty result lets the caller settle without downloading. + expect(classificationLabelTargetStubs("a", [stub({ id: "z" })])).toEqual( + [], + ); + }); +}); diff --git a/frontend/editor/src/proprietary/components/policies/dispatchSemaphore.test.ts b/frontend/editor/src/proprietary/components/policies/dispatchSemaphore.test.ts new file mode 100644 index 0000000000..355d8b5526 --- /dev/null +++ b/frontend/editor/src/proprietary/components/policies/dispatchSemaphore.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + acquireDispatchSlot, + releaseDispatchSlot, + resetDispatchSemaphoreForTests, +} from "@app/components/policies/dispatchSemaphore"; + +// Drain the microtask queue so an acquire's await-resume AND the caller's .then +// have both run. +const flush = () => new Promise((r) => setTimeout(r, 0)); + +beforeEach(() => resetDispatchSemaphoreForTests()); + +describe("dispatchSemaphore", () => { + it("lets up to 4 acquire without waiting, then blocks the 5th", async () => { + for (let i = 0; i < 4; i++) await acquireDispatchSlot(); + let fifthAcquired = false; + void acquireDispatchSlot().then(() => { + fifthAcquired = true; + }); + await flush(); + expect(fifthAcquired).toBe(false); + releaseDispatchSlot(); + await flush(); + expect(fifthAcquired).toBe(true); + }); + + it("serves a priority (chained) waiter before earlier normal waiters", async () => { + for (let i = 0; i < 4; i++) await acquireDispatchSlot(); // pool full + const order: string[] = []; + // Two normal (new-file) dispatches queue first… + void acquireDispatchSlot(false).then(() => order.push("normal-1")); + void acquireDispatchSlot(false).then(() => order.push("normal-2")); + // …then a chained dispatch arrives — it must jump ahead. + void acquireDispatchSlot(true).then(() => order.push("chained")); + await flush(); + + releaseDispatchSlot(); + await flush(); + releaseDispatchSlot(); + await flush(); + releaseDispatchSlot(); + await flush(); + + expect(order).toEqual(["chained", "normal-1", "normal-2"]); + }); +}); diff --git a/frontend/editor/src/proprietary/components/policies/dispatchSemaphore.ts b/frontend/editor/src/proprietary/components/policies/dispatchSemaphore.ts new file mode 100644 index 0000000000..d5119e5923 --- /dev/null +++ b/frontend/editor/src/proprietary/components/policies/dispatchSemaphore.ts @@ -0,0 +1,42 @@ +/** + * Bounded concurrency for policy run-dispatch uploads. + * + * Each dispatch POSTs a file's bytes; firing a whole drop at once saturates the + * browser's per-origin connection pool, so status polls and output downloads of + * already-running files queue behind the pending uploads and nothing visibly + * progresses. A small window keeps connections free. + * + * `priority` (a chained/downstream dispatch) jumps to the FRONT of the queue, so + * a file already mid-chain finishes its whole policy flow before a brand-new + * file's first policy starts. Without it a chained dispatch would sit behind the + * entire first-policy wave (FIFO) — e.g. classification wouldn't start on any + * file until security had finished on all of them. + */ +const MAX_CONCURRENT_DISPATCHES = 4; + +let slotsInUse = 0; +const waiters: Array<() => void> = []; + +export async function acquireDispatchSlot(priority = false): Promise { + if (slotsInUse < MAX_CONCURRENT_DISPATCHES) { + slotsInUse++; + return; + } + await new Promise((resolve) => { + if (priority) waiters.unshift(resolve); + else waiters.push(resolve); + }); +} + +export function releaseDispatchSlot(): void { + const next = waiters.shift(); + // Hand the slot straight to the next waiter, else free it. + if (next) next(); + else slotsInUse--; +} + +/** Test-only: reset module state between cases. */ +export function resetDispatchSemaphoreForTests(): void { + slotsInUse = 0; + waiters.length = 0; +} diff --git a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.batch.test.tsx b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.batch.test.tsx index 5837bfb285..bc4fa54dff 100644 --- a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.batch.test.tsx +++ b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.batch.test.tsx @@ -2,21 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; /** - * Batch integration test for the policy auto-run orchestration, at the scale the - * user hit the bug: 61 files uploaded at once, two active upload policies - * (Classification → Security) chained. Drives the REAL policyRunStore + the REAL - * hook effects (dispatch → poll → import → chain), mocking only the IO boundaries - * (network, storage, thumbnail/stub creation). - * - * Proves the invariants the user asked for: - * - 61 files ⇒ exactly 122 runs (61 classification, then 61 security). - * - Delivery is SILENT + in place (consumeFiles called with { silent: true }), - * never adding a second copy — the workspace never grows past 61. - * - No runaway: if the loop guard regressed, the run count would blow past 122 - * (or the test would time out), so an exact 122 is a hard regression gate. - * - Closing all files mid-run does NOT re-open them: with the workspace emptied, - * outputs are delivered to storage (persistVersionedOutputs), never re-added - * to the workspace via consumeFiles. + * Batch integration test (61 files, two chained upload policies) driving the real + * store + hook effects, IO mocked. Classification is forced last (see the sort). */ const FILE_COUNT = 61; @@ -25,13 +12,15 @@ const FILE_COUNT = 61; // the workbench, mirrored into useAllFiles. consumeFiles mutates it in place // (input id → output id) exactly as the real silent reducer would. const mocks = vi.hoisted(() => ({ - workspace: [] as Array<{ id: string }>, + workspace: [] as Array<{ id: string; classificationLabels?: string[] }>, consumeSilentCalls: 0, consumeNonSilentCalls: 0, persistCalls: 0, addFilesCalls: 0, stubCounter: 0, backendOutCounter: 0, + dispatchInFlight: 0, + maxDispatchInFlight: 0, bumpRevision: vi.fn(), runStoredPolicy: vi.fn(), getPolicyRun: vi.fn(), @@ -66,7 +55,8 @@ vi.mock("@app/contexts/IndexedDBContext", () => ({ vi.mock("@app/hooks/usePolicies", () => ({ usePolicies: () => ({ policies: { - // Classification runs first (order 0), Security second (order 1). + // Classification is configured first (order 0) but is FORCED to run last + // by the orchestrator; Security (order 1) therefore runs first. classification: { configured: true, status: "active", @@ -107,7 +97,9 @@ vi.mock("@app/services/fileStubHelpers", () => ({ createStirlingFilesAndStubs: mocks.createStirlingFilesAndStubs, })); vi.mock("@app/services/fileClassification", () => ({ - readClassificationLabelsFromFile: vi.fn().mockResolvedValue(null), + // Classification always resolves labels here, so the metadata-only import path + // stamps them onto the stub. + readClassificationLabelsFromFile: vi.fn().mockResolvedValue(["Invoice"]), })); import { usePolicyAutoRun } from "@app/components/policies/usePolicyAutoRun"; @@ -141,6 +133,8 @@ beforeEach(() => { mocks.addFilesCalls = 0; mocks.stubCounter = 0; mocks.backendOutCounter = 0; + mocks.dispatchInFlight = 0; + mocks.maxDispatchInFlight = 0; mocks.workspace = Array.from({ length: FILE_COUNT }, (_, i) => ({ id: `file-${i}`, @@ -155,15 +149,31 @@ beforeEach(() => { mocks.persistVersionedOutputs.mockImplementation(async () => { mocks.persistCalls += 1; }); - mocks.updateFileMetadata.mockResolvedValue(false); + mocks.updateFileMetadata.mockResolvedValue(true); mocks.downloadPolicyOutput.mockResolvedValue( new Blob(["x"], { type: "application/pdf" }), ); + // Apply stub updates to the shared workspace, as the real reducer does — the + // label stamp's second pass reads them back to stay idempotent. + mocks.updateStirlingFileStub.mockImplementation( + (id: string, updates: Record) => { + const stub = mocks.workspace.find((s) => s.id === id); + if (stub) Object.assign(stub, updates); + }, + ); // Each dispatch gets a unique run id; the run's single backend output likewise. - mocks.runStoredPolicy.mockImplementation( - async () => `run-${mocks.stubCounter++}`, - ); + // Takes real time so overlapping dispatches are measurable (the upload window). + mocks.runStoredPolicy.mockImplementation(async () => { + mocks.dispatchInFlight++; + mocks.maxDispatchInFlight = Math.max( + mocks.maxDispatchInFlight, + mocks.dispatchInFlight, + ); + await new Promise((resolve) => setTimeout(resolve, 2)); + mocks.dispatchInFlight--; + return `run-${mocks.stubCounter++}`; + }); mocks.getPolicyRun.mockImplementation(async (runId: string) => ({ runId, policyId: null, @@ -225,8 +235,8 @@ async function runUntilSettled(expectedRuns: number) { }); } -describe("policy auto-run — 61-file batch through a Classification → Security chain", () => { - it("produces exactly 122 runs (61 classification, then 61 security)", async () => { +describe("policy auto-run — 61-file batch through a Security → Classification chain", () => { + it("produces exactly 122 runs (61 security, then 61 classification)", async () => { await runUntilSettled(FILE_COUNT * 2); const classification = latestRuns.filter( @@ -239,15 +249,26 @@ describe("policy auto-run — 61-file batch through a Classification → Securit expect(latestRuns).toHaveLength(FILE_COUNT * 2); }); - it("delivers every output SILENTLY in place — workspace never grows past 61", async () => { + it("bounds concurrent dispatch uploads so polls/downloads keep connections", async () => { + await runUntilSettled(FILE_COUNT * 2); + expect(mocks.maxDispatchInFlight).toBeGreaterThan(1); // still parallel… + expect(mocks.maxDispatchInFlight).toBeLessThanOrEqual(4); // …but windowed + }); + + it("versions on Security in place, tags on Classification — workspace never grows past 61", async () => { await runUntilSettled(FILE_COUNT * 2); - // 122 deliveries, all silent (background), none via the disruptive path. - expect(mocks.consumeSilentCalls).toBe(FILE_COUNT * 2); + // Only the 61 Security runs fork a version, and every one silently in place. + expect(mocks.consumeSilentCalls).toBe(FILE_COUNT); expect(mocks.consumeNonSilentCalls).toBe(0); + // Classification never forks a version — it only stamps labels onto the stub. + expect(mocks.updateStirlingFileStub).toHaveBeenCalledTimes(FILE_COUNT); + for (const call of mocks.updateStirlingFileStub.mock.calls) { + expect(call[1]).toEqual({ classificationLabels: ["Invoice"] }); + } // Never added as brand-new files either. expect(mocks.addFilesCalls).toBe(0); - // In-place versioning: each file replaced twice, count unchanged. + // In-place versioning + metadata-only tagging: count unchanged. expect(mocks.workspace).toHaveLength(FILE_COUNT); }); @@ -273,8 +294,8 @@ describe("policy auto-run — 61-file batch through a Classification → Securit ); }); - // Still fully processed (chain intact), but delivered to STORAGE, never - // re-added to the workbench — the workspace stays empty. + // Still fully processed (chain intact), but Security's versions went to + // STORAGE, never re-added to the workbench — the workspace stays empty. expect(latestRuns).toHaveLength(FILE_COUNT * 2); expect(mocks.workspace).toHaveLength(0); expect(mocks.consumeSilentCalls).toBe(0); diff --git a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.race.test.tsx b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.race.test.tsx new file mode 100644 index 0000000000..b67648e7fc --- /dev/null +++ b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.race.test.tsx @@ -0,0 +1,293 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; + +/** + * Mid-run race: classification is in flight (its labelled output is still + * downloading) when the user manually runs a tool on the same file — e.g. + * quickly redacting it — which consumes the input and forks a new leaf. + * + * The label targets must be resolved AT WRITE TIME (after the download/parse + * window), not snapshotted at run completion: a stale snapshot points at the + * consumed id, no-ops, and silently loses the labels — the file then shows the + * classification badge (provenance-resolved) but never gets its labels. + */ + +const mocks = vi.hoisted(() => ({ + workspace: [] as Array<{ + id: string; + sourceFileIds?: string[]; + derivedFromTool?: boolean; + classificationLabels?: string[]; + }>, + runStoredPolicy: vi.fn(), + getPolicyRun: vi.fn(), + listPolicyRuns: vi.fn(), + downloadPolicyOutput: vi.fn(), + getStirlingFile: vi.fn(), + getStirlingFileStub: vi.fn(), + persistVersionedOutputs: vi.fn(), + updateFileMetadata: vi.fn(), + createStirlingFilesAndStubs: vi.fn(), + addFiles: vi.fn(), + updateStirlingFileStub: vi.fn(), + consumeFiles: vi.fn(), + bumpRevision: vi.fn(), +})); + +// Classification chains server-side only when the AI engine is on (else it runs +// client-side); this race is in the server import path, so force the engine on. +vi.mock("@app/hooks/useAiEngineEnabled", () => ({ + useAiEngineEnabled: () => true, +})); +vi.mock("@app/contexts/FileContext", () => ({ + useAllFiles: () => ({ fileStubs: mocks.workspace }), + useFileManagement: () => ({ + addFiles: mocks.addFiles, + updateStirlingFileStub: mocks.updateStirlingFileStub, + }), + useFileContext: () => ({ consumeFiles: mocks.consumeFiles }), +})); +vi.mock("@app/contexts/IndexedDBContext", () => ({ + useIndexedDB: () => ({ bumpRevision: mocks.bumpRevision }), +})); +vi.mock("@app/hooks/usePolicies", () => ({ + usePolicies: () => ({ + policies: { + classification: { + configured: true, + status: "active", + backendId: "backend-classification", + runOn: "upload", + order: 0, + outputMode: "new_version", + outputName: "", + }, + }, + }), +})); +vi.mock("@app/services/policyApi", () => ({ + runStoredPolicy: mocks.runStoredPolicy, + getPolicyRun: mocks.getPolicyRun, + listPolicyRuns: mocks.listPolicyRuns, + downloadPolicyOutput: mocks.downloadPolicyOutput, + resolvePolicyRunTarget: () => "saas", +})); +vi.mock("@app/services/fileStorage", () => ({ + fileStorage: { + getStirlingFile: mocks.getStirlingFile, + getStirlingFileStub: mocks.getStirlingFileStub, + persistVersionedOutputs: mocks.persistVersionedOutputs, + updateFileMetadata: mocks.updateFileMetadata, + }, +})); +vi.mock("@app/services/fileStubHelpers", () => ({ + createStirlingFilesAndStubs: mocks.createStirlingFilesAndStubs, +})); +vi.mock("@app/services/fileClassification", () => ({ + readClassificationLabelsFromFile: vi.fn().mockResolvedValue(["Invoice"]), +})); + +import { usePolicyAutoRun } from "@app/components/policies/usePolicyAutoRun"; +import { + usePolicyRuns, + resetPolicyRuns, +} from "@app/components/policies/policyRunStore"; +import type { PolicyRunRecord } from "@app/components/policies/policyRunStore"; + +let latestRuns: PolicyRunRecord[] = []; +function Harness() { + usePolicyAutoRun(); + latestRuns = usePolicyRuns(); + return null; +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((r) => { + resolve = r; + }); + return { promise, resolve }; +} + +beforeEach(() => { + localStorage.clear(); + resetPolicyRuns(); + vi.clearAllMocks(); + + mocks.workspace = [{ id: "file-0" }]; + + mocks.listPolicyRuns.mockResolvedValue([]); + mocks.getStirlingFile.mockResolvedValue( + new File(["x"], "doc.pdf", { type: "application/pdf" }), + ); + mocks.getStirlingFileStub.mockResolvedValue(null); + mocks.updateFileMetadata.mockResolvedValue(true); + // Apply stub updates to the shared workspace, as the real reducer does — the + // label stamp's second pass reads them back to stay idempotent. + mocks.updateStirlingFileStub.mockImplementation( + (id: string, updates: Record) => { + const stub = mocks.workspace.find((s) => s.id === id); + if (stub) Object.assign(stub, updates); + }, + ); + mocks.runStoredPolicy.mockResolvedValue("run-0"); + mocks.getPolicyRun.mockResolvedValue({ + runId: "run-0", + policyId: null, + status: "COMPLETED", + currentStep: 1, + stepCount: 1, + error: null, + outputs: [{ fileId: "backend-out-0", fileName: "doc.pdf" }], + }); +}); + +async function settleImport(timeout = 8000) { + await act(async () => { + await vi.waitFor( + () => { + expect(latestRuns.filter((r) => r.imported)).toHaveLength(1); + }, + { timeout, interval: 20 }, + ); + }); +} + +describe("classification vs a mid-run manual tool edit", () => { + it("labels land on the forked leaf when a tool consumes the file during the label download", async () => { + // The classified output's download hangs until we release it — this is the + // async window the user's edit slips into. + const download = deferred(); + mocks.downloadPolicyOutput.mockReturnValue(download.promise); + + const { rerender } = renderHook(() => Harness()); + + // Run dispatched, completed, import started — now hanging in the window. + await act(async () => { + await vi.waitFor( + () => expect(mocks.downloadPolicyOutput).toHaveBeenCalled(), + { timeout: 8000, interval: 20 }, + ); + }); + + // User quickly redacts: the tool consumes file-0 and forks a new leaf. + // (derivedFromTool + sourceFileIds are what CONSUME_FILES stamps.) + act(() => { + mocks.workspace = [ + { + id: "file-0~redacted", + sourceFileIds: ["file-0"], + derivedFromTool: true, + }, + ]; + rerender(); + }); + + // The download finally lands. + download.resolve(new Blob(["x"], { type: "application/pdf" })); + await settleImport(); + + // Labels stamped onto the LIVE leaf, not no-oped on the consumed id. + const stampedIds = mocks.updateStirlingFileStub.mock.calls.map((c) => c[0]); + expect(stampedIds).toEqual(["file-0~redacted"]); + expect(mocks.updateStirlingFileStub).toHaveBeenCalledWith( + "file-0~redacted", + { classificationLabels: ["Invoice"] }, + ); + // Badge persists on the leaf: the run's outputFileIds are the tagged files. + expect(latestRuns[0].outputFileIds).toEqual(["file-0~redacted"]); + }); + + it("control: with no mid-run edit, labels land on the original file", async () => { + mocks.downloadPolicyOutput.mockResolvedValue( + new Blob(["x"], { type: "application/pdf" }), + ); + + renderHook(() => Harness()); + await settleImport(); + + expect(mocks.updateStirlingFileStub).toHaveBeenCalledWith("file-0", { + classificationLabels: ["Invoice"], + }); + expect(latestRuns[0].outputFileIds).toEqual(["file-0"]); + }); + + it("stamps the forked leaf when the consume lands in the same frame as the first stamp", async () => { + // Tighter than the case above: the consume is dispatched but hasn't rendered + // when the labels are stamped, so the workspace snapshot still shows file-0 + // and that stamp no-ops against the real reducer. The post-commit second pass + // is what saves the labels. + mocks.downloadPolicyOutput.mockResolvedValue( + new Blob(["x"], { type: "application/pdf" }), + ); + mocks.updateStirlingFileStub.mockImplementation((id: string) => { + // file-0 is already consumed, so its stamp is lost (no Object.assign) and + // the forked leaf only becomes visible afterwards. Mutate the workspace in + // place: the hook holds it by ref, which is what the second pass re-reads. + if (id === "file-0") { + mocks.workspace.splice(0, mocks.workspace.length, { + id: "file-0~redacted", + sourceFileIds: ["file-0"], + }); + return; + } + const stub = mocks.workspace.find((s) => s.id === id); + if (stub) Object.assign(stub, { classificationLabels: ["Invoice"] }); + }); + + renderHook(() => Harness()); + await settleImport(); + + const stampedIds = mocks.updateStirlingFileStub.mock.calls.map((c) => c[0]); + expect(stampedIds).toEqual(["file-0", "file-0~redacted"]); + // The leaf becoming visible also queues its own classification run, so pick + // the settled one rather than assuming an index. + const imported = latestRuns.find((r) => r.imported); + expect(imported?.outputFileIds).toContain("file-0~redacted"); + }); +}); + +// The label read backs off between attempts (2s, then 4s), so these run on fake +// timers — sleeping for real would hold a worker long enough to starve the suite. +describe("classification label-read failures", () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + async function settleOnFakeTime(maxMs = 30_000) { + for (let elapsed = 0; elapsed < maxMs; elapsed += 250) { + if (latestRuns.some((r) => r.imported)) return; + await act(async () => { + await vi.advanceTimersByTimeAsync(250); + }); + } + throw new Error("classification run never settled"); + } + + it("retries a transient failure instead of leaving the run unsettled", async () => { + // The import effect only re-runs when the run store changes, so bailing out + // on a transient failure would leave this run "running" forever. + mocks.downloadPolicyOutput + .mockRejectedValueOnce(new Error("network blip")) + .mockResolvedValue(new Blob(["x"], { type: "application/pdf" })); + + renderHook(() => Harness()); + await settleOnFakeTime(); + + expect(mocks.downloadPolicyOutput).toHaveBeenCalledTimes(2); + expect(mocks.updateStirlingFileStub).toHaveBeenCalledWith("file-0", { + classificationLabels: ["Invoice"], + }); + }); + + it("settles a run whose labels never become readable", async () => { + // Permanent failure: give up after the retry budget and settle unlabelled, + // rather than spinning the file's "running" pill indefinitely. + mocks.downloadPolicyOutput.mockRejectedValue(new Error("network down")); + + renderHook(() => Harness()); + await settleOnFakeTime(); + + expect(mocks.updateStirlingFileStub).not.toHaveBeenCalled(); + expect(latestRuns.find((r) => r.imported)?.outputFileIds).toEqual([]); + }); +}); diff --git a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.retry.test.tsx b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.retry.test.tsx index c25f80ce65..3c54e5cc42 100644 --- a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.retry.test.tsx +++ b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.retry.test.tsx @@ -69,8 +69,17 @@ afterEach(() => vi.useRealTimers()); describe("auto-run queue-rejection retry", () => { it("relabels a queue-rejected run as retrying, then re-dispatches it in place", async () => { - // The polled run comes back queue-rejected; the retry resolves the file + fires a fresh run. - getRunApi.mockResolvedValue(queueFullView); + // The polled run comes back queue-rejected once; the retry resolves the file + // and fires a fresh run, whose own polls then see it genuinely running. + getRunApi.mockResolvedValueOnce(queueFullView).mockResolvedValue({ + runId: "run-2", + status: "RUNNING", + currentStep: 1, + stepCount: 2, + error: null, + errorCode: null, + outputs: [], + } as never); getFile.mockResolvedValue({ size: 1234 } as never); runStored.mockResolvedValue("run-2"); @@ -92,19 +101,20 @@ describe("auto-run queue-rejection retry", () => { return usePolicyRuns(); }); - // First poll (2s cadence) sees the rejection → relabel as a soft "retrying" row. + // First poll sees the rejection → relabel as a soft "retrying" row. await act(async () => { await vi.advanceTimersByTimeAsync(2000); }); expect(getRun("run-1")?.retrying).toBe(true); expect(runStored).not.toHaveBeenCalled(); - // After the first backoff window (BASE 4s) the rejected record is dropped and a fresh run fires. + // After the first backoff window (BASE 4s) the rejected record is dropped and + // a fresh run fires; its own first poll shows it genuinely running. await act(async () => { - await vi.advanceTimersByTimeAsync(4000); + await vi.advanceTimersByTimeAsync(6000); }); expect(runStored).toHaveBeenCalledWith("backend-1", [{ size: 1234 }]); expect(getRun("run-1")).toBeUndefined(); - expect(getRun("run-2")?.status).toBe("PENDING"); + expect(getRun("run-2")?.status).toBe("RUNNING"); }); }); diff --git a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts index ca096b6792..e475d784ea 100644 --- a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts +++ b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts @@ -39,6 +39,11 @@ import { dispatchPaygLimitReached } from "@app/services/usageLimitBridge"; import type { FileId } from "@app/types/file"; import { createStirlingFilesAndStubs } from "@app/services/fileStubHelpers"; import { readClassificationLabelsFromFile } from "@app/services/fileClassification"; +import { isClassificationCategory } from "@app/data/policyCategories"; +import { + acquireDispatchSlot, + releaseDispatchSlot, +} from "@app/components/policies/dispatchSemaphore"; import type { StirlingFile, StirlingFileStub } from "@app/types/fileContext"; import type { PoliciesByCategory } from "@app/types/policies"; import { usePolicies } from "@app/hooks/usePolicies"; @@ -59,6 +64,10 @@ import { /** Status poll cadence. */ const POLL_MS = 2000; +/** First poll fires early so a fresh run shows real progress quickly instead of + * sitting on an indeterminate spinner for a full poll interval. */ +const FIRST_POLL_MS = 500; + /** The server aborts any single tool step that runs longer than its internal-API * read timeout, then fails the run — so a run can legitimately stay in flight * for up to this long per step. The client must keep polling at least that long, @@ -175,7 +184,14 @@ export function usePolicyAutoRun(): void { // Classification policy out of the server chain when the AI engine is off. !(id === "classification" && !aiEnabled), ) - .sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0)) + // Classification runs last: it's non-blocking, so an enforcement policy + // running after it would fork a new version and drop the user's edits. + .sort(([idA, a], [idB, b]) => { + const ca = isClassificationCategory(idA) ? 1 : 0; + const cb = isClassificationCategory(idB) ? 1 : 0; + if (ca !== cb) return ca - cb; + return (a.order ?? 0) - (b.order ?? 0); + }) .map(([id]) => id), [policies, aiEnabled], ); @@ -310,6 +326,7 @@ export function usePolicyAutoRun(): void { backendId, outputId as FileId, run.fileName, + true, // chained → jump the dispatch queue ahead of new files ).catch(() => {}); } } @@ -330,15 +347,33 @@ export function usePolicyAutoRun(): void { // so the enforced file appears in the app rather than only on the backend. useEffect(() => { for (const run of runs) { + const classification = isClassificationCategory(run.categoryId); if ( run.status !== "COMPLETED" || run.imported || - !run.outputs?.length || - importing.current.has(run.runId) + importing.current.has(run.runId) || + // Classification settles even with no outputs (nothing to tag); other + // policies need an output to import. + (!run.outputs?.length && !classification) ) { continue; } importing.current.add(run.runId); + // Classification is metadata-only: stamp labels onto the current leaf of + // the file it ran on (no version fork). See importClassificationLabels. + if (classification) { + // Targets are resolved by importClassificationLabels AT WRITE TIME (not + // snapshotted here): its download/parse is an async window during which + // a manual tool run can consume the input and fork a new leaf, and a + // stale snapshot would no-op on the dead id and lose the labels. + void importClassificationLabels( + run, + () => + classificationLabelTargetStubs(run.fileId, fileStubsRef.current), + { updateStirlingFileStub, bumpRevision }, + ).finally(() => importing.current.delete(run.runId)); + continue; + } // Honour the policy's output mode: a new file, or a new version of the // input file it ran on (needs that input's stub, still in the workspace). const outputMode = policies[run.categoryId]?.outputMode ?? "new_version"; @@ -501,6 +536,142 @@ function categoryForPolicy( )?.[0]; } +interface ClassificationImportContext { + updateStirlingFileStub: ( + fileId: FileId, + updates: Partial, + ) => void; + bumpRevision: () => void; +} + +/** Workspace stubs to tag with a classification run's labels: the file it ran + * on plus any live descendants, so an edit made during the async run (which + * forks a new leaf) still shows the tags. Empty once the document has left the + * workspace (closed, or a reconciled run with no local input link). */ +export function classificationLabelTargetStubs( + runFileId: string, + stubs: ReadonlyArray, +): StirlingFileStub[] { + return stubs.filter( + (s) => + (s.id as string) === runFileId || + s.parentFileId === runFileId || + s.sourceFileIds?.includes(runFileId as FileId), + ); +} + +/** Attempts to read a completed run's labels before giving up, and the backoff + * between them (delay × attempt). The import effect only re-runs when the run + * store changes, so a transient read failure has to be retried HERE: bailing + * out would leave the run unsettled and the file's "running" pill spinning + * until unrelated policy activity happened to nudge the effect. */ +const LABEL_READ_ATTEMPTS = 3; +const LABEL_READ_RETRY_MS = 2000; + +/** + * Read classification labels out of a completed run's output PDF. A 404 means + * that output aged out, so it's skipped; any other failure is transient and + * retried with backoff. Returns null when there are genuinely no labels to + * apply (including a run with no outputs), so the caller can settle the run. + */ +async function readRunLabels(run: PolicyRunRecord): Promise { + for (let attempt = 0; attempt < LABEL_READ_ATTEMPTS; attempt++) { + if (attempt > 0) await delay(LABEL_READ_RETRY_MS * attempt); + let transientFailure = false; + for (const out of run.outputs) { + try { + const blob = await downloadPolicyOutput(out.fileId, run.target); + const file = new File([blob], out.fileName ?? run.fileName, { + type: blob.type || "application/pdf", + }); + const labels = await readClassificationLabelsFromFile(file); + if (labels && labels.length > 0) return labels; + } catch (err) { + if (!isNotFoundError(err)) transientFailure = true; + } + } + // Every output was read (or had aged out): there are no labels to apply. + if (!transientFailure) return null; + } + // Out of attempts. Settle the run unlabelled rather than spin forever; the + // file keeps its classification badge, just without tags. + return null; +} + +/** + * Stamp `labels` onto the run's live descendants in place (workspace + storage) + * — no versioned child, no history entry, only tags. Returns the tagged ids. + * + * Runs twice, because `resolveTargets` reads a rendered snapshot of the + * workspace: a CONSUME_FILES that was dispatched but not yet rendered when the + * first pass ran leaves its target already gone by the time UPDATE_FILE_RECORD + * is processed, so that stamp no-ops and the labels would be silently lost. The + * second pass sees the forked leaf and tags it. Each id is stamped at most once + * across both passes, so the pass costs nothing when no consume raced. + */ +async function stampClassificationLabels( + labels: string[], + resolveTargets: () => StirlingFileStub[], + ctx: ClassificationImportContext, +): Promise { + const updates = { classificationLabels: labels }; + const tagged = new Set(); + + for (let pass = 0; pass < 2; pass++) { + // Resolve and stamp the store in one synchronous block — no await between + // them, so a target can't be consumed in between. A consume AFTER the stamp + // is safe too: the CONSUME_FILES reducer carries classificationLabels onto + // the new leaf. + const fresh = resolveTargets().filter((s) => !tagged.has(s.id)); + for (const stub of fresh) { + tagged.add(stub.id); + ctx.updateStirlingFileStub(stub.id, updates); + } + + let mutated = false; + for (const stub of fresh) { + if (await fileStorage.updateFileMetadata(stub.id, updates)) + mutated = true; + } + if (mutated) ctx.bumpRevision(); + + // Yield a macrotask so React processes this pass's stamps (and any consume + // that raced them) before the next pass re-resolves. + if (pass === 0) await new Promise((resolve) => setTimeout(resolve)); + } + return Array.from(tagged); +} + +/** + * Deliver a classification run: read its labels and tag the live document with + * them. Metadata-only — nothing is versioned. + */ +async function importClassificationLabels( + run: PolicyRunRecord, + resolveTargets: () => StirlingFileStub[], + ctx: ClassificationImportContext, +): Promise { + if (resolveTargets().length === 0) { + // The document left the workspace (closed, or a server-reconciled run with + // no local input link) — nothing to tag. + updateRun(run.runId, { imported: true }); + return; + } + const labels = await readRunLabels(run); + const targetIds = + labels && labels.length > 0 + ? await stampClassificationLabels(labels, resolveTargets, ctx) + : []; + // Settle either way so it stops re-importing. outputFileIds are the TAGGED + // workspace files (no forked version), so their policy badge persists. Safe + // to chain-key on: classification is always last, so nothing chains off it. + updateRun(run.runId, { + imported: true, + importedFileIds: run.outputs.map((o) => o.fileId), + outputFileIds: targetIds, + }); +} + /** * Fetch a completed run's not-yet-imported output files and deliver them to the * workspace. Per-output, via allSettled: each output is tracked once delivered, @@ -737,6 +908,9 @@ async function runPolicyOnFile( backendId: string, fileId: FileId, fileName: string, + // Chained (downstream) dispatch — jumps the dispatch queue so a file mid-chain + // finishes its flow before new files start (see acquireDispatchSlot). + priority = false, ): Promise { // A freshly-uploaded file's bytes are written to IndexedDB asynchronously, so // its stub can appear in the file list a beat before getStirlingFile resolves @@ -762,6 +936,9 @@ async function runPolicyOnFile( markDispatched(categoryId, fileId); return; } + // Bounded upload window — see MAX_CONCURRENT_DISPATCHES. Only the POST is + // gated; the IDB wait above never holds a slot. + await acquireDispatchSlot(priority); try { const target = resolvePolicyRunTarget(); const runId = await runStoredPolicy(backendId, [file]); @@ -783,6 +960,8 @@ async function runPolicyOnFile( // the absent run simply won't appear in the activity feed. If the backend did // start a run we never recorded, reconcileServerRuns rediscovers it. markDispatched(categoryId, fileId); + } finally { + releaseDispatchSlot(); } } @@ -803,8 +982,10 @@ export async function poll( // would quit while a long step is still legitimately running. let budgetMs = DEFAULT_STEP_COUNT * STEP_TIMEOUT_MS + POLL_GRACE_MS; const startedAt = Date.now(); + let nextDelayMs = FIRST_POLL_MS; while (Date.now() - startedAt < budgetMs) { - await delay(POLL_MS); + await delay(nextDelayMs); + nextDelayMs = POLL_MS; let view; try { view = await getPolicyRun(runId); diff --git a/frontend/editor/src/proprietary/components/shared/PolicyEnforcingOverlay.tsx b/frontend/editor/src/proprietary/components/shared/PolicyEnforcingOverlay.tsx index 45dea92070..048bca15ac 100644 --- a/frontend/editor/src/proprietary/components/shared/PolicyEnforcingOverlay.tsx +++ b/frontend/editor/src/proprietary/components/shared/PolicyEnforcingOverlay.tsx @@ -11,6 +11,7 @@ import { import { ActionIcon } from "@app/ui/ActionIcon"; import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; import CloseIcon from "@mui/icons-material/Close"; +import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon"; import { useTranslation } from "react-i18next"; interface PolicyEnforcingOverlayProps { @@ -23,6 +24,9 @@ interface PolicyEnforcingOverlayProps { /** CSS colour var of the enforcing policy's accent (e.g. `var(--color-orange)`), * so the icon/spinner match that policy's badge instead of a fixed blue. */ accentVar?: string; + /** Category of the enforcing policy — picks its shared icon (shield for + * security, label for classification, …); generic shield when unknown. */ + categoryId?: string; } /** @@ -35,6 +39,7 @@ export function PolicyEnforcingOverlay({ zIndex = 200, onDismiss, accentVar, + categoryId, }: PolicyEnforcingOverlayProps) { const { t } = useTranslation(); if (!enforcing) return null; @@ -87,7 +92,11 @@ export function PolicyEnforcingOverlay({ : undefined } > - + {categoryId ? ( + policyCategoryIcon(categoryId, { fontSize: 26 }) + ) : ( + + )} {t("policy.enforcingTitle", "Enforcing policy…")} diff --git a/frontend/editor/src/proprietary/components/viewer/PolicyEnforcementOverlay.tsx b/frontend/editor/src/proprietary/components/viewer/PolicyEnforcementOverlay.tsx index 75fa9cb952..71d5294f2d 100644 --- a/frontend/editor/src/proprietary/components/viewer/PolicyEnforcementOverlay.tsx +++ b/frontend/editor/src/proprietary/components/viewer/PolicyEnforcementOverlay.tsx @@ -71,6 +71,7 @@ export function PolicyEnforcementOverlay({ runs }: Props) { progress={progress} onDismiss={() => setDismissed(true)} accentVar={policyAccentVar(inFlight.categoryId)} + categoryId={inFlight.categoryId} /> ); } diff --git a/frontend/editor/src/proprietary/components/viewer/Viewer.tsx b/frontend/editor/src/proprietary/components/viewer/Viewer.tsx index e1d0cd96ab..1f04ea22ca 100644 --- a/frontend/editor/src/proprietary/components/viewer/Viewer.tsx +++ b/frontend/editor/src/proprietary/components/viewer/Viewer.tsx @@ -8,6 +8,7 @@ import { usePolicyRuns, type PolicyRunRecord, } from "@app/components/policies/policyRunStore"; +import { isClassificationCategory } from "@app/data/policyCategories"; import { PolicyEnforcementOverlay } from "@app/components/viewer/PolicyEnforcementOverlay"; type SignatureOverlayPassThrough = Pick< @@ -29,6 +30,8 @@ const Viewer = (props: ViewerProps & SignatureOverlayPassThrough) => { ? allRuns.filter( (r: PolicyRunRecord) => r.fileId === activeFileId && + // Classification runs async and must never block the viewer. + !isClassificationCategory(r.categoryId) && (POLICY_IN_FLIGHT_STATUSES.includes(r.status) || r.retrying === true), ) : []; diff --git a/frontend/editor/src/proprietary/data/policyCategories.test.ts b/frontend/editor/src/proprietary/data/policyCategories.test.ts new file mode 100644 index 0000000000..4b4fca2b7e --- /dev/null +++ b/frontend/editor/src/proprietary/data/policyCategories.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { + isClassificationCategory, + pinClassificationLast, +} from "@app/data/policyCategories"; + +describe("isClassificationCategory", () => { + it("recognises the classification category and nothing else", () => { + expect(isClassificationCategory("classification")).toBe(true); + expect(isClassificationCategory("security")).toBe(false); + expect(isClassificationCategory("")).toBe(false); + }); +}); + +describe("pinClassificationLast", () => { + it("moves classification to the end, preserving other order", () => { + expect( + pinClassificationLast(["classification", "security", "compliance"]), + ).toEqual(["security", "compliance", "classification"]); + }); + + it("leaves an order without classification untouched", () => { + expect(pinClassificationLast(["security", "compliance"])).toEqual([ + "security", + "compliance", + ]); + }); + + it("is a no-op when classification is already last", () => { + expect(pinClassificationLast(["security", "classification"])).toEqual([ + "security", + "classification", + ]); + }); + + it("handles classification as the only policy", () => { + expect(pinClassificationLast(["classification"])).toEqual([ + "classification", + ]); + }); +}); diff --git a/frontend/editor/src/proprietary/data/policyCategories.ts b/frontend/editor/src/proprietary/data/policyCategories.ts new file mode 100644 index 0000000000..820a93366b --- /dev/null +++ b/frontend/editor/src/proprietary/data/policyCategories.ts @@ -0,0 +1,21 @@ +/** The classification policy's catalog category id. */ +export const CLASSIFICATION_CATEGORY_ID = "classification"; + +/** + * Classification is metadata-only: it runs async (never blocks), never forks a + * version, and always runs last. This predicate gates that special handling. + */ +export function isClassificationCategory(categoryId: string): boolean { + return categoryId === CLASSIFICATION_CATEGORY_ID; +} + +/** + * Move classification to the end of an execution order (others keep their order), + * so a persisted/displayed order can't place it anywhere but last. + */ +export function pinClassificationLast(orderedCategoryIds: string[]): string[] { + return [ + ...orderedCategoryIds.filter((id) => !isClassificationCategory(id)), + ...orderedCategoryIds.filter((id) => isClassificationCategory(id)), + ]; +} diff --git a/frontend/editor/src/proprietary/hooks/usePolicies.ts b/frontend/editor/src/proprietary/hooks/usePolicies.ts index 8043e6b66a..389f98cd65 100644 --- a/frontend/editor/src/proprietary/hooks/usePolicies.ts +++ b/frontend/editor/src/proprietary/hooks/usePolicies.ts @@ -35,6 +35,7 @@ import { removePolicy, } from "@app/services/policyBackend"; import { reorderPolicies as reorderBackendPolicies } from "@app/services/policyApi"; +import { pinClassificationLast } from "@app/data/policyCategories"; import type { PolicyToStore } from "@app/services/policyPipeline"; import type { PoliciesByCategory, @@ -326,9 +327,12 @@ export function usePolicies() { * first for an instant re-render; the next reconcile re-reads the server order. */ const reorderPolicies = useCallback((orderedCategoryIds: string[]) => { - persistPolicyOrder(orderedCategoryIds); + // Pin classification last so the persisted/server order matches execution + // (it always runs last — see usePolicyAutoRun). + const ordered = pinClassificationLast(orderedCategoryIds); + persistPolicyOrder(ordered); const current = loadPolicies(); - const backendIds = orderedCategoryIds + const backendIds = ordered .map((categoryId) => current[categoryId]?.backendId) .filter((id): id is string => !!id); if (backendIds.length > 0) { diff --git a/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.test.ts b/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.test.ts index c5d8b9952b..2364321213 100644 --- a/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.test.ts +++ b/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.test.ts @@ -1,11 +1,14 @@ import { describe, it, expect } from "vitest"; -import { buildPolicyBadgeMap } from "@app/hooks/usePolicyFileBadges"; +import { + buildPolicyBadgeMap, + reusePolicyBadgeArrays, +} from "@app/hooks/usePolicyFileBadges"; import type { PolicyRunRecord } from "@app/components/policies/policyRunStore"; -const NOW = 1_000_000; const labels = new Map([ ["security", "Security"], ["watermark", "Watermark"], + ["classification", "Classification"], ]); function run(overrides: Partial): PolicyRunRecord { @@ -20,29 +23,24 @@ function run(overrides: Partial): PolicyRunRecord { outputs: [], outputFileIds: ["out"], error: null, - startedAt: NOW - 1_000, // recent by default + startedAt: 0, ...overrides, }; } describe("buildPolicyBadgeMap — badge follows the document onto derived files", () => { - it("badges a policy's direct output, and marks it recent within the window", () => { - const map = buildPolicyBadgeMap([run({})], [{ id: "out" }], labels, NOW); - const badges = map.get("out") ?? []; - expect(badges.map((b) => b.id)).toEqual(["security"]); - expect(badges[0].recent).toBe(true); + it("badges a policy's direct output", () => { + const map = buildPolicyBadgeMap([run({})], [{ id: "out" }], labels); + expect((map.get("out") ?? []).map((b) => b.id)).toEqual(["security"]); }); - it("a versioned edit inherits the badge via parentFileId (never glows)", () => { + it("a versioned edit inherits the badge via parentFileId", () => { const map = buildPolicyBadgeMap( [run({})], [{ id: "out" }, { id: "edit", parentFileId: "out" }], labels, - NOW, ); - const edit = map.get("edit") ?? []; - expect(edit.map((b) => b.id)).toEqual(["security"]); - expect(edit[0].recent).toBe(false); + expect((map.get("edit") ?? []).map((b) => b.id)).toEqual(["security"]); }); it("SPLIT parts inherit the badge via sourceFileIds, though they have no parent", () => { @@ -56,11 +54,9 @@ describe("buildPolicyBadgeMap — badge follows the document onto derived files" { id: "part2", sourceFileIds: ["out"] }, ], labels, - NOW, ); expect((map.get("part1") ?? []).map((b) => b.id)).toEqual(["security"]); expect((map.get("part2") ?? []).map((b) => b.id)).toEqual(["security"]); - expect((map.get("part1") ?? [])[0].recent).toBe(false); }); it("resolves transitively when an intermediate edit was consumed/removed", () => { @@ -70,7 +66,6 @@ describe("buildPolicyBadgeMap — badge follows the document onto derived files" [run({})], [{ id: "part", sourceFileIds: ["editGone", "out"] }], labels, - NOW, ); expect((map.get("part") ?? []).map((b) => b.id)).toEqual(["security"]); }); @@ -83,7 +78,6 @@ describe("buildPolicyBadgeMap — badge follows the document onto derived files" ], [{ id: "merged", sourceFileIds: ["a", "b"] }], labels, - NOW, ); expect((map.get("merged") ?? []).map((b) => b.id).sort()).toEqual([ "security", @@ -96,24 +90,33 @@ describe("buildPolicyBadgeMap — badge follows the document onto derived files" [run({})], [{ id: "out" }, { id: "unrelated", sourceFileIds: ["someUpload"] }], labels, - NOW, ); expect(map.has("unrelated")).toBe(false); }); - it("inherited badges never glow even when the source run is recent", () => { + it("a completed classification run badges the files it tagged", () => { + // Classification is metadata-only: its outputFileIds are the tagged + // workspace files (no forked version), so the label badge persists there. const map = buildPolicyBadgeMap( - [run({ startedAt: NOW })], // maximally recent - [{ id: "out" }, { id: "part", sourceFileIds: ["out"] }], + [ + run({ + categoryId: "classification", + fileId: "in", + outputFileIds: ["in"], + imported: true, + }), + ], + [{ id: "in" }], labels, - NOW, ); - expect((map.get("out") ?? [])[0].recent).toBe(true); - expect((map.get("part") ?? [])[0].recent).toBe(false); + const badges = map.get("in") ?? []; + expect(badges.map((b) => b.id)).toEqual(["classification"]); + expect(badges[0].enforcing).toBeUndefined(); + expect(badges[0].background).toBeUndefined(); }); }); -describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", () => { +describe("buildPolicyBadgeMap — in-flight indicators", () => { const enforcingOn = ( map: Map, id: string, @@ -124,7 +127,6 @@ describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", ( [run({ status: "RUNNING", outputFileIds: [] })], [{ id: "in" }], labels, - NOW, ); expect(enforcingOn(map, "in")).toBe(true); }); @@ -136,7 +138,6 @@ describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", ( [run({ status: "COMPLETED" })], [{ id: "in" }], labels, - NOW, ); expect(enforcingOn(before, "in")).toBe(true); @@ -144,7 +145,6 @@ describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", ( [run({ status: "COMPLETED", imported: true })], [{ id: "in" }], labels, - NOW, ); expect(enforcingOn(after, "in")).toBe(false); }); @@ -155,7 +155,6 @@ describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", ( [run({ status, outputFileIds: [] })], [{ id: "in" }], labels, - NOW, ); expect(enforcingOn(map, "in")).toBe(false); } @@ -166,7 +165,6 @@ describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", ( [run({ status: "FAILED", retrying: true, outputFileIds: [] })], [{ id: "in" }], labels, - NOW, ); expect(enforcingOn(map, "in")).toBe(true); }); @@ -176,8 +174,97 @@ describe("buildPolicyBadgeMap — enforcing spinner while a run is in flight", ( [run({ status: "RUNNING", fileId: "", outputFileIds: [] })], [{ id: "in" }], labels, - NOW, ); expect(enforcingOn(map, "in")).toBe(false); }); + + it("an in-flight classification run is background, never enforcing", () => { + // Non-blocking: shows a spinner but must never trip the enforcing flag + // that gates actions and overlays. + const map = buildPolicyBadgeMap( + [ + run({ + categoryId: "classification", + status: "RUNNING", + outputFileIds: [], + }), + ], + [{ id: "in" }], + labels, + ); + const badges = map.get("in") ?? []; + expect(badges.map((b) => b.id)).toEqual(["classification"]); + expect(badges[0].background).toBe(true); + expect(enforcingOn(map, "in")).toBe(false); + }); +}); + +describe("reusePolicyBadgeArrays — per-file identity across rebuilds", () => { + // buildPolicyBadgeMap allocates fresh arrays every call and the run store hands + // back a new `runs` array on every status poll, so without this the memoized + // sidebar rows get a new `policies` prop for EVERY badged file on each tick. + const build = (runs: PolicyRunRecord[], stubs: { id: string }[]) => + buildPolicyBadgeMap(runs, stubs, labels); + + const twoFiles = [{ id: "a" }, { id: "b" }]; + // Settled + imported, so the badge is a plain one (a COMPLETED run keeps + // `enforcing` until its outputs land — see the in-flight tests above). + const settled = (id: string) => + run({ + runId: `r${id}`, + fileId: id, + outputFileIds: [id], + status: "COMPLETED", + imported: true, + }); + const bothSettled = () => [settled("a"), settled("b")]; + + it("returns the same map when nothing changed", () => { + const first = build(bothSettled(), twoFiles); + const second = reusePolicyBadgeArrays( + first, + build(bothSettled(), twoFiles), + ); + expect(second).toBe(first); + }); + + it("keeps the untouched file's array identity when another file changes", () => { + const first = build(bothSettled(), twoFiles); + // "a" goes in-flight; "b" is unaffected and must keep its exact array. + const next = build( + [ + run({ + runId: "ra", + fileId: "a", + outputFileIds: ["a"], + status: "RUNNING", + }), + settled("b"), + ], + twoFiles, + ); + const second = reusePolicyBadgeArrays(first, next); + expect(second).not.toBe(first); + expect(second.get("b")).toBe(first.get("b")); + expect(second.get("a")).not.toBe(first.get("a")); + expect((second.get("a") ?? [])[0].enforcing).toBe(true); + expect((first.get("a") ?? [])[0].enforcing).toBeUndefined(); + }); + + it("a new badged file doesn't disturb the existing files' arrays", () => { + const first = build(bothSettled(), twoFiles); + const next = build( + [...bothSettled(), settled("c")], + [...twoFiles, { id: "c" }], + ); + const second = reusePolicyBadgeArrays(first, next); + expect(second.get("a")).toBe(first.get("a")); + expect(second.get("b")).toBe(first.get("b")); + expect((second.get("c") ?? []).map((b) => b.id)).toEqual(["security"]); + }); + + it("passes the fresh map straight through on the first build", () => { + const map = build(bothSettled(), twoFiles); + expect(reusePolicyBadgeArrays(null, map)).toBe(map); + }); }); diff --git a/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.ts b/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.ts index 40fcd0399e..52779b9cf8 100644 --- a/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.ts +++ b/frontend/editor/src/proprietary/hooks/usePolicyFileBadges.ts @@ -1,17 +1,12 @@ -import { useMemo } from "react"; +import { useMemo, useRef } from "react"; import { usePolicyRuns } from "@app/components/policies/policyRunStore"; import type { PolicyRunRecord } from "@app/components/policies/policyRunStore"; import { useAllFiles } from "@app/contexts/FileContext"; import { loadPolicyCatalog } from "@app/services/policyCatalog"; import { policyAccentVar } from "@app/components/policies/policyStatus"; +import { isClassificationCategory } from "@app/data/policyCategories"; import type { FileItemPolicyRef } from "@app/components/shared/PolicyBadges"; -/** How long after a run a badge counts as "recent" (drives the one-off glow). - * Measured from run start — must exceed the longest realistic policy wall-clock - * time so the glow still fires after a slow run completes and imports. Old or - * reloaded runs fall outside this window, suppressing the glow on page reload. */ -const RECENT_MS = 5 * 60 * 1000; - /** Minimal provenance shape needed to resolve a file's inherited badges. */ type LineageStub = { id: string; @@ -19,14 +14,10 @@ type LineageStub = { sourceFileIds?: string[]; }; -/** Merge a ref into a list, deduping by policy id. A direct (recent) hit wins - * the glow over an inherited one for the same policy. */ +/** Merge a ref into a list, deduping by policy id. */ function mergeRef(list: FileItemPolicyRef[], ref: FileItemPolicyRef): void { - const existing = list.find((p) => p.id === ref.id); - if (!existing) { + if (!list.some((p) => p.id === ref.id)) { list.push(ref); - } else if (ref.recent) { - existing.recent = true; } } @@ -42,21 +33,18 @@ function mergeRef(list: FileItemPolicyRef[], ref: FileItemPolicyRef): void { * from: its transitive `sourceFileIds` (recorded at the consume boundary, so it * covers split/merge/convert too) plus, defensively, its `parentFileId`. * Because `sourceFileIds` is transitive, a flat lookup suffices — no chain walk, - * and it survives a consumed intermediate. Inherited badges never glow - * (recent=false): only the original application does. + * and it survives a consumed intermediate. */ export function buildPolicyBadgeMap( runs: ReadonlyArray, stubs: ReadonlyArray, labelById: ReadonlyMap, - now: number, ): Map { // Direct badges: a file that IS a policy run's output. const directByFile = new Map(); for (const run of runs) { const name = labelById.get(run.categoryId); if (!name) continue; - const recent = now - run.startedAt < RECENT_MS; for (const fileId of run.outputFileIds ?? []) { const list = directByFile.get(fileId) ?? []; if (!list.some((p) => p.id === run.categoryId)) { @@ -64,7 +52,6 @@ export function buildPolicyBadgeMap( id: run.categoryId, name, accentColor: policyAccentVar(run.categoryId), - recent, }); directByFile.set(fileId, list); } @@ -86,7 +73,7 @@ export function buildPolicyBadgeMap( // from. `sourceFileIds` is the transitive provenance set (so a flat lookup // catches even ancestors whose intermediate edits were consumed), and // `parentFileId` is included defensively for any child not created via a - // consume. Inherited badges are marked recent=false (carried, not applied). + // consume. for (const stub of stubs) { const sources = new Set(stub.sourceFileIds ?? []); if (stub.parentFileId) sources.add(stub.parentFileId); @@ -94,14 +81,17 @@ export function buildPolicyBadgeMap( const srcBadges = directByFile.get(src); if (!srcBadges?.length) continue; const list = result.get(stub.id) ?? []; - for (const ref of srcBadges) mergeRef(list, { ...ref, recent: false }); + for (const ref of srcBadges) mergeRef(list, { ...ref }); result.set(stub.id, list); } } // In-flight pass: add (or upgrade) a badge on the input file for any run that // is currently being processed, so the sidebar shows a spinning indicator - // while the policy is actively enforcing — not just after it completes. + // while the policy is actively running — not just after it completes. + // Blocking policies set `enforcing` (which gates actions/overlays); + // classification is non-blocking, so it sets `background` instead — same + // spinner, but nothing is ever gated on it. // Keep the spinner until `imported` is true: the status reaches COMPLETED // before the output files are imported into the workspace, so gating on // status alone would drop the badge during that async gap. @@ -112,17 +102,19 @@ export function buildPolicyBadgeMap( if (settled && !run.retrying) continue; const name = labelById.get(run.categoryId); if (!name) continue; + const inFlightFlag = isClassificationCategory(run.categoryId) + ? ("background" as const) + : ("enforcing" as const); const list = result.get(run.fileId) ?? []; const existing = list.find((p) => p.id === run.categoryId); if (existing) { - existing.enforcing = true; + existing[inFlightFlag] = true; } else { list.push({ id: run.categoryId, name, accentColor: policyAccentVar(run.categoryId), - recent: false, - enforcing: true, + [inFlightFlag]: true, }); result.set(run.fileId, list); } @@ -131,20 +123,70 @@ export function buildPolicyBadgeMap( return result; } +/** Field-wise equality for a badge ref — the whole shape `PolicyBadges` renders. */ +function sameRef(a: FileItemPolicyRef, b: FileItemPolicyRef): boolean { + return ( + a.id === b.id && + a.name === b.name && + a.accentColor === b.accentColor && + !!a.enforcing === !!b.enforcing && + !!a.background === !!b.background + ); +} + +function sameRefs(a: FileItemPolicyRef[], b: FileItemPolicyRef[]): boolean { + return a.length === b.length && a.every((ref, i) => sameRef(ref, b[i])); +} + +/** + * Carry the previous map's array references over to files whose badges didn't + * change, and return the previous MAP itself when none did. + * + * {@link buildPolicyBadgeMap} allocates a fresh array per badged file on every + * call, and the run store hands back a new `runs` array on every status poll — + * so without this, one file's poll tick gives EVERY badged file a new `policies` + * identity, and the memoized sidebar rows can never bail out (the case the + * memoization exists for). `NO_POLICIES` in FileSidebar only covers the rows + * with no badges at all. + */ +export function reusePolicyBadgeArrays( + previous: Map | null, + next: Map, +): Map { + if (!previous) return next; + let changed = previous.size !== next.size; + for (const [fileId, refs] of next) { + const before = previous.get(fileId); + if (before && sameRefs(before, refs)) next.set(fileId, before); + else changed = true; + } + return changed ? next : previous; +} + /** * Distinct policies that have produced each file, keyed by fileId, derived from * the reactive policy run store. Drives the file sidebar's shield badges. The * badge follows a document down its tool-edit chain — see * {@link buildPolicyBadgeMap}. Shadows the core stub via the {@code @app/*} * alias cascade. + * + * Per-file array identity is preserved across rebuilds so memoized consumers + * (the sidebar rows) only re-render for the file that actually changed — see + * {@link reusePolicyBadgeArrays}. */ export function usePolicyFileBadges(): Map { const runs = usePolicyRuns(); const { fileStubs } = useAllFiles(); + const previous = useRef | null>(null); return useMemo(() => { const labelById = new Map( loadPolicyCatalog().categories.map((c) => [c.id, c.label]), ); - return buildPolicyBadgeMap(runs, fileStubs, labelById, Date.now()); + const map = reusePolicyBadgeArrays( + previous.current, + buildPolicyBadgeMap(runs, fileStubs, labelById), + ); + previous.current = map; + return map; }, [runs, fileStubs]); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 970d6703cd..7db904ddf6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -84,6 +84,7 @@ "signature_pad": "^5.0.4", "smol-toml": "^1.4.2", "tailwindcss": "^4.1.13", + "use-sync-external-store": "^1.6.0", "web-vitals": "^5.1.0" }, "devDependencies": { @@ -109,6 +110,7 @@ "@types/node": "^24.5.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.1.9", + "@types/use-sync-external-store": "^0.0.6", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@typescript/native": "npm:typescript@^7.0.2", diff --git a/frontend/package.json b/frontend/package.json index 1763a2126a..dfcfa2842c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -81,6 +81,7 @@ "signature_pad": "^5.0.4", "smol-toml": "^1.4.2", "tailwindcss": "^4.1.13", + "use-sync-external-store": "^1.6.0", "web-vitals": "^5.1.0" }, "scripts": { @@ -131,6 +132,7 @@ "@types/node": "^24.5.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.1.9", + "@types/use-sync-external-store": "^0.0.6", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@typescript/native": "npm:typescript@^7.0.2", From a2dd0298dc7931c1e7f202343a75937a4edd889a Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:00:04 +0200 Subject: [PATCH 012/154] refactor(api): replace length checks with isEmpty (#7214) # Description of Changes Stylistic problem reported by static analyzer. Changes: * Replaced `sb.length() > 0` and `sb.length() == 0` with `!sb.isEmpty()` and `sb.isEmpty()` for `StringBuilder`, `String`, and collections throughout the codebase, improving readability and aligning with modern Java best practices. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have run `task check` to verify linters, typechecks, and tests pass - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../common/pdf/PdfMarkdownConverter.java | 2 +- .../software/common/util/GeneralUtils.java | 2 +- .../common/pdf/PdfMarkdownConverterTest.java | 2 +- .../SPDF/config/ExternalAppDepConfig.java | 2 +- .../SPDF/controller/api/UIDataController.java | 2 +- .../api/security/PasswordController.java | 8 ++++---- .../api/security/RedactExecuteService.java | 18 +++++++++--------- .../SPDF/controller/web/MetricsController.java | 6 +++--- .../SPDF/service/HardwareKeyStoreService.java | 4 ++-- .../controller/api/UserController.java | 2 +- .../security/service/UserService.java | 2 +- .../service/PortalInfraAuditService.java | 2 +- .../service/UserLicenseSettingsService.java | 4 ++-- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java b/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java index c19468b5ed..73f2d7f5ad 100644 --- a/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java +++ b/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java @@ -983,7 +983,7 @@ public class PdfMarkdownConverter { ordered.sort(Comparator.comparingDouble((Line l) -> l.y).reversed()); StringBuilder sb = new StringBuilder(); for (Line l : ordered) { - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append(' '); } sb.append(l.text); diff --git a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java index 4a9ef0834b..52f79f733a 100644 --- a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java @@ -941,7 +941,7 @@ public class GeneralUtils { } // If no MAC address found, use hostname as fallback - if (sb.length() == 0) { + if (sb.isEmpty()) { String hostname = InetAddress.getLocalHost().getHostName(); sb.append(hostname != null ? hostname : "unknown-host"); log.warn("No MAC address found, using hostname for fingerprint generation"); diff --git a/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java b/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java index b3c104da85..7e1d3d2e35 100644 --- a/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java +++ b/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java @@ -154,7 +154,7 @@ class PdfMarkdownConverterTest { || isTableSeparatorRow(line)) { continue; } - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append('\n'); } sb.append(line); diff --git a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index 8755dfe2ef..46b1976ca7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -321,7 +321,7 @@ public class ExternalAppDepConfig { new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { - if (sb.length() > 0) sb.append('\n'); + if (!sb.isEmpty()) sb.append('\n'); sb.append(line); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index de391c7c32..a3ed09fe5d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -130,7 +130,7 @@ public class UIDataController { objectMapper.readValue( config, new TypeReference>() {}); String name = (String) jsonContent.get("name"); - if (name == null || name.length() < 1) { + if (name == null || name.isEmpty()) { String filename = jsonFiles .get(pipelineConfigs.indexOf(config)) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 690c82ca8f..2ad494fc63 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -124,15 +124,15 @@ public class PasswordController { StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); - if ((ownerPassword != null && ownerPassword.length() > 0) - || (password != null && password.length() > 0)) { + if ((ownerPassword != null && !ownerPassword.isEmpty()) + || (password != null && !password.isEmpty())) { spp.setEncryptionKeyLength(keyLength); } spp.setPermissions(ap); document.protect(spp); - if ((ownerPassword == null || ownerPassword.length() == 0) - && (password == null || password.length() == 0)) + if ((ownerPassword == null || ownerPassword.isEmpty()) + && (password == null || password.isEmpty())) return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java index 4a53be97b6..c43abcf666 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java @@ -760,12 +760,12 @@ class RedactExecuteService { char ch = raw.charAt(i); if (Character.isLetterOrDigit(ch)) { current.append(ch); - } else if (current.length() > 0) { + } else if (!current.isEmpty()) { tokens.add(current.toString()); current.setLength(0); } } - if (current.length() > 0) tokens.add(current.toString()); + if (!current.isEmpty()) tokens.add(current.toString()); if (tokens.size() < 2) return null; StringBuilder out = new StringBuilder(); for (int i = 0; i < tokens.size(); i++) { @@ -788,25 +788,25 @@ class RedactExecuteService { StringBuilder current = new StringBuilder(); for (String token : tokens) { if (token.isEmpty()) { - if (current.length() > 0) { - if (result.length() > 0) result.append(' '); + if (!current.isEmpty()) { + if (!result.isEmpty()) result.append(' '); result.append(current); current.setLength(0); } } else if (token.length() == 1) { current.append(token); } else { - if (current.length() > 0) { - if (result.length() > 0) result.append(' '); + if (!current.isEmpty()) { + if (!result.isEmpty()) result.append(' '); result.append(current); current.setLength(0); } - if (result.length() > 0) result.append(' '); + if (!result.isEmpty()) result.append(' '); result.append(token); } } - if (current.length() > 0) { - if (result.length() > 0) result.append(' '); + if (!current.isEmpty()) { + if (!result.isEmpty()) result.append(' '); result.append(current); } return result.toString().trim(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index de28d66ca9..4bbf3dfb82 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -251,7 +251,7 @@ public class MetricsController { // For GET requests, validate if we have a list of valid endpoints final boolean validateGetEndpoints = - endpointInspector.getValidGetEndpoints().size() != 0; + !endpointInspector.getValidGetEndpoints().isEmpty(); if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { @@ -292,7 +292,7 @@ public class MetricsController { // For GET requests, validate if we have a list of valid endpoints final boolean validateGetEndpoints = - endpointInspector.getValidGetEndpoints().size() != 0; + !endpointInspector.getValidGetEndpoints().isEmpty(); if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { @@ -332,7 +332,7 @@ public class MetricsController { // For GET requests, validate if we have a list of valid endpoints final boolean validateGetEndpoints = - endpointInspector.getValidGetEndpoints().size() != 0; + !endpointInspector.getValidGetEndpoints().isEmpty(); if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { diff --git a/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java b/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java index 988b935f27..93ca36d08e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java @@ -237,12 +237,12 @@ public class HardwareKeyStoreService { combined.append(env); } if (prop != null && !prop.isBlank()) { - if (combined.length() > 0) { + if (!combined.isEmpty()) { combined.append(java.io.File.pathSeparator); } combined.append(prop); } - if (combined.length() == 0) { + if (combined.isEmpty()) { return List.of(); } return Arrays.stream(combined.toString().split("[,;" + java.io.File.pathSeparator + "]")) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java index b19c052ff1..fdacda72b2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java @@ -199,7 +199,7 @@ public class UserController { return ResponseEntity.status(HttpStatus.CONFLICT) .body(Map.of("error", "usernameExists", "message", "Username already exists")); } - if (newUsername != null && newUsername.length() > 0) { + if (newUsername != null && !newUsername.isEmpty()) { try { userService.changeUsername(user, newUsername); } catch (IllegalArgumentException e) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java index 0cb4653ef1..e08dd52d07 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java @@ -205,7 +205,7 @@ public class UserService implements UserServiceInterface { User user = findByUsernameIgnoreCase(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); - if (user.getApiKey() == null || user.getApiKey().length() == 0) { + if (user.getApiKey() == null || user.getApiKey().isEmpty()) { user = addApiKeyToUser(username); } return user.getApiKey(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java index c47df3649f..86f944ca9d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java @@ -224,7 +224,7 @@ public class PortalInfraAuditService { if (word.isEmpty()) { continue; } - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append(' '); } String lower = word.toLowerCase(Locale.ROOT); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java index 54660a1ccb..085a9ffcf1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java @@ -515,7 +515,7 @@ public class UserLicenseSettingsService { appendIfPresent(builder, applicationProperties.getAutomaticallyGenerated().getUUID()); appendIfPresent(builder, applicationProperties.getPremium().getKey()); - if (builder.length() == 0) { + if (builder.isEmpty()) { builder.append(DEFAULT_INTEGRITY_SECRET); } @@ -524,7 +524,7 @@ public class UserLicenseSettingsService { private void appendIfPresent(StringBuilder builder, String value) { if (value != null && !value.isBlank()) { - if (builder.length() > 0) { + if (!builder.isEmpty()) { builder.append(SIGNATURE_SEPARATOR); } builder.append(value); From e5c6ceedc52a3aee263ffb20ca2ff4813ca9799c Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:00:25 +0200 Subject: [PATCH 013/154] fix(ui): resolve double scrollbar issue in Sidebar Categories modal (#7142) # Description of Changes Resolves double scrollbar design bug. ### New image ### Old image --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../proprietary/components/shared/FileSidebarGroupControls.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/editor/src/proprietary/components/shared/FileSidebarGroupControls.css b/frontend/editor/src/proprietary/components/shared/FileSidebarGroupControls.css index 16712e351a..0889dfe3d1 100644 --- a/frontend/editor/src/proprietary/components/shared/FileSidebarGroupControls.css +++ b/frontend/editor/src/proprietary/components/shared/FileSidebarGroupControls.css @@ -3,9 +3,6 @@ display: flex; flex-direction: column; gap: 12px; - max-height: 60vh; - overflow-y: auto; - padding-right: 4px; } .fsg-footer { From 732a6025038edfa6250b11b32ce100f9116d5273 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:00:43 +0200 Subject: [PATCH 014/154] style(scanner-effect): remove padding from ToolButton of Scanner-effect (#7205) # Description of Changes ### New image ### Old image --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [X] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../editor/src/core/components/tools/toolPicker/ToolButton.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/editor/src/core/components/tools/toolPicker/ToolButton.tsx b/frontend/editor/src/core/components/tools/toolPicker/ToolButton.tsx index 78c573bb38..4792fd721a 100644 --- a/frontend/editor/src/core/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/editor/src/core/components/tools/toolPicker/ToolButton.tsx @@ -286,7 +286,7 @@ const ToolButton: React.FC = ({ accent="neutral" onClick={() => handleClick(id)} size="sm" - p="sm" + p="none" fullWidth justify="start" className="tool-button" @@ -297,6 +297,7 @@ const ToolButton: React.FC = ({ borderRadius: 0, cursor: visuallyUnavailable ? "not-allowed" : undefined, overflow: "visible", + ...selectedBg, }} > {buttonContent} From cc1c6bc9e37ecc4904b74a489fa6f0d4a6f98d4e Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:01:46 +0200 Subject: [PATCH 015/154] style(people): Fix convert dropdown single-category formatting and admin table header styling (#7139) # Description of Changes The dropdown table with the people looked out-place mainly due to the blue, i think... this simplifies and make more consistent with the rest of the "new" UI and not so aggresive with the colour schema. ### New: image ### Old: image --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [X] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../config/configSections/PeopleSection.tsx | 24 ++++--------------- .../configSections/TeamDetailsSection.tsx | 24 ++++--------------- .../config/configSections/TeamsSection.tsx | 9 +------ 3 files changed, 9 insertions(+), 48 deletions(-) diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/PeopleSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/PeopleSection.tsx index 10e9f39fb0..d955cb341b 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/PeopleSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/PeopleSection.tsx @@ -588,38 +588,22 @@ export default function PeopleSection() { {/* Members Table */} - +
    - - + + {t("workspace.people.user")} {t("workspace.people.role")} - + {t("workspace.people.team")} diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx index cabdfb642c..d3702e7f80 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx @@ -408,29 +408,13 @@ export default function TeamDetailsSection({ {/* Members Table */} -
    +
    - - + + {t("workspace.people.user")} - + {t("workspace.people.role")} diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx index 4559296afe..d4da200d59 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx @@ -298,19 +298,13 @@ export default function TeamsSection() { verticalSpacing="sm" withRowBorders highlightOnHover - style={ - { - "--table-border-color": "var(--mantine-color-gray-3)", - } as React.CSSProperties - } > - + {t("workspace.teams.teamName")} @@ -319,7 +313,6 @@ export default function TeamsSection() { style={{ fontWeight: 600, fontSize: "0.875rem", - color: "var(--mantine-color-gray-7)", }} > {t("workspace.teams.totalMembers")} From 9aaf4173036f4ebca74c8ea8379e4362a17bd969 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:11:26 +0200 Subject: [PATCH 016/154] perf(ui): lazy-load MobileScannerPage and optimize bundle splitting (#7122) # Description of Changes This pull request improves the frontend's performance and code organization by optimizing how certain pages are loaded and by updating the application's bundle splitting strategy. Changes: * Updated the `manualChunks` configuration in `vite.config.ts` to more granularly split vendor dependencies into separate chunks based on their library or usage, which can improve caching and load performance. * Updated MobileScanner code to be lazy loaded --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- frontend/editor/src/core/App.tsx | 5 ++-- frontend/editor/src/proprietary/App.tsx | 5 ++-- frontend/editor/src/saas/App.tsx | 5 ++-- frontend/editor/vite.config.ts | 33 ++++++++++++++++++++++--- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/frontend/editor/src/core/App.tsx b/frontend/editor/src/core/App.tsx index 566e861520..6fd23e5510 100644 --- a/frontend/editor/src/core/App.tsx +++ b/frontend/editor/src/core/App.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import { Suspense, lazy } from "react"; import { Routes, Route } from "react-router-dom"; import { AppProviders } from "@app/components/AppProviders"; import { AppLayout } from "@app/components/AppLayout"; @@ -6,9 +6,10 @@ import { LoadingFallback } from "@app/components/shared/LoadingFallback"; import { ThemeProvider } from "@app/components/shared/ThemeProvider"; import { PreferencesProvider } from "@app/contexts/PreferencesContext"; import HomePage from "@app/pages/HomePage"; -import MobileScannerPage from "@app/pages/MobileScannerPage"; import Onboarding from "@app/components/onboarding/Onboarding"; +const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage")); + // Import global styles import "@app/styles/tailwind.css"; import "@app/styles/cookieconsent.css"; diff --git a/frontend/editor/src/proprietary/App.tsx b/frontend/editor/src/proprietary/App.tsx index e98064ebc8..a2103bd60a 100644 --- a/frontend/editor/src/proprietary/App.tsx +++ b/frontend/editor/src/proprietary/App.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import { Suspense, lazy } from "react"; import { Routes, Route, useParams } from "react-router-dom"; import { AppProviders } from "@app/components/AppProviders"; import { AppLayout } from "@app/components/AppLayout"; @@ -12,9 +12,10 @@ import AuthCallback from "@app/routes/AuthCallback"; import InviteAccept from "@app/routes/InviteAccept"; import ShareLinkPage from "@app/routes/ShareLinkPage"; import ParticipantView from "@app/components/workflow/ParticipantView"; -import MobileScannerPage from "@app/pages/MobileScannerPage"; import Onboarding from "@app/components/onboarding/Onboarding"; import WatchedFoldersRegistration from "@app/components/watchedFolders/WatchedFoldersRegistration"; + +const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage")); import { WATCHED_FOLDERS_ENABLED } from "@app/constants/featureFlags"; import { getAdminRouteExtensions } from "@app/routes/adminRouteExtensions"; import { LoginLandingRedirect } from "@app/components/LoginLandingRedirect"; diff --git a/frontend/editor/src/saas/App.tsx b/frontend/editor/src/saas/App.tsx index 32af9a6782..9509f630b0 100644 --- a/frontend/editor/src/saas/App.tsx +++ b/frontend/editor/src/saas/App.tsx @@ -1,4 +1,4 @@ -import { Suspense, type ReactNode } from "react"; +import { Suspense, lazy, type ReactNode } from "react"; import { Routes, Route, useLocation } from "react-router-dom"; import { isAuthRoute } from "@app/utils/pathUtils"; import { AppProviders } from "@app/components/AppProviders"; @@ -16,13 +16,14 @@ import AuthCallback from "@app/routes/AuthCallback"; import ResetPassword from "@app/routes/ResetPassword"; import OAuthConsent from "@app/routes/OAuthConsent"; import ShareLinkPage from "@app/routes/ShareLinkPage"; -import MobileScannerPage from "@app/pages/MobileScannerPage"; import { getAdminRouteExtensions } from "@app/routes/adminRouteExtensions"; import OnboardingBootstrap from "@app/components/OnboardingBootstrap"; import SignupRequiredBootstrap from "@app/components/SignupRequiredBootstrap"; import UsageLimitModalHost from "@app/components/UsageLimitModalHost"; import { LoginLandingRedirect } from "@app/components/LoginLandingRedirect"; +const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage")); + // Import global styles import "@app/styles/tailwind.css"; import "@app/auth/ui/auth-theme.css"; diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts index 0d367d2465..2e72d1efec 100644 --- a/frontend/editor/vite.config.ts +++ b/frontend/editor/vite.config.ts @@ -358,9 +358,36 @@ export default defineConfig(async ({ mode, command }) => { target: "esnext", rollupOptions: { output: { - manualChunks: { - "vendor-react": ["react", "react-dom"], - "pdf-engine": ["@embedpdf/engines", "@embedpdf/pdfium"], + manualChunks(id) { + if (id.includes("material-symbols-icons.json")) + return "vendor-iconset"; + if (id.includes("node_modules")) { + if (id.includes("pdfjs-dist")) return "vendor-pdfjs"; + if (id.includes("@embedpdf")) return "vendor-embedpdf"; + if ( + id.includes("react") || + id.includes("@mantine") || + id.includes("@emotion") || + id.includes("@mui") || + id.includes("@iconify") + ) { + return "vendor-ui"; + } + if (id.includes("@supabase")) return "vendor-supabase"; + if (id.includes("posthog-js") || id.includes("@posthog")) + return "vendor-posthog"; + if (id.includes("@cantoo/pdf-lib") || id.includes("pdf-lib")) + return "vendor-pdflib"; + if ( + id.includes("recharts") || + id.includes("d3") || + id.includes("decimal.js") + ) + return "vendor-charts"; + if (id.includes("jszip") || id.includes("pako")) + return "vendor-zip"; + if (id.includes("i18next")) return "vendor-i18n"; + } }, }, }, From 934ad180cb9cf87aa2709c1b40f4e5ca4ee2f223 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:16:15 +0200 Subject: [PATCH 017/154] fix(ui): handle 404 policy error on upload without toast popup (#7254) # Description of Changes Fixes policy error pop-ups that sometimes happen upon upload. Changes: - Improved the error handling in `runPolicyOnFile` to log detailed debug information when policy dispatch fails, making it easier to trace issues such as missing policies or backend errors. - Updated the `runStoredPolicy` function to pass `{ suppressErrorToast: true }` to the API client, preventing error toasts from appearing in the UI when the policy run fails. --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../proprietary/components/policies/usePolicyAutoRun.ts | 8 ++++++-- frontend/editor/src/proprietary/services/policyApi.ts | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts index e475d784ea..597136a32f 100644 --- a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts +++ b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts @@ -955,10 +955,14 @@ async function runPolicyOnFile( error: null, startedAt: Date.now(), }); - } catch { - // Dispatch failed (offline / backend error). Mark dispatched so we don't hammer; + } catch (err) { + // Dispatch failed (e.g. policy deleted/404 or backend offline). Mark dispatched so we don't hammer; // the absent run simply won't appear in the activity feed. If the backend did // start a run we never recorded, reconcileServerRuns rediscovers it. + console.debug( + `[PolicyAutoRun] Failed to dispatch policy ${categoryId} (${backendId}):`, + err, + ); markDispatched(categoryId, fileId); } finally { releaseDispatchSlot(); diff --git a/frontend/editor/src/proprietary/services/policyApi.ts b/frontend/editor/src/proprietary/services/policyApi.ts index a10c7a8382..b0d78f36f7 100644 --- a/frontend/editor/src/proprietary/services/policyApi.ts +++ b/frontend/editor/src/proprietary/services/policyApi.ts @@ -73,6 +73,7 @@ export async function runStoredPolicy( const res = await apiClient.post( `/api/v1/policies/${encodeURIComponent(id)}/run`, form, + { suppressErrorToast: true }, ); return res.data.jobId; } From 7cccee4c346c5bf10058bb7e570c23f7374bb9f5 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:17:27 +0200 Subject: [PATCH 018/154] refactor(get-info): remove redundant PDF validation logic (#7213) # Description of Changes Could not get past validation, since very few endpoint have such validation, i think redundant. Changes: * Removed the `validatePdfFile` method, which previously checked for file presence, size limits, and content type, from `GetInfoOnPDF.java`. * Deleted the invocation of `validatePdfFile` and its associated error handling from the `getPdfInfo` method, so uploaded files are no longer validated at this layer. --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../controller/api/security/GetInfoOnPDF.java | 28 ------- .../api/security/GetInfoOnPDFMoreTest.java | 15 ---- .../api/security/GetInfoOnPDFTest.java | 76 ------------------- 3 files changed, 119 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index d29480fd3c..d2775e910b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -61,7 +61,6 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.model.tool.ToolFormat; import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @@ -270,25 +269,6 @@ public class GetInfoOnPDF { } } - private static void validatePdfFile(MultipartFile file) { - if (file == null || file.isEmpty()) { - throw new IllegalArgumentException("PDF file is required"); - } - - if (file.getSize() > MAX_FILE_SIZE) { - throw ExceptionUtils.createIllegalArgumentException( - "error.fileSizeLimit", - "File size ({0} bytes) exceeds maximum allowed size ({1} bytes)", - file.getSize(), - MAX_FILE_SIZE); - } - - String contentType = file.getContentType(); - if (contentType != null && !"application/pdf".equals(contentType)) { - log.warn("File content type is {}, expected application/pdf", contentType); - } - } - private static ResponseEntity createErrorResponse(String errorMessage) { try { ObjectNode errorNode = objectMapper.createObjectNode(); @@ -1104,14 +1084,6 @@ public class GetInfoOnPDF { public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) throws IOException { MultipartFile inputFile = request.getFileInput(); - // Validate input - try { - validatePdfFile(inputFile); - } catch (IllegalArgumentException e) { - log.error("Invalid PDF file: {}", e.getMessage()); - return createErrorResponse("Invalid PDF file: " + e.getMessage()); - } - List verificationResults = null; try { verificationResults = veraPDFService.validatePDF(inputFile.getInputStream()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java index 7e2aa388fa..1df66f739e 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java @@ -264,21 +264,6 @@ class GetInfoOnPDFMoreTest { @DisplayName("error handling") class Errors { - @Test - @DisplayName("empty file input yields an error response") - void emptyFile() throws Exception { - MockMultipartFile mf = - new MockMultipartFile("fileInput", "x.pdf", "application/pdf", new byte[0]); - PDFFile request = new PDFFile(); - request.setFileInput(mf); - ResponseEntity resp = getInfoOnPDF.getPdfInfo(request); - // createErrorResponse returns HTTP 200 with a JSON body carrying an "error" field. - assertThat(resp.getBody()).isNotNull(); - JsonNode body = om.readTree(resp.getBody()); - assertThat(body.has("error")).isTrue(); - assertThat(body.get("error").asText("")).contains("Invalid"); - } - @Test @DisplayName("veraPDF failure is swallowed and a report is still produced") void veraPdfFailureSwallowed() throws Exception { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java index efc09cf190..8d17d729ea 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java @@ -556,24 +556,6 @@ class GetInfoOnPDFTest { @DisplayName("Validation and Error Handling Tests") class ValidationErrorTests { - @Test - @DisplayName("Should reject null file") - void testValidation_NullFile() throws IOException { - PDFFile request = new PDFFile(); - request.setFileInput(null); - - ResponseEntity response = getInfoOnPDF.getPdfInfo(request); - - Assertions.assertEquals( - HttpStatus.OK, response.getStatusCode()); // Returns error JSON with 200 - String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8); - JsonNode jsonNode = objectMapper.readTree(jsonResponse); - - Assertions.assertTrue(jsonNode.has("error")); - Assertions.assertTrue( - jsonNode.get("error").asText("").contains("PDF file is required")); - } - @Test @DisplayName("Should reject empty file") void testValidation_EmptyFile() throws IOException { @@ -591,64 +573,6 @@ class GetInfoOnPDFTest { Assertions.assertTrue(jsonNode.has("error")); } - - @Test - @DisplayName("Should reject file that exceeds max size") - void testValidation_TooLargeFile() throws IOException { - MultipartFile largeFile = - new MultipartFile() { - @Override - public String getName() { - return "file"; - } - - @Override - public String getOriginalFilename() { - return "large.pdf"; - } - - @Override - public String getContentType() { - return MediaType.APPLICATION_PDF_VALUE; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public long getSize() { - // Report 101 MB without allocating memory - return 101L * 1024L * 1024L; - } - - @Override - public byte[] getBytes() { - return new byte[0]; - } - - @Override - public java.io.InputStream getInputStream() { - return java.io.InputStream.nullInputStream(); - } - - @Override - public void transferTo(java.io.File dest) throws IllegalStateException {} - }; - - PDFFile request = new PDFFile(); - request.setFileInput(largeFile); - - ResponseEntity response = getInfoOnPDF.getPdfInfo(request); - - String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8); - JsonNode jsonNode = objectMapper.readTree(jsonResponse); - - Assertions.assertTrue(jsonNode.has("error")); - Assertions.assertTrue( - jsonNode.get("error").asText("").contains("exceeds maximum allowed size")); - } } @Nested From e560ee4cc45e1bde1e71012f888e05f83add8b86 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:25:22 +0200 Subject: [PATCH 019/154] chore(deps): update junrar dependency to version 8.0.0 (#7210) # Description of Changes Since this was a major version update i tested manually, afterwards figured i'll submit as PR. This version of junrar adds long-awaited (by me) RAR 5 support to the library. RAR 5 is newest version of the RAR file format and was not available in previous Junrar version, but is somewhat common for CBR files to be RAR 5. For junrar release notes see: https://github.com/junrar/junrar/releases/tag/v8.0.0 Changes: - Bumped junrar dep to version 8.0.0 --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- app/common/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/build.gradle b/app/common/build.gradle index 73be441940..942dddc5fd 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -16,7 +16,7 @@ dependencies { api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion" api "org.apache.pdfbox:xmpbox:$pdfboxVersion" api "org.apache.pdfbox:preflight:$pdfboxVersion" - api 'com.github.junrar:junrar:7.6.0' // RAR archive support for CBR files + api 'com.github.junrar:junrar:8.0.0' // RAR archive support for CBR files api 'jakarta.servlet:jakarta.servlet-api:6.1.0' api 'org.snakeyaml:snakeyaml-engine:3.0.1' api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3" From 866e56728d11a8e9e6c5313cdb860e5e814abac1 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 6 Aug 2026 13:06:16 +0200 Subject: [PATCH 020/154] fix(storage): delete share access records before expired share links (#7161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - Updated expired share-link cleanup to delete related `FileShareAccess` records before deleting their parent `FileShare` records. - Wrapped the cleanup operation in a transaction to ensure the deletion order is enforced atomically. - Prevents foreign-key constraint violations and scheduled-task failures during cleanup. - The full backend check was limited by a Gradle distribution download/network error. ```cmd [backend:dev:proprietary] 16:25:43.362 [scheduled-vt-2] WARN org.hibernate.orm.jdbc.error - HHH000247: ErrorCode: 23503, SQLState: 23503 [backend:dev:proprietary] 16:25:43.362 [scheduled-vt-2] WARN org.hibernate.orm.jdbc.error - Referentielle Integrität verletzt: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))" [backend:dev:proprietary] Referential integrity constraint violation: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))"; SQL statement: [backend:dev:proprietary] delete from file_shares where file_share_id=? [23503-240] [backend:dev:proprietary] 16:25:43.380 [scheduled-vt-2] ERROR o.s.s.s.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task [backend:dev:proprietary] org.springframework.dao.DataIntegrityViolationException: could not execute statement [Referentielle Integrität verletzt: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))" [backend:dev:proprietary] Referential integrity constraint violation: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))"; SQL statement: [backend:dev:proprietary] delete from file_shares where file_share_id=? [23503-240]] [delete from file_shares where file_share_id=?]; SQL [delete from file_shares where file_share_id=?]; constraint [FKQ6V4QH5LFCAWII0ABRVSJO5SG] [backend:dev:proprietary] at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.convertHibernateAccessException(HibernateExceptionTranslator.java:169) [backend:dev:proprietary] at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.convertHibernateAccessException(HibernateExceptionTranslator.java:131) [backend:dev:proprietary] at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.translateExceptionIfPossible(HibernateExceptionTranslator.java:105) [backend:dev:proprietary] at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:223) [backend:dev:proprietary] at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:557) [backend:dev:proprietary] at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:794) [backend:dev:proprietary] at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:757) [backend:dev:proprietary] at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:687) [backend:dev:proprietary] at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:408) [backend:dev:proprietary] at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130) [backend:dev:proprietary] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [backend:dev:proprietary] at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:135) [backend:dev:proprietary] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [backend:dev:proprietary] at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:166) [backend:dev:proprietary] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [backend:dev:proprietary] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222) [backend:dev:proprietary] at jdk.proxy4/jdk.proxy4.$Proxy246.deleteAll(Unknown Source) [backend:dev:proprietary] at stirling.software.proprietary.storage.service.StorageCleanupService.cleanupExpiredShareLinks(StorageCleanupService.java:71) [backend:dev:proprietary] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) [backend:dev:proprietary] at java.base/java.lang.reflect.Method.invoke(Method.java:565) [backend:dev:proprietary] at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:128) [backend:dev:proprietary] at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$1(ScheduledMethodRunnable.java:122) [backend:dev:proprietary] at io.micrometer.observation.Observation.observe(Observation.java:569) [backend:dev:proprietary] at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:122) [backend:dev:proprietary] at org.springframework.scheduling.config.Task$OutcomeTrackingRunnable.run(Task.java:88) [backend:dev:proprietary] at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) [backend:dev:proprietary] at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545) [backend:dev:proprietary] at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:369) [backend:dev:proprietary] at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:310) [backend:dev:proprietary] at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090) [backend:dev:proprietary] at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614) [backend:dev:proprietary] at java.base/java.lang.VirtualThread.run(VirtualThread.java:460) [backend:dev:proprietary] Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement [Referentielle Integrität verletzt: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))" [backend:dev:proprietary] Referential integrity constraint violation: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))"; SQL statement: [backend:dev:proprietary] delete from file_shares where file_share_id=? [23503-240]] [delete from file_shares where file_share_id=?] [backend:dev:proprietary] at org.hibernate.dialect.H2Dialect.lambda$buildSQLExceptionConversionDelegate$0(H2Dialect.java:840) [backend:dev:proprietary] at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:34) [backend:dev:proprietary] at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:115) [backend:dev:proprietary] at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:184) [backend:dev:proprietary] at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:145) [backend:dev:proprietary] at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:53) [backend:dev:proprietary] at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:66) [backend:dev:proprietary] at org.hibernate.persister.entity.mutation.AbstractDeleteCoordinator.doStaticDelete(AbstractDeleteCoordinator.java:268) [backend:dev:proprietary] at org.hibernate.persister.entity.mutation.AbstractDeleteCoordinator.delete(AbstractDeleteCoordinator.java:79) [backend:dev:proprietary] at org.hibernate.action.internal.EntityDeleteAction.execute(EntityDeleteAction.java:119) [backend:dev:proprietary] at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:634) [backend:dev:proprietary] at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:505) [backend:dev:proprietary] at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:381) [backend:dev:proprietary] at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) [backend:dev:proprietary] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138) [backend:dev:proprietary] at org.hibernate.internal.SessionImpl.fireFlush(SessionImpl.java:1484) [backend:dev:proprietary] at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:481) [backend:dev:proprietary] at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2111) [backend:dev:proprietary] at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2033) [backend:dev:proprietary] at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:410) [backend:dev:proprietary] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:166) [backend:dev:proprietary] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commitNoRollbackOnly(JdbcResourceLocalTransactionCoordinatorImpl.java:248) [backend:dev:proprietary] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:242) [backend:dev:proprietary] at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:89) [backend:dev:proprietary] at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:553) [backend:dev:proprietary] ... 27 common frames omitted [backend:dev:proprietary] Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referentielle Integrität verletzt: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))" [backend:dev:proprietary] Referential integrity constraint violation: "FKQ6V4QH5LFCAWII0ABRVSJO5SG: PUBLIC.FILE_SHARE_ACCESSES FOREIGN KEY(FILE_SHARE_ID) REFERENCES PUBLIC.FILE_SHARES(FILE_SHARE_ID) (CAST(97 AS BIGINT))"; SQL statement: [backend:dev:proprietary] delete from file_shares where file_share_id=? [23503-240] [backend:dev:proprietary] at org.h2.message.DbException.getJdbcSQLException(DbException.java:520) [backend:dev:proprietary] at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) [backend:dev:proprietary] at org.h2.message.DbException.get(DbException.java:223) [backend:dev:proprietary] at org.h2.message.DbException.get(DbException.java:199) [backend:dev:proprietary] at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:363) [backend:dev:proprietary] at org.h2.constraint.ConstraintReferential.checkRowRefTable(ConstraintReferential.java:380) [backend:dev:proprietary] at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:254) [backend:dev:proprietary] at org.h2.table.Table.fireConstraints(Table.java:1208) [backend:dev:proprietary] at org.h2.table.Table.fireAfterRow(Table.java:1226) [backend:dev:proprietary] at org.h2.command.dml.Delete.update(Delete.java:81) [backend:dev:proprietary] at org.h2.command.dml.DataChangeStatement.update(DataChangeStatement.java:77) [backend:dev:proprietary] at org.h2.command.CommandContainer.update(CommandContainer.java:139) [backend:dev:proprietary] at org.h2.command.Command.executeUpdate(Command.java:306) [backend:dev:proprietary] at org.h2.command.Command.executeUpdate(Command.java:250) [backend:dev:proprietary] at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:213) [backend:dev:proprietary] at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:172) [backend:dev:proprietary] at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) [backend:dev:proprietary] at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) [backend:dev:proprietary] at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:181) [backend:dev:proprietary] ... 48 common frames omitted ``` --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../proprietary/storage/service/StorageCleanupService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java index 5ea5f28def..32c54c6298 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java @@ -7,12 +7,14 @@ import java.util.concurrent.TimeUnit; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.storage.model.StorageCleanupEntry; import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.repository.FileShareAccessRepository; import stirling.software.proprietary.storage.repository.FileShareRepository; import stirling.software.proprietary.storage.repository.StorageCleanupEntryRepository; @@ -25,6 +27,7 @@ public class StorageCleanupService { private final StorageProvider storageProvider; private final StorageCleanupEntryRepository cleanupEntryRepository; + private final FileShareAccessRepository fileShareAccessRepository; private final FileShareRepository fileShareRepository; @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS) @@ -62,12 +65,14 @@ public class StorageCleanupService { } @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS) + @Transactional public void cleanupExpiredShareLinks() { List expired = fileShareRepository.findByExpiresAtBeforeAndShareTokenNotNull(LocalDateTime.now()); if (expired.isEmpty()) { return; } + expired.forEach(fileShareAccessRepository::deleteByFileShare); fileShareRepository.deleteAll(expired); } } From b10fc1b2de79ced7e7b26d89191c27868619dd80 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 6 Aug 2026 13:06:28 +0200 Subject: [PATCH 021/154] fix(java): prevent executor, task, regex, and stream resource leaks (#7284) # Description of Changes - Added graceful shutdown handling for service-owned executors in `JobExecutorService`, `PolicyEngine`, and `AsyncConfig`. - Added expiration and cleanup for abandoned pending jobs in `TaskManager`. - Replaced the unbounded regex pattern cache with a bounded cache limited to 512 entries. - Ensured `Files.walk()` is closed correctly in `MobileScannerService`. - These changes prevent unbounded heap growth, lingering virtual-thread executors, and file-descriptor leaks. - Added configurable pending-job expiration through `stirling.job.pendingExpiryMinutes`, defaulting to 24 hours. --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../common/service/JobExecutorService.java | 16 ++++++++ .../common/service/MobileScannerService.java | 28 +++++++------ .../software/common/service/TaskManager.java | 27 ++++++++++--- .../common/util/RegexPatternUtils.java | 40 +++++++++++++++---- .../proprietary/config/AsyncConfig.java | 30 ++++++++++++-- .../policy/engine/PolicyEngine.java | 17 ++++++++ 6 files changed, 128 insertions(+), 30 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java index 23a23e868b..f283f65763 100644 --- a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java +++ b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import jakarta.annotation.PreDestroy; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; @@ -63,6 +64,21 @@ public class JobExecutorService { "Job executor configured with effective timeout of {} ms", this.effectiveTimeoutMs); } + /** Stop the service-owned executor when the application context is closed or restarted. */ + @PreDestroy + public void shutdown() { + log.debug("Shutting down job executor"); + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + executor.shutdownNow(); + } + } + public ResponseEntity runJobGeneric(boolean async, Supplier work) { return runJobGeneric(async, work, -1); } diff --git a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java index 7c544b6242..18958841b2 100644 --- a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java +++ b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java @@ -225,19 +225,21 @@ public class MobileScannerService { Path sessionDir = getSafeSessionDirectory(sessionId); if (Files.exists(sessionDir)) { // Delete all files in session directory - Files.walk(sessionDir) - .sorted( - (a, b) -> - -a.compareTo(b)) // Reverse order to delete files before - // directory - .forEach( - path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - log.warn("Failed to delete file: {}", path, e); - } - }); + try (var paths = Files.walk(sessionDir)) { + paths.sorted( + (a, b) -> + -a.compareTo( + b)) // Reverse order to delete files before + // directory + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn("Failed to delete file: {}", path, e); + } + }); + } } log.info("Deleted session: {}", sessionId); } catch (IllegalArgumentException e) { diff --git a/app/common/src/main/java/stirling/software/common/service/TaskManager.java b/app/common/src/main/java/stirling/software/common/service/TaskManager.java index 3fd9ac3fe4..f504b39395 100644 --- a/app/common/src/main/java/stirling/software/common/service/TaskManager.java +++ b/app/common/src/main/java/stirling/software/common/service/TaskManager.java @@ -48,6 +48,10 @@ public class TaskManager { @Value("${stirling.jobResultExpiryMinutes:30}") private int jobResultExpiryMinutes = 30; + /** Maximum age of a task that never reached a terminal state. */ + @Value("${stirling.job.pendingExpiryMinutes:1440}") + private int pendingJobExpiryMinutes = 1440; + private final FileStorage fileStorage; private final JobStore jobStore; private final ClusterBackplane clusterBackplane; @@ -332,19 +336,32 @@ public class TaskManager { } LocalDateTime expiryThreshold = LocalDateTime.now().minus(jobResultExpiryMinutes, ChronoUnit.MINUTES); + LocalDateTime pendingExpiryThreshold = + LocalDateTime.now().minus(pendingJobExpiryMinutes, ChronoUnit.MINUTES); int removedCount = 0; try { for (Map.Entry entry : jobResults.entrySet()) { JobResult result = entry.getValue(); - // Remove completed jobs that are older than the expiry threshold - if (result.isComplete() - && result.getCompletedAt() != null - && result.getCompletedAt().isBefore(expiryThreshold)) { + boolean expiredCompletedJob = + result.isComplete() + && result.getCompletedAt() != null + && result.getCompletedAt().isBefore(expiryThreshold); + boolean abandonedPendingJob = + !result.isComplete() + && result.getCreatedAt() != null + && result.getCreatedAt().isBefore(pendingExpiryThreshold); + + // Remove old terminal results and abandoned pending jobs. Without the second + // branch, a client that starts a task and never completes it keeps its result in + // memory forever. + if (expiredCompletedJob || abandonedPendingJob) { // Clean up file results - cleanupJobFiles(result, entry.getKey()); + if (expiredCompletedJob) { + cleanupJobFiles(result, entry.getKey()); + } // Remove the job result jobResults.remove(entry.getKey()); diff --git a/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java index b4821edd9c..9d2d1b74db 100644 --- a/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java @@ -1,17 +1,22 @@ package stirling.software.common.util; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.UncheckedExecutionException; + import lombok.extern.slf4j.Slf4j; @Slf4j public final class RegexPatternUtils { private static final RegexPatternUtils INSTANCE = new RegexPatternUtils(); - private final ConcurrentHashMap patternCache = new ConcurrentHashMap<>(); + private static final long MAX_CACHED_PATTERNS = 512; + private final Cache patternCache = + CacheBuilder.newBuilder().maximumSize(MAX_CACHED_PATTERNS).build(); private static final String WHITESPACE_REGEX = "\\s++"; private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$"; @@ -51,7 +56,7 @@ public final class RegexPatternUtils { throw new IllegalArgumentException("Regex pattern cannot be null"); } - return patternCache.computeIfAbsent(new PatternKey(regex, 0), this::compilePattern); + return getOrCompile(new PatternKey(regex, 0)); } /** @@ -77,7 +82,7 @@ public final class RegexPatternUtils { throw new IllegalArgumentException("Regex pattern cannot be null"); } - return patternCache.computeIfAbsent(new PatternKey(regex, flags), this::compilePattern); + return getOrCompile(new PatternKey(regex, flags)); } /** @@ -98,7 +103,7 @@ public final class RegexPatternUtils { * @return true if pattern is cached, false otherwise */ public boolean isCached(String regex, int flags) { - return regex != null && patternCache.containsKey(new PatternKey(regex, flags)); + return regex != null && patternCache.getIfPresent(new PatternKey(regex, flags)) != null; } /** @@ -107,7 +112,7 @@ public final class RegexPatternUtils { * @return number of patterns currently cached */ public int getCacheSize() { - return patternCache.size(); + return (int) patternCache.size(); } /** @@ -115,7 +120,7 @@ public final class RegexPatternUtils { * useful for testing or memory cleanup in long-running applications. */ public void clearCache() { - patternCache.clear(); + patternCache.invalidateAll(); log.debug("Regex pattern cache cleared"); } @@ -141,13 +146,32 @@ public final class RegexPatternUtils { return false; } PatternKey key = new PatternKey(regex, flags); - boolean removed = patternCache.remove(key) != null; + boolean removed = patternCache.getIfPresent(key) != null; + patternCache.invalidate(key); if (removed) { log.debug("Removed regex pattern from cache: {} (flags: {})", regex, flags); } return removed; } + private Pattern getOrCompile(PatternKey key) { + try { + return patternCache.get(key, () -> compilePattern(key)); + } catch (UncheckedExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof PatternSyntaxException patternSyntaxException) { + throw patternSyntaxException; + } + throw e; + } catch (java.util.concurrent.ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof PatternSyntaxException patternSyntaxException) { + throw patternSyntaxException; + } + throw new IllegalStateException("Failed to compile regex pattern", cause); + } + } + /** * Internal method to compile a pattern and handle errors consistently. * diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java index ea096a8d23..3ba4adcbee 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java @@ -2,6 +2,7 @@ package stirling.software.proprietary.config; import java.util.Map; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.slf4j.MDC; @@ -12,10 +13,15 @@ import org.springframework.core.task.support.TaskExecutorAdapter; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; +import jakarta.annotation.PreDestroy; + @Configuration @EnableAsync public class AsyncConfig { + private ExecutorService auditExecutorService; + private ExecutorService aiStreamExecutorService; + /** * MDC context-propagating task decorator. Copies MDC context from the caller thread to the * virtual thread executing the task. @@ -44,8 +50,8 @@ public class AsyncConfig { @Bean(name = "auditExecutor") public Executor auditExecutor() { - TaskExecutorAdapter adapter = - new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); + auditExecutorService = Executors.newVirtualThreadPerTaskExecutor(); + TaskExecutorAdapter adapter = new TaskExecutorAdapter(auditExecutorService); adapter.setTaskDecorator(new MDCContextTaskDecorator()); return adapter; } @@ -53,9 +59,25 @@ public class AsyncConfig { /** Propagates the request's SecurityContext onto background AI-orchestration threads. */ @Bean(name = "aiStreamExecutor") public Executor aiStreamExecutor() { - TaskExecutorAdapter adapter = - new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); + aiStreamExecutorService = Executors.newVirtualThreadPerTaskExecutor(); + TaskExecutorAdapter adapter = new TaskExecutorAdapter(aiStreamExecutorService); adapter.setTaskDecorator(new MDCContextTaskDecorator()); return new DelegatingSecurityContextExecutor(adapter); } + + /** + * Close the underlying executors because the exposed Spring adapters do not own their + * lifecycle. + */ + @PreDestroy + void shutdown() { + shutdownExecutor(auditExecutorService); + shutdownExecutor(aiStreamExecutorService); + } + + private void shutdownExecutor(ExecutorService executor) { + if (executor != null) { + executor.shutdownNow(); + } + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java index e6dce0ee7b..1f31d153a6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java @@ -16,6 +16,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientResponseException; +import jakarta.annotation.PreDestroy; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -79,6 +81,21 @@ public class PolicyEngine { private final ExecutorService asyncExecutor = ExecutorFactory.newVirtualThreadExecutor(); + /** Stop the service-owned executor when the application context is closed or restarted. */ + @PreDestroy + void shutdown() { + log.debug("Shutting down policy engine executor"); + asyncExecutor.shutdown(); + try { + if (!asyncExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { + asyncExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + asyncExecutor.shutdownNow(); + } + } + /** * Submit a pipeline to run asynchronously. The handle's run id scopes a {@link TaskManager} job * (status/notes/results observable via the job endpoints); its future resolves when the run From 2265e48b3215f77a3b52cf0c6ce47f94f1dcc3c2 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 6 Aug 2026 13:43:53 +0200 Subject: [PATCH 022/154] chore(ci): optimize GitHub Actions Gradle caching across workflows (#7299) # Description of Changes This PR refactors Gradle caching across the GitHub Actions workflows to improve cache reuse, reduce dependency resolution overhead, and shorten CI execution times. ### What was changed - Replaced multiple `gradle/actions/setup-gradle` steps with a unified `actions/cache`-based Gradle User Home cache strategy. - Standardized cache paths across workflows to include: - `~/.gradle/caches` - `~/.gradle/wrapper` - Introduced consistent cache keys using: - Runner OS - Runner architecture - JDK version - Hashes of Gradle wrapper, version catalog, Gradle build files, and project build scripts. - Added restore keys to maximize cache hit rates across similar environments. - Added a new **`gradle-cache-prime`** job in the main build workflow that: - Restores or creates the shared Gradle cache. - Resolves backend dependencies before downstream jobs execute. - Makes the populated cache available to subsequent jobs. - Updated workflow dependencies so Gradle-based jobs wait for the cache priming job before execution. - Simplified and unified Gradle cache handling across numerous CI workflows, including backend builds, OpenAPI generation, database migration tests, Docker tests, Tauri builds, Swagger generation, enterprise builds, release workflows, and license generation. - Updated workflow comments to reflect the new caching strategy and shared cache behavior. ### Why the change was made The previous workflows used a mixture of Gradle setup actions and partial dependency caches, leading to duplicated dependency downloads, inconsistent cache behavior, and longer CI runtimes. Consolidating all workflows onto a shared Gradle User Home cache with a dedicated cache priming job improves cache reuse, reduces unnecessary dependency resolution, and makes CI execution 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../workflows/PR-Demo-Comment-with-react.yml | 12 +++-- .github/workflows/backend-build.yml | 16 +++--- .github/workflows/build-enterprise.yml | 10 ++++ .github/workflows/build.yml | 53 +++++++++++++++---- .github/workflows/check-generated-models.yml | 12 +++-- .github/workflows/check-licence.yml | 16 +++--- .github/workflows/check-openapi.yml | 16 +++--- .github/workflows/coverage-aggregate.yml | 22 ++++---- .github/workflows/db-migration-test.yml | 20 +++---- .github/workflows/docker-compose-tests.yml | 16 +++--- .github/workflows/e2e-live.yml | 17 +++--- .../frontend-backend-licenses-update.yml | 12 +++-- .github/workflows/multiOSReleases.yml | 36 ++++++++----- .github/workflows/push-docker.yml | 12 ++--- .github/workflows/swagger.yml | 12 +++-- .github/workflows/tauri-build.yml | 12 +++-- .github/workflows/test-build-docker.yml | 16 +++--- .github/workflows/testdriver.yml | 12 +++-- 18 files changed, 187 insertions(+), 135 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index e48a6170da..3826897a39 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -211,10 +211,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 4d133593d3..47b5591e19 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -40,20 +40,16 @@ jobs: java-version: ${{ matrix.jdk-version }} distribution: "temurin" - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml index 0fb02ad90f..398cca3144 100644 --- a/.github/workflows/build-enterprise.yml +++ b/.github/workflows/build-enterprise.yml @@ -60,6 +60,16 @@ jobs: with: java-version: "25" distribution: "temurin" + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d4aee192d..4ad4b046e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,8 +60,43 @@ jobs: with: filters: .github/config/.files.yaml - build: + gradle-cache-prime: + name: Prime shared Gradle cache needs: [files-changed] + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: "25" + distribution: "temurin" + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- + - name: Resolve backend dependencies + run: ./gradlew :stirling-pdf:classes -PnoSpotless --no-daemon + env: + STIRLING_FLAVOR: saas + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} + + build: + needs: [files-changed, gradle-cache-prime] permissions: actions: read contents: read @@ -76,7 +111,7 @@ jobs: # works after Hibernate's ddl-auto=update migrates the schema. Gated on # the `project` filter so doc-only PRs skip this ~5-minute job. if: needs.files-changed.outputs.project == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/db-migration-test.yml @@ -84,7 +119,7 @@ jobs: check-generateOpenApiDocs: if: needs.files-changed.outputs.openapi == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/check-openapi.yml @@ -120,7 +155,7 @@ jobs: playwright-e2e-live: if: needs.files-changed.outputs.frontend == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/e2e-live.yml @@ -128,7 +163,7 @@ jobs: playwright-e2e-enterprise: if: needs.files-changed.outputs.proprietary == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/build-enterprise.yml @@ -136,7 +171,7 @@ jobs: check-licence: if: needs.files-changed.outputs.build == 'true' - needs: [files-changed, build] + needs: [files-changed, build, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/check-licence.yml @@ -144,7 +179,7 @@ jobs: docker-compose-tests: if: needs.files-changed.outputs.project == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: actions: write contents: read @@ -156,7 +191,7 @@ jobs: test-build-docker-images: if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true' - needs: [files-changed, build, check-generateOpenApiDocs, check-licence] + needs: [files-changed, build, check-generateOpenApiDocs, check-licence, gradle-cache-prime] permissions: contents: read packages: read @@ -199,7 +234,7 @@ jobs: # frontend filter, so a CSS-only PR does not pay for a backend build. generated-models: if: needs.files-changed.outputs.generated-models == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read pull-requests: write diff --git a/.github/workflows/check-generated-models.yml b/.github/workflows/check-generated-models.yml index a758c4268e..a8559a7d78 100644 --- a/.github/workflows/check-generated-models.yml +++ b/.github/workflows/check-generated-models.yml @@ -42,10 +42,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.0 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index c9f64a0b93..61d21c0501 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -26,20 +26,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index eb89d32629..c188e8ff23 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -27,20 +27,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/coverage-aggregate.yml b/.github/workflows/coverage-aggregate.yml index e3a871b08e..1ece083252 100644 --- a/.github/workflows/coverage-aggregate.yml +++ b/.github/workflows/coverage-aggregate.yml @@ -46,20 +46,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -81,7 +77,7 @@ jobs: # Each lands as a sibling dir under coverage-execs/, with the .exec # files preserving their original relative paths. - name: Download all .exec artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: jacoco-exec-* path: coverage-execs/ @@ -206,7 +202,7 @@ jobs: # absence on backend-only runs by skipping the download entirely # when the producer job was not part of this workflow run. if: inputs.frontend-validation-result == 'success' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: frontend-coverage path: matrix-inputs/vitest/ @@ -216,7 +212,7 @@ jobs: # e2e-live uploads the artifact with a stable name. Skip the # download entirely when the producer job did not run. if: inputs.playwright-e2e-live-result == 'success' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: playwright-frontend-coverage path: matrix-inputs/playwright/ diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index 8bd28fc060..edb2d0547c 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -30,23 +30,19 @@ jobs: java-version: 25 distribution: temurin - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - # No `-PnoSpotless` here yet because the upstream cache layer matches the - # backend build's; reuse keeps cold-cache cost identical. + # Keep the normal formatting path here so this smoke test exercises the + # same Gradle configuration as the backend build. - name: Build Stirling-PDF JAR env: MAVEN_USER: ${{ secrets.MAVEN_USER }} diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index 9b81f774ef..049db8adc0 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -38,20 +38,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- # When the PR changes the base image, test.sh builds it locally # (stirling-pdf-base:local) into the daemon image store. A buildx diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 302663cbf9..0e64b89099 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -25,21 +25,16 @@ jobs: with: java-version: "25" distribution: "temurin" - # Same cache layer as backend-build.yml. Without it every run resolved the - # whole classpath cold and eventually got HTTP 429 from Maven Central. - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- # Gradle does not retry 429s, and a cold cache resolving the buildscript # classpath is exactly where Maven Central rate-limits us. Retry it here, # where a failure is cheap, instead of inside the backgrounded bootRun. diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index 8c852ae08f..1f901a7f05 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -350,10 +350,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 8303cf102a..f5a99a0ef2 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -57,20 +57,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependencies + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} restore-keys: | - gradle-${{ runner.os }}- - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -151,10 +147,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Setup Node.js if: matrix.variant.build_frontend == true @@ -255,10 +257,16 @@ jobs: java-version: "25" distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 12fcd23f6c..f4416a726d 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -65,20 +65,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependencies + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} restore-keys: | - gradle-${{ runner.os }}- - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Set up Docker Buildx id: buildx diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index ffbeafdd1e..76b28c4566 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -39,10 +39,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Generate Swagger documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index 415e8be4e5..09422a8093 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -179,10 +179,16 @@ jobs: java-version: "25" distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Setup Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index d935cac53d..7f8c72d41e 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -84,20 +84,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts + - name: Cache Gradle User Home uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | + ~/.gradle/caches ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index a7f74df4a7..a8d777744a 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -38,10 +38,16 @@ jobs: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Cache Gradle User Home + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - gradle-version: 9.6.1 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + restore-keys: | + gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- + gradle-${{ runner.os }}-${{ runner.arch }}- - name: Build with Gradle run: ./gradlew build From 8094765babe12b96d46e5d61f1d258fa29b1eb81 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 6 Aug 2026 13:44:12 +0200 Subject: [PATCH 023/154] build(licenses): Module-specific license. Add dependency overrides. (#7049) # Description of Changes This change adds a version-scoped override mechanism for dependencies whose published metadata does not expose a detectable license. - Added `app/license-overrides.json` with verified Apache License 2.0 metadata for: - `com.hubspot.immutables:immutables-exceptions:1.9` - `com.hubspot:algebra:1.5` - Added `ModuleLicenseOverrideFilter` as custom `buildSrc` logic for the Gradle dependency license report plugin. - Applied overrides only when the exact `group:artifact:version` matches and no usable license metadata was detected. - Added automatic maintenance of the override file: - Removes overrides when the dependency is no longer resolved. - Removes overrides when the dependency starts publishing valid license metadata. - Migrates stale overrides to newer unresolved versions and clears their metadata for re-verification. - Adds null-valued placeholders for newly detected dependencies without license metadata. - Preserves populated overrides for newer versions when already present. - Added Gradle version-aware dependency ordering for override migration. - Registered `app/license-overrides.json` as an input for license-report and license-check preparation tasks. - Centralized the dependency license report plugin version in `buildSrc`. - Added unit tests covering override application, cleanup, migration, exact-version matching, concurrent versions, placeholder generation, and numeric version ordering. - Added documentation describing the override lifecycle, verification requirements, maintenance workflow, and validation commands. - Replaced broad null-license allowances for the two HubSpot modules with explicit Apache License 2.0 metadata. - Added accepted GNU Lesser General Public License name variants encountered in dependency metadata. The change was made because some dependencies have known upstream licenses but do not publish license metadata in a form detected by the Gradle license report plugin. Previously, these dependencies were permitted through module-specific null-license exceptions, leaving incomplete information in the generated report. The new mechanism supplies verified metadata without overriding valid metadata published by dependencies. --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- app/allowed-licenses.json | 16 +- app/license-overrides.json | 12 + build.gradle | 13 +- buildSrc/README.md | 168 +++++++++++ buildSrc/build.gradle | 22 ++ .../gradle/ModuleLicenseOverrideFilter.groovy | 173 ++++++++++++ .../ModuleLicenseOverrideFilterTest.groovy | 265 ++++++++++++++++++ docker/backend/Dockerfile | 2 + docker/embedded/Dockerfile | 2 + docker/embedded/Dockerfile.fat | 2 + docker/embedded/Dockerfile.ultra-lite | 2 + 11 files changed, 668 insertions(+), 9 deletions(-) create mode 100644 app/license-overrides.json create mode 100644 buildSrc/README.md create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy create mode 100644 buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy diff --git a/app/allowed-licenses.json b/app/allowed-licenses.json index 9b5ef66556..033661629f 100644 --- a/app/allowed-licenses.json +++ b/app/allowed-licenses.json @@ -156,6 +156,14 @@ "moduleName": ".*", "moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception" }, + { + "moduleName": ".*", + "moduleLicense": "GNU Lesser Public License" + }, + { + "moduleName": ".*", + "moduleLicense": "The GNU Lesser General Public License" + }, { "moduleName": "com.martiansoftware:jsap", "moduleLicense": "LGPL" @@ -224,14 +232,6 @@ "moduleName": "com.google.re2j:re2j", "moduleLicense": "Go License" }, - { - "moduleName": "com.hubspot:algebra", - "moduleLicense": null - }, - { - "moduleName": "com.hubspot.immutables:immutables-exceptions", - "moduleLicense": null - }, { "moduleName": ".*", "moduleLicense": "UnRar License" diff --git a/app/license-overrides.json b/app/license-overrides.json new file mode 100644 index 0000000000..0ea43fcaf9 --- /dev/null +++ b/app/license-overrides.json @@ -0,0 +1,12 @@ +{ + "com.hubspot.immutables:immutables-exceptions:1.9": { + "name": "The Apache License, Version 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "projectUrl": "https://github.com/HubSpot/hubspot-immutables/tree/58628096ac99b286fe4f8bfe12aa3cff0f0589d3" + }, + "com.hubspot:algebra:1.5": { + "name": "The Apache License, Version 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "projectUrl": "https://github.com/HubSpot/algebra/tree/5d42983fd3a26539df9ba2cbeac32a1bddce0494" + } +} diff --git a/build.gradle b/build.gradle index 2af5522a73..31fa6b1bdb 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "io.swagger.swaggerhub" version "1.3.2" id "com.diffplug.spotless" version "8.8.0" - id "com.github.jk1.dependency-license-report" version "3.1.2" + id "com.github.jk1.dependency-license-report" //id "nebula.lint" version "19.0.3" id "org.sonarqube" version "7.2.3.7755" } @@ -18,6 +18,7 @@ import groovy.xml.XmlSlurper import org.gradle.api.JavaVersion import org.gradle.api.tasks.testing.Test import org.gradle.jvm.toolchain.JavaLanguageVersion +import stirling.software.gradle.ModuleLicenseOverrideFilter ext { springBootVersion = "4.0.6" @@ -550,6 +551,7 @@ gradle.taskGraph.whenReady { graph -> } def allProjects = ((subprojects as Set) + project) as Set +def moduleLicenseOverridesFile = project.layout.projectDirectory.file("app/license-overrides.json").asFile licenseReport { projects = allProjects @@ -557,6 +559,15 @@ licenseReport { allowedLicensesFile = project.layout.projectDirectory.file("app/allowed-licenses.json").asFile outputDir = project.layout.buildDirectory.dir("reports/dependency-license").get().asFile.path configurations = [ "productionRuntimeClasspath", "runtimeClasspath" ] + filters = [new ModuleLicenseOverrideFilter(moduleLicenseOverridesFile)] +} + +tasks.named('generateLicenseReport') { + inputs.file(moduleLicenseOverridesFile) +} + +tasks.named('checkLicensePreparation') { + inputs.file(moduleLicenseOverridesFile) } // Configure the forked spring boot run task to properly delegate to the stirling-pdf module diff --git a/buildSrc/README.md b/buildSrc/README.md new file mode 100644 index 0000000000..bbcfb0de6c --- /dev/null +++ b/buildSrc/README.md @@ -0,0 +1,168 @@ +# Dependency license overrides + +The backend dependency license report is generated by the +[`com.github.jk1.dependency-license-report`](https://github.com/jk1/Gradle-License-Report) +Gradle plugin. Most license information is read from dependency POM files, manifests, or packaged +license files. Some artifacts do not publish license metadata in a form the plugin can detect, even +though the artifact has a known license. + +This directory contains the build logic used to provide narrowly scoped fallback license metadata +for those artifacts. + +## Files + +- `build.gradle` makes version 3.1.4 of the license report plugin available to the custom build + logic. The root build applies that plugin without a second version declaration so both use the + same classpath. +- `src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy` implements the + plugin's `DependencyFilter` interface. +- `../app/license-overrides.json` contains the actual module-specific fallback values. +- `../app/allowed-licenses.json` defines which detected or supplied licenses are accepted by + `checkLicense`. + +## How it works + +The root `build.gradle` passes `app/license-overrides.json` to +`ModuleLicenseOverrideFilter`: + +```groovy +filters = [new ModuleLicenseOverrideFilter(moduleLicenseOverridesFile)] +``` + +For every dependency discovered by the license plugin, the filter builds an identifier in this +format: + +```text +group:artifact:version +``` + +The filter applies a populated override only when both conditions are true: + +1. The complete identifier, including the version, exists in `app/license-overrides.json`. +2. The plugin did not discover a non-empty license name for that dependency. + +When both conditions match, the filter adds the configured license as fallback manifest metadata. +The normal report renderer and `checkLicense` then consume that metadata in the same way as +metadata discovered from the dependency itself. + +An override never replaces a license that the plugin already detected. Updating a dependency also +does not silently reuse the override because a different version produces a different identifier. + +Overrides are temporary fallbacks, not a permanent license catalog. If the plugin starts detecting +the original license for an overridden module, the filter automatically removes that exact entry +from `app/license-overrides.json` and logs the cleanup. When the overridden version is no longer +resolved, a newer resolved version takes its place: if it declares a license, the stale entry is +removed; otherwise the entry moves to the new exact version and its values are cleared for +re-verification. An already populated entry for the new version is preserved. If no higher version +is resolved, the unused override is removed instead. + +Because the report aggregates several projects and configurations, multiple versions of the same +`group:artifact` can be present at once. An override is retained whenever its exact version is still +resolved. Only when that exact version is absent may the filter treat a higher version as an update; +version ordering then follows Gradle's own dependency version comparator. Overrides for dependency +versions that are no longer resolved and have no higher replacement are deleted automatically. + +The filter also records every resolved dependency without detected license metadata that has no +override yet. It writes a placeholder with `null` values for `name`, `url`, and `projectUrl`. +Placeholders deliberately do not affect the generated report until `name` is filled in. This makes +new missing metadata visible in the source-controlled override file instead of only in a generated +report. Review and fill or remove every new placeholder before committing the resulting JSON. + +## Adding an override + +First verify the license from an authoritative source such as the upstream repository, the +published artifact metadata, or the license file shipped inside the artifact. Do not infer a +license from the organization name or from a related artifact. + +Add an entry to `app/license-overrides.json`: + +```json +{ + "com.example:example-library:1.2.3": { + "name": "Apache License, Version 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0", + "projectUrl": "https://github.com/example/example-library/tree/0123456789abcdef0123456789abcdef01234567" + } +} +``` + +The key must contain the exact resolved version. `name` must be non-empty for the override to be +applied. `url` should point to the canonical license text. `projectUrl` must point to the immutable +Git tree for the exact module version, using the commit hash at which that version was introduced: + +```text +https://github.com///tree/ +``` + +Do not use the repository's default branch or another moving URL. See the existing entries in +`app/license-overrides.json` for concrete examples. + +If the license name is not already accepted, add a suitably narrow rule to +`app/allowed-licenses.json`. Adding an override and allowing a license are separate operations: + +- `license-overrides.json` supplies missing metadata for a specific artifact version. +- `allowed-licenses.json` defines the policy enforced by `checkLicense`. + +## Updating a dependency + +When an overridden dependency changes version: + +1. Verify the license for the new version again. +2. Run the license report so the filter can move the old key or add a placeholder for the new full + `group:artifact:version` key. +3. Re-verify and fill the license values and the version's immutable Git-tree `projectUrl`; moved + values are intentionally cleared because a license conclusion for one release is not assumed + for another. +4. Regenerate and inspect the report. + +If the new artifact publishes usable license metadata, no override is necessary. The next license +report or license check removes the old entry from `app/license-overrides.json` automatically. The +file must contain only overrides that are still needed. + +## Verification + +Run the filter unit tests: + +```powershell +.\gradlew.bat -p buildSrc test +``` + +The tests use `com.example:example-library` versions 1.4 and 1.7 to cover the missing +metadata fallback, placeholder creation, version migration, preservation of a populated newer +override, automatic cleanup after license metadata appears, exact-version matching, and concurrent +resolved versions. They also verify removal when a dependency version disappears. A separate `1.9` +to `1.11.0` case verifies numeric Gradle version ordering. + +Run the normal backend license workflow from the repository root: + +```powershell +task backend:licenses:generate +``` + +Then inspect: + +- `build/reports/dependency-license/index.json` for the rendered module, version, license name, and + URL. +- `build/reports/dependency-license/dependencies-without-allowed-license.json` when `checkLicense` + reports a policy failure. + +Also run the backend quality gate after changing the filter or its build wiring: + +```powershell +task backend:check +``` + +The override JSON is registered as an input of `generateLicenseReport` and +`checkLicensePreparation`, so changing the file invalidates the corresponding Gradle task outputs. + +## What not to do + +- Do not use an unversioned key. It cannot match the filter and would make the intended scope + ambiguous. +- Do not use an override to replace valid license metadata published by a dependency. +- Do not add an empty license to `allowed-licenses.json` merely to silence `checkLicense`; that + would still leave the generated report without useful license information. +- Do not exclude a dependency from the report solely because it is transitive. Runtime transitive + dependencies are still distributed components and their licenses remain relevant. +- Do not edit generated files under `build/reports/dependency-license` or the copied static license + report by hand. diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000000..25eee5c0f5 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'groovy' +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation localGroovy() + implementation gradleApi() + implementation 'com.github.jk1:gradle-license-report:3.1.4' + testImplementation platform('org.junit:junit-bom:6.1.2') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' + testLogging.showStandardStreams = true +} diff --git a/buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy b/buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy new file mode 100644 index 0000000000..df0d76158c --- /dev/null +++ b/buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy @@ -0,0 +1,173 @@ +package stirling.software.gradle + +import com.github.jk1.license.License +import com.github.jk1.license.ManifestData +import com.github.jk1.license.ModuleData +import com.github.jk1.license.ProjectData +import com.github.jk1.license.filter.DependencyFilter +import com.github.jk1.license.render.LicenseDataCollector +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.DefaultVersionComparator +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.Version +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionParser + +class ModuleLicenseOverrideFilter implements DependencyFilter { + private static final VersionParser VERSION_PARSER = new VersionParser() + private static final Comparator VERSION_COMPARATOR = + new DefaultVersionComparator().asVersionComparator() + + private final File overridesFile + + ModuleLicenseOverrideFilter(File overridesFile) { + this.overridesFile = overridesFile + } + + @Override + ProjectData filter(ProjectData projectData) { + Map> overrides = loadOverrides() + List modules = projectData.configurations + .collectMany { configuration -> configuration.dependencies } + Map> modulesByCoordinate = modules + .groupBy { module -> moduleCoordinate(module) } + + boolean overridesChanged = false + overrides.keySet().toList().each { overrideId -> + ModuleCoordinates overrideModule = parseModuleId(overrideId) + List coordinateModules = modulesByCoordinate[overrideModule.coordinate] + ModuleData currentModule = coordinateModules + ?.find { module -> module.version == overrideModule.version } + if (currentModule == null) { + currentModule = newestModule(coordinateModules, overrideModule.version) + } + if (currentModule == null) { + overrides.remove(overrideId) + overridesChanged = true + projectData.project.logger.lifecycle( + "Removed unused license override for ${overrideId}: " + + 'dependency version is no longer resolved') + return + } + + if (hasDeclaredLicense(currentModule)) { + overrides.remove(overrideId) + overridesChanged = true + projectData.project.logger.lifecycle( + "Removed stale license override for ${overrideId}: " + + "${moduleId(currentModule)} now declares a license") + return + } + + if (compareVersions(currentModule.version, overrideModule.version) > 0) { + String currentModuleId = moduleId(currentModule) + overrides.remove(overrideId) + if (!overrides.containsKey(currentModuleId)) { + overrides[currentModuleId] = [name: null, url: null, projectUrl: null] + } + overridesChanged = true + projectData.project.logger.lifecycle( + "Updated license override from ${overrideId} to ${currentModuleId}: " + + 'newer dependency still declares no license') + } + } + + modules.groupBy { module -> moduleId(module) }.each { currentModuleId, matchingModules -> + ModuleData module = matchingModules.first() + if (!overrides.containsKey(currentModuleId) && !hasDeclaredLicense(module)) { + overrides[currentModuleId] = [name: null, url: null, projectUrl: null] + overridesChanged = true + projectData.project.logger.lifecycle( + "Added missing license override for ${currentModuleId}. " + + "Set 'name' and 'url' in ${overridesFile}.") + } + } + if (overridesChanged) { + saveOverrides(overrides) + } + + projectData.configurations.each { configuration -> + configuration.dependencies.each { module -> applyOverride(module, overrides) } + } + return projectData + } + + private void applyOverride( + ModuleData module, Map> overrides) { + String moduleId = moduleId(module) + Map override = overrides[moduleId] + if (override == null) { + return + } + + String licenseName = override.name + String licenseUrl = override.url + String projectUrl = override.projectUrl + if (licenseName == null || licenseName.isBlank()) { + return + } + + Set licenses = [new License(licenseName, licenseUrl)] as LinkedHashSet + ManifestData manifest = + new ManifestData(module.name, module.version, null, null, projectUrl, licenses, false) + Set manifests = new LinkedHashSet<>(module.manifests ?: []) + manifests.add(manifest) + module.manifests = manifests + } + + private Map> loadOverrides() { + Object parsed = new JsonSlurper().parse(overridesFile) + if (!(parsed instanceof Map)) { + throw new IllegalArgumentException( + "License overrides file ${overridesFile} must contain a JSON object") + } + return parsed as Map> + } + + private void saveOverrides(Map> overrides) { + String json = JsonOutput.prettyPrint(JsonOutput.toJson(overrides)) + System.lineSeparator() + overridesFile.setText(json, 'UTF-8') + } + + private static String moduleId(ModuleData module) { + return "${module.group}:${module.name}:${module.version}" + } + + private static String moduleCoordinate(ModuleData module) { + return "${module.group}:${module.name}" + } + + private static ModuleCoordinates parseModuleId(String moduleId) { + List parts = moduleId.split(':', 3) as List + if (parts.size() != 3 || parts.any { part -> part.isBlank() }) { + throw new IllegalArgumentException( + "License override key ${moduleId} must use group:module:version") + } + return new ModuleCoordinates("${parts[0]}:${parts[1]}", parts[2]) + } + + private static ModuleData newestModule(List modules, String minimumVersion) { + return modules + ?.findAll { module -> compareVersions(module.version, minimumVersion) > 0 } + ?.max { left, right -> compareVersions(left.version, right.version) } + } + + private static int compareVersions(String left, String right) { + return VERSION_COMPARATOR.compare( + VERSION_PARSER.transform(left), VERSION_PARSER.transform(right)) + } + + private static boolean hasDeclaredLicense(ModuleData module) { + Set licenses = LicenseDataCollector.multiModuleLicenseInfo(module).licenses + return licenses.any { license -> license.name != null && !license.name.isBlank() } + } + + private static class ModuleCoordinates { + final String coordinate + final String version + + ModuleCoordinates(String coordinate, String version) { + this.coordinate = coordinate + this.version = version + } + } +} diff --git a/buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy b/buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy new file mode 100644 index 0000000000..7536d08af0 --- /dev/null +++ b/buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy @@ -0,0 +1,265 @@ +package stirling.software.gradle + +import com.github.jk1.license.ConfigurationData +import com.github.jk1.license.License +import com.github.jk1.license.ManifestData +import com.github.jk1.license.ModuleData +import com.github.jk1.license.ProjectData +import com.github.jk1.license.render.LicenseDataCollector +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import java.nio.file.Path +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertTrue + +class ModuleLicenseOverrideFilterTest { + private static final String GROUP = 'com.example' + private static final String MODULE = 'example-library' + private static final String VERSION_WITHOUT_LICENSE = '1.4' + private static final String VERSION_WITH_LICENSE = '1.7' + private static final String APACHE_NAME = 'Apache License, Version 2.0' + private static final String APACHE_URL = 'https://www.apache.org/licenses/LICENSE-2.0' + private static final String PROJECT_URL = 'https://github.com/HubSpot/hubspot-immutables' + + @TempDir + Path temporaryDirectory + + @Test + void keepsOverrideForVersionWithoutLicenseMetadata() { + ModuleData module = createModule(VERSION_WITHOUT_LICENSE, null) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertTrue(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void removesOverrideWhenLaterVersionDeclaresLicense() { + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData module = createModule(VERSION_WITH_LICENSE, publishedLicense) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void removesOverrideWhenOnlyOlderVersionIsResolved() { + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData module = createModule(VERSION_WITHOUT_LICENSE, publishedLicense) + File overridesFile = createOverridesFile(moduleId(VERSION_WITH_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITH_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void removesOverrideWhenModuleIsNoLongerResolved() { + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData()) + + assertTrue(readOverrides(overridesFile).isEmpty()) + } + + @Test + void keepsOverrideWhenExactAndNewerVersionsAreBothResolved() { + ModuleData olderModule = createModule(VERSION_WITHOUT_LICENSE, null) + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData newerModule = createModule(VERSION_WITH_LICENSE, publishedLicense) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + new ModuleLicenseOverrideFilter(overridesFile) + .filter(createProjectData(olderModule, newerModule)) + + Map overrides = readOverrides(overridesFile) + assertTrue(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(olderModule)) + assertEquals([APACHE_NAME], licenseNames(newerModule)) + } + + @Test + void movesOverrideUsingGradleNumericVersionOrdering() { + String oldVersion = '1.9' + String newVersion = '1.11.0' + ModuleData module = createModule(newVersion, null) + File overridesFile = createOverridesFile(moduleId(oldVersion)) + + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(oldVersion))) + assertEquals( + [name: null, url: null, projectUrl: null], overrides[moduleId(newVersion)]) + } + + @Test + void movesOverrideToLaterVersionWithoutLicenseAndClearsLicenseData() { + ModuleData module = createModule(VERSION_WITH_LICENSE, null) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals( + [name: null, url: null, projectUrl: null], + overrides[moduleId(VERSION_WITH_LICENSE)]) + assertTrue(licenseNames(module).isEmpty()) + } + + @Test + void preservesExistingOverrideWhenRemovingOlderVersion() { + ModuleData module = createModule(VERSION_WITH_LICENSE, null) + File overridesFile = createOverridesFile( + [ + (moduleId(VERSION_WITHOUT_LICENSE)): [ + name: APACHE_NAME, url: APACHE_URL + ], + (moduleId(VERSION_WITH_LICENSE)): [ + name: APACHE_NAME, url: APACHE_URL, projectUrl: PROJECT_URL + ] + ]) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals( + [name: APACHE_NAME, url: APACHE_URL, projectUrl: PROJECT_URL], + overrides[moduleId(VERSION_WITH_LICENSE)]) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void addsMissingOverrideForModuleWithoutLicense() { + ModuleData module = createModule(VERSION_WITHOUT_LICENSE, null) + File overridesFile = createEmptyOverridesFile() + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertEquals( + [name: null, url: null, projectUrl: null], + overrides[moduleId(VERSION_WITHOUT_LICENSE)]) + assertTrue(licenseNames(module).isEmpty()) + } + + @Test + void doesNotAddOverrideForModuleWithLicense() { + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData module = createModule(VERSION_WITH_LICENSE, publishedLicense) + File overridesFile = createEmptyOverridesFile() + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + assertTrue(readOverrides(overridesFile).isEmpty()) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + private File createOverridesFile(String moduleId) { + Map> overrides = [ + (moduleId): [name: APACHE_NAME, url: APACHE_URL] + ] + return createOverridesFile(overrides) + } + + private File createOverridesFile(Map> overrides) { + File overridesFile = temporaryDirectory.resolve('license-overrides.json').toFile() + overridesFile.setText(JsonOutput.prettyPrint(JsonOutput.toJson(overrides)), 'UTF-8') + return overridesFile + } + + private File createEmptyOverridesFile() { + File overridesFile = temporaryDirectory.resolve('license-overrides.json').toFile() + overridesFile.setText('{}', 'UTF-8') + return overridesFile + } + + private static Map readOverrides(File overridesFile) { + return new JsonSlurper().parse(overridesFile) as Map + } + + private static void debugState(String stage, ModuleData module, File overridesFile) { + String resolvedModuleId = "${module.group}:${module.name}:${module.version}" + Map overrides = readOverrides(overridesFile) + System.out.println( + "[license-override-test] ${stage}: module=${resolvedModuleId}, " + + "licenses=${licenseNames(module)}, " + + "matchingOverride=${overrides.containsKey(resolvedModuleId)}, " + + "overrideKeys=${overrides.keySet().sort()}") + } + + private static ProjectData createProjectData(ModuleData module) { + return createProjectData(module as ModuleData[]) + } + + private static ProjectData createProjectData(ModuleData... modules) { + ConfigurationData configuration = + new ConfigurationData( + 'runtimeClasspath', modules as LinkedHashSet) + return new ProjectData( + ProjectBuilder.builder().build(), + [configuration] as LinkedHashSet) + } + + private static ModuleData createModule(String version, License license) { + Set manifests = new LinkedHashSet<>() + if (license != null) { + manifests.add( + new ManifestData( + MODULE, + version, + null, + null, + null, + [license] as LinkedHashSet, + false)) + } + return new ModuleData( + GROUP, + MODULE, + version, + true, + manifests, + new LinkedHashSet<>(), + new LinkedHashSet<>()) + } + + private static String moduleId(String version) { + return "${GROUP}:${MODULE}:${version}" + } + + private static List licenseNames(ModuleData module) { + Set licenses = LicenseDataCollector.multiModuleLicenseInfo(module).licenses + return licenses.collect { license -> license.name }.sort() + } +} diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 5c6680661f..ab79caf9f0 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -17,6 +17,8 @@ WORKDIR /app COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index 54458667be..4ae0ee81e7 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -30,6 +30,8 @@ WORKDIR /app COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index 9b2655987f..6e679b97c8 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -31,6 +31,8 @@ WORKDIR /app COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index bcf189e9ee..14f9e934d0 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -23,6 +23,8 @@ WORKDIR /app # Copy gradle files for dependency resolution COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ From ddc0baa41daa863c8952f071a5a275cf33f51f1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:00:50 +0000 Subject: [PATCH 024/154] build(deps-dev): bump ip-address from 10.2.0 to 10.4.0 in /frontend (#7278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.2.0 to 10.4.0.
    Release notes

    Sourced from ip-address's releases.

    v10.4.0

    What's Changed

    Full Changelog: https://github.com/beaugunderson/ip-address/compare/v10.3.1...v10.4.0

    v10.3.1

    Full Changelog: https://github.com/beaugunderson/ip-address/compare/v10.3.0...v10.3.1

    v10.3.0

    Full Changelog: https://github.com/beaugunderson/ip-address/compare/v10.2.2...v10.3.0

    v10.2.2

    Full Changelog: https://github.com/beaugunderson/ip-address/compare/v10.2.1...v10.2.2

    v10.2.1

    Full Changelog: https://github.com/beaugunderson/ip-address/compare/v10.2.0...v10.2.1

    Commits
    • fbb8db2 10.4.0
    • 45a2b11 Validate the byte arrays Address6 is given (#217)
    • bac8810 Keep the package loadable on node 12, and enforce it (#216)
    • 9b3d848 Add a security policy and a README section on security posture
    • e84a7b3 Order the README API reference Address4, Address6, AddressError
    • 015160b Collapse each class in the README API reference
    • 34061a8 Pin checkout and setup-node to commits in the release job
    • c5fae5d Pin action-gh-release to a commit and move it to 3.0.2
    • e0ef048 Replace CircleCI with GitHub Actions
    • 5e3ceb7 Add GitHub Actions CI across Node 20, 22, 24 and 25 (#213)
    • Additional commits viewable in compare view
    Maintainer changes

    This version was pushed to npm by GitHub Actions, a new releaser for ip-address since your current version.

    Install script changes

    This version adds prepare script that runs during installation. Review the package contents before updating.


    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ip-address&package-manager=npm_and_yarn&previous-version=10.2.0&new-version=10.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7db904ddf6..a5abc659e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10774,9 +10774,9 @@ } }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "dev": true, "license": "MIT", "engines": { From 7aeec93032a952c78cf10e77804e71052df1dc38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:01:20 +0100 Subject: [PATCH 025/154] build(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.2 (#7273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.2.
    Release notes

    Sourced from softprops/action-gh-release's releases.

    v3.0.2

    3.0.2 is a patch release focused on release reliability and compatibility. It reuses existing draft releases when publishing prereleases, supports replacing release assets on Gitea, hardens streamed asset uploads, and provides clearer release-creation diagnostics. It also includes TypeScript, coverage, and tooling maintenance merged since 3.0.1.

    This release fixes #795, #438, and #803. The upload transport hardening covers the historical failure reported in #790, although current hosted Node 24 runners did not reproduce it naturally. The diagnostics work is related to #786 and does not claim a reproducible release-creation fix.

    What's Changed

    Exciting New Features 🎉

    Bug fixes 🐛

    Other Changes 🔄

    v3.0.1

    3.0.1

    • maintenance release with updated dependencies
    Changelog

    Sourced from softprops/action-gh-release's changelog.

    3.0.2

    3.0.2 is a patch release focused on release reliability and compatibility. It reuses existing draft releases when publishing prereleases, supports replacing release assets on Gitea, hardens streamed asset uploads, and provides clearer release-creation diagnostics. It also includes TypeScript, coverage, and tooling maintenance merged since 3.0.1.

    This release fixes #795, #438, and #803. The upload transport hardening covers the historical failure reported in #790, although current hosted Node 24 runners did not reproduce it naturally. The diagnostics work is related to #786 and does not claim a reproducible release-creation fix.

    What's Changed

    Exciting New Features 🎉

    Bug fixes 🐛

    Other Changes 🔄

    3.0.1

    • maintenance release with updated dependencies

    3.0.0

    3.0.0 is a major release that moves the action runtime from Node 20 to Node 24. Use v3 on GitHub-hosted runners and self-hosted fleets that already support the Node 24 Actions runtime. v2.6.2 was the final Node 20-compatible release and is no longer maintained or supported.

    What's Changed

    Other Changes 🔄

    • Move the action runtime and bundle target to Node 24
    • Update @types/node to the Node 24 line and allow future Dependabot updates
    • Keep the floating major tag on v3; freeze v2 at the final v2.6.2 release

    ... (truncated)

    Commits
    • 3d0d988 release 3.0.2 (#818)
    • 7e13ed4 fix: clarify release creation 404 errors (#817)
    • e6c70a5 fix: replace existing release assets on Gitea (#816)
    • f345337 fix: publish existing draft releases as prereleases (#801)
    • d8a89a2 fix: upload small checksum assets reliably (#815)
    • 45ece40 chore(deps): remove unused TypeScript tooling (#814)
    • f6b913c feat: improve release error reporting and test coverage (#813)
    • 15f193d chore(deps): upgrade TypeScript to 7 (#812)
    • cc8268d chore(deps): bump actions/checkout in the github-actions group (#810)
    • fd0ed1e chore(deps): bump the npm group with 3 updates (#811)
    • Additional commits viewable in compare view

    Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | softprops/action-gh-release | [>= 2.2.a, < 2.3] |
    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=3.0.0&new-version=3.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/multiOSReleases.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index f5a99a0ef2..ebf2585bb1 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -902,7 +902,7 @@ jobs: # instead of silently shipping a broken auto-update. - name: Upload binaries to Release if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master' - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: tag_name: v${{ needs.determine-matrix.outputs.version }} # Don't regenerate/append notes on re-runs, and don't force this into the From 5c319f13cbbc72c48dcad93eda7a36e6cf44a4aa Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:13:30 +0000 Subject: [PATCH 026/154] Fix pt-BR download label mislabeled as "Baixar (JSON)" (#7043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - **What:** Corrected the pt-BR (Brazilian Portuguese) `download` translation from `"Baixar (JSON)"` to `"Baixar"` in `frontend/editor/public/locales/pt-BR/translation.toml`, in both the root table (line 32) and the `[fileManager]` section (line 3577). - **Why:** The generic `download` key flows through `useFileActionTerminology` (`download: t("download", "Download")`) into the shared download button rendered on tool-result screens (e.g. `ReviewToolStep`). Because the string was `"Baixar (JSON)"`, every tool's Download button showed "Baixar (JSON)" for pt-BR users — implying a JSON export regardless of the actual output format. This mislabeling was locale-wide (all pt-BR users, all tool downloads). Session autocapture confirmed the confusion: a pt-BR user on `/convert` repeatedly clicked a button whose text was exactly "Baixar (JSON)", then abandoned the flow. Nothing crashed — it's a confusing label, not a functional break. - **Scope / verification:** en-US uses plain `"Download"` for this key and pt-PT already uses `"Transferir"`; no other locale carried the `"(JSON)"` suffix on the download key, so the defect was isolated to pt-BR. Only translation values changed — no keys added/removed, so translation counts are unaffected. Note: I scoped this to the mislabel — the exact symptom users observed. The report also mentions the download being a silent anchor-click with no success toast; that's a separate, broader UX enhancement in `ReviewToolStep`/`WorkbenchBar`/`downloadService`, so it's intentionally left out of this focused translation fix. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Translations (if applicable) - [x] Only a value correction in `pt-BR`; no translation tags added or removed. --- *Created with [PostHog Code](https://posthog.com/code?ref=pr) from [an inbox report](posthog-code://inbox/019f655d-bd60-78c2-ba59-98c23243ed57).* Co-authored-by: posthog-eu[bot] <226701856+posthog-eu[bot]@users.noreply.github.com> --- frontend/editor/public/locales/pt-BR/translation.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/editor/public/locales/pt-BR/translation.toml b/frontend/editor/public/locales/pt-BR/translation.toml index 6fbc440d20..689d72b58c 100644 --- a/frontend/editor/public/locales/pt-BR/translation.toml +++ b/frontend/editor/public/locales/pt-BR/translation.toml @@ -29,7 +29,7 @@ customTextTooltip = "Formato personalizado opcional para os números de página. delete = "Apagar" details = "Detalhes" discardChanges = "Descartar alterações" -download = "Baixar (JSON)" +download = "Baixar" downloadPdf = "Baixar PDF" downloadUnavailable = "Download indisponível para este item" edit = "Editar" @@ -3574,7 +3574,7 @@ deleteAll = "Excluir tudo" deleteSelected = "Apagar Selecionados" deselectAll = "Desselecionar Tudo" details = "Detalhes do arquivo" -download = "Baixar (JSON)" +download = "Baixar" downloadSelected = "Baixar selecionados" dropFilesHere = "Solte os arquivos aqui" fileFormat = "Formato" From 94fbc74271a44f4817672e71580df45f5f8c156f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:20:17 +0100 Subject: [PATCH 027/154] build(deps): bump the uv group across 1 directory with 3 updates (#7287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the uv group with 3 updates in the /engine directory: [cryptography](https://github.com/pyca/cryptography), [aiohttp](https://github.com/aio-libs/aiohttp) and [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator). Updates `cryptography` from 49.0.0 to 50.0.0
    Changelog

    Sourced from cryptography's changelog.

    50.0.0 - 2026-07-31

    
    * **SECURITY ISSUE**:
    
    :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_der`
    and its PEM and S/MIME variants no longer expose distinguishable errors
    or
    timing when unwrapping a ``RecipientInfo``'s ``encryptedKey``, which
    could
    act as a Bleichenbacher oracle for callers that decrypt untrusted
    messages.
    A random key is now substituted on failure, as described in :rfc:`3218`.
      Credit to **@X1AOxiang** for reporting the issue. **CVE-2026-69247**
    * Deprecated Diffie-Hellman key exchange over finite fields (FFDH).
      Everything FFDH is deprecated, including the types in
    ``cryptography.hazmat.primitives.asymmetric.dh`` and loading FFDH keys
    or
      parameters with the key loading APIs. Users should migrate to a more
      modern key exchange algorithm.
    * Added ``xof()`` class methods to
      :class:`~cryptography.hazmat.primitives.hashes.SHAKE128` and
    :class:`~cryptography.hazmat.primitives.hashes.SHAKE256` for
    constructing
      algorithm instances configured for use with
      :class:`~cryptography.hazmat.primitives.hashes.XOFHash`.
    * The :mod:`X.509 verification <cryptography.x509.verification>`
    APIs are now
      considered stable and are subject to our API stability policy.
    * Added the :doc:`/cobblestone` recipe, an implementation of the
      Cobblestone-128 and Cobblestone-256 instantiations of the `C2SP
      chunked-encryption specification
    <https://c2sp.org/chunked-encryption>`_ for streaming
    authenticated
      encryption of large messages.
    * Parsing a Signed Certificate Timestamp list now rejects encodings that
    carry trailing bytes after the list or after an individual SCT, instead
    of
      silently ignoring them.
    * Added support for using :class:`~cryptography.x509.Name` as a field
    type in
      the :doc:`/hazmat/asn1/index` module.
    * Loading a public key or an EC private key now rejects DER where the
    ``subjectPublicKey`` (or EC ``publicKey``) ``BIT STRING`` declares a
    non-zero
      number of unused bits, instead of silently ignoring it.
    * Parsing a CRL entry's ``InvalidityDate`` extension now rejects a
    ``GeneralizedTime`` that carries fractional seconds or another non-DER
    form,
    matching the strict encoding already required for every other X.509 time
      field.
    * :func:`~cryptography.x509.ocsp.load_der_ocsp_request` and
    :func:`~cryptography.x509.ocsp.load_der_ocsp_response` now reject a
    request
    or response whose ``version`` field is not ``v1``, the only version
    defined
    by RFC 6960, matching the version validation already performed when
    loading
      certificates, CSRs and CRLs.
    * :class:`~cryptography.hazmat.primitives.hashes.XOFHash` is now
    supported
      when building against AWS-LC.
    * HMAC (and therefore PBKDF2-HMAC) with SHA-3 hashes is now supported
    when
      building against AWS-LC.
    * Diffie-Hellman (:doc:`/hazmat/primitives/asymmetric/dh`) is now
    supported
      when building against AWS-LC.
    </tr></table>
    

    ... (truncated)

    Commits

    Updates `aiohttp` from 3.14.1 to 3.14.3
    Changelog

    Sourced from aiohttp's changelog.

    3.14.3 (2026-07-22)

    Bug fixes

    • Fixed the client dropping only the first Authorization, Cookie and Proxy-Authorization header when a redirect crossed an origin -- by :user:arshsmith1.

      Related issues and pull requests on GitHub: :issue:13180.

    • Fixed error message construction in the C HTTP parser -- by :user:bdraco.

      Related issues and pull requests on GitHub: :issue:13222.


    3.14.2 (2026-07-20)

    Bug fixes

    • Fixed :py:attr:~aiohttp.web.StreamResponse.last_modified rounding a :class:datetime.datetime with a fractional second down.

      Related issues and pull requests on GitHub: :issue:5303.

    • Fixed resolving localhost on Windows to fall back without AI_ADDRCONFIG when the first lookup fails, so localhost still works without an active network.

      Related issues and pull requests on GitHub: :issue:5357.

    ... (truncated)

    Commits

    Updates `datamodel-code-generator` from 0.56.0 to 0.64.0
    Release notes

    Sourced from datamodel-code-generator's releases.

    0.64.0

    Breaking Changes

    Code Generation Changes

    • Self-referencing fields are now quoted with --disable-future-imports - When --disable-future-imports is set (no from __future__ import annotations and no native PEP 649 deferred evaluation on Python < 3.14), self-referencing and forward-referencing field annotations in regular BaseModel classes are now emitted as quoted forward references instead of bare names. Previously such annotations were left unquoted, producing invalid code that raised NameError (Ruff F821) at class-evaluation time. Output for the common case (with from __future__ import annotations or Python 3.14 native deferred annotations) is unchanged. Users who snapshot/golden-file generated output for the --disable-future-imports configuration with self-referencing models will see the annotation change from unquoted to quoted, e.g. children: Optional[List[Node]]children: Optional[List["Node"]]. (#3387)

    What's Changed

    ... (truncated)

    Changelog

    Sourced from datamodel-code-generator's changelog.

    0.64.0 - 2026-06-14

    Breaking Changes

    Code Generation Changes

    • Self-referencing fields are now quoted with --disable-future-imports - When --disable-future-imports is set (no from __future__ import annotations and no native PEP 649 deferred evaluation on Python < 3.14), self-referencing and forward-referencing field annotations in regular BaseModel classes are now emitted as quoted forward references instead of bare names. Previously such annotations were left unquoted, producing invalid code that raised NameError (Ruff F821) at class-evaluation time. Output for the common case (with from __future__ import annotations or Python 3.14 native deferred annotations) is unchanged. Users who snapshot/golden-file generated output for the --disable-future-imports configuration with self-referencing models will see the annotation change from unquoted to quoted, e.g. children: Optional[List[Node]]children: Optional[List["Node"]]. (#3387)

    What's Changed

    ... (truncated)

    Commits

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- engine/pyproject.toml | 2 +- engine/uv.lock | 302 +++++++++++++++++++++--------------------- 2 files changed, 152 insertions(+), 152 deletions(-) diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 8cbb94daf8..29f2dcbdf7 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "AI Document Engine" requires-python = ">=3.13" dependencies = [ - "cryptography>=44.0.0", + "cryptography>=50.0.0", "fastapi>=0.116.0", "pgvector>=0.3.6", "psycopg[binary,pool]>=3.2", diff --git a/engine/uv.lock b/engine/uv.lock index fd10a81182..4f0ff52acd 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -41,7 +41,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -52,72 +52,72 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, - { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, - { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, - { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, - { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -473,52 +473,52 @@ wheels = [ [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, ] [[package]] @@ -538,21 +538,21 @@ wheels = [ [[package]] name = "datamodel-code-generator" -version = "0.56.0" +version = "0.64.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, - { name = "black" }, + { name = "black", marker = "sys_platform != 'emscripten'" }, { name = "genson" }, { name = "inflect" }, - { name = "isort" }, + { name = "isort", marker = "sys_platform != 'emscripten'" }, { name = "jinja2" }, { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/7d/7fc2bb3d8946ca45851da3f23497a2c6e252e92558ccbd89d609cf1e13d4/datamodel_code_generator-0.56.0.tar.gz", hash = "sha256:e7c003fb5421b890aabe12f66ae65b57198b04cfe1da7c40810798020835b3a8", size = 837708, upload-time = "2026-04-04T09:46:19.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/d2/86c94a2836ed42231653a7ddaefa0a5bc23418167a876bba7376c96b3a35/datamodel_code_generator-0.64.0.tar.gz", hash = "sha256:9c592900a00b20e416494273c22435f5a9aef6ea8c7b9190747522a60497a1cb", size = 1316440, upload-time = "2026-06-14T17:24:50.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/3a/7f169ffc7a2d69a4f9158b1ac083f685b7f4a1a8a1db5d1e4abbb4e741b7/datamodel_code_generator-0.56.0-py3-none-any.whl", hash = "sha256:a0559683fbe90cdf2ce9b6637e3adae3e3a8056a8d0516df581d486e2834ead2", size = 256545, upload-time = "2026-04-04T09:46:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/23/94/71338e2f0146ac10747a5537b3a1e45256e66b7c229869eb0ee787111b41/datamodel_code_generator-0.64.0-py3-none-any.whl", hash = "sha256:b7cd8bd41a312aa997aec6150670bad781847c5b674f17e4d70e78208a0fb990", size = 374698, upload-time = "2026-06-14T17:24:48.809Z" }, ] [package.optional-dependencies] @@ -632,7 +632,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "cryptography", specifier = ">=44.0.0" }, + { name = "cryptography", specifier = ">=50.0.0" }, { name = "fastapi", specifier = ">=0.116.0" }, { name = "opentelemetry-sdk", specifier = ">=1.39.0" }, { name = "pgvector", specifier = ">=0.3.6" }, @@ -800,7 +800,7 @@ name = "ffmpeg-python" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future", marker = "python_full_version < '3.14'" }, + { name = "future" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } wheels = [ @@ -1346,7 +1346,7 @@ name = "jsonpatch" version = "1.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonpointer", marker = "python_full_version < '3.14'" }, + { name = "jsonpointer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } wheels = [ @@ -1444,15 +1444,15 @@ name = "langchain-core" version = "1.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonpatch", marker = "python_full_version < '3.14'" }, - { name = "langchain-protocol", marker = "python_full_version < '3.14'" }, - { name = "langsmith", marker = "python_full_version < '3.14'" }, - { name = "packaging", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "pyyaml", marker = "python_full_version < '3.14'" }, - { name = "tenacity", marker = "python_full_version < '3.14'" }, - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, - { name = "uuid-utils", marker = "python_full_version < '3.14'" }, + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" } wheels = [ @@ -1464,7 +1464,7 @@ name = "langchain-protocol" version = "0.0.18" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } wheels = [ @@ -1476,7 +1476,7 @@ name = "langchain-text-splitters" version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core", marker = "python_full_version < '3.14'" }, + { name = "langchain-core" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } wheels = [ @@ -1488,20 +1488,20 @@ name = "langsmith" version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version < '3.14'" }, - { name = "distro", marker = "python_full_version < '3.14'" }, - { name = "httpx", marker = "python_full_version < '3.14'" }, - { name = "orjson", marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'" }, - { name = "packaging", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, - { name = "requests-toolbelt", marker = "python_full_version < '3.14'" }, - { name = "sniffio", marker = "python_full_version < '3.14'" }, - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, - { name = "uuid-utils", marker = "python_full_version < '3.14'" }, - { name = "websockets", marker = "python_full_version < '3.14'" }, - { name = "xxhash", marker = "python_full_version < '3.14'" }, - { name = "zstandard", marker = "python_full_version < '3.14'" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/26/b72987d947278f63ec1e85f01ce85ca7ab2621c7efc0845d4a3a8e5d5dfb/langsmith-0.9.1.tar.gz", hash = "sha256:e5eb905224d156bcece4985285c55b51fffcb06c9353b2c4adb42e1c48b0d05d", size = 4557557, upload-time = "2026-06-23T17:04:23.233Z" } wheels = [ @@ -2879,7 +2879,7 @@ name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ @@ -3343,16 +3343,16 @@ name = "voyageai" version = "0.3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp", marker = "python_full_version < '3.14'" }, - { name = "aiolimiter", marker = "python_full_version < '3.14'" }, - { name = "ffmpeg-python", marker = "python_full_version < '3.14'" }, - { name = "langchain-text-splitters", marker = "python_full_version < '3.14'" }, - { name = "numpy", marker = "python_full_version < '3.14'" }, - { name = "pillow", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, - { name = "tenacity", marker = "python_full_version < '3.14'" }, - { name = "tokenizers", marker = "python_full_version < '3.14'" }, + { name = "aiohttp" }, + { name = "aiolimiter" }, + { name = "ffmpeg-python" }, + { name = "langchain-text-splitters" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "tokenizers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/16/1b46b3cd401e1717a68197c1fe336d7bb4e0a1833f8105e1738f5b1add05/voyageai-0.3.7.tar.gz", hash = "sha256:826cd97f97223f42b5babc5c459c9c80f3a8215ce5c0e007b0b276550f790d24", size = 26485, upload-time = "2025-12-16T18:43:05.26Z" } wheels = [ From 8476d3cdec02ce59a51b055c8ade1e4405a322a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:20:27 +0000 Subject: [PATCH 028/154] build(deps-dev): bump eslint from 10.1.0 to 10.8.0 in /frontend in the eslint group across 1 directory (#7274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the eslint group with 1 update in the /frontend directory: [eslint](https://github.com/eslint/eslint). Updates `eslint` from 10.1.0 to 10.8.0
    Release notes

    Sourced from eslint's releases.

    v10.8.0

    Features

    • 2fee9bb feat: export ConfigObject from eslint/config (#21082) (sethamus)

    Bug Fixes

    • 6b8d2f7 fix: escape reserved characters in rule id in html formatter (#21129) (Francesco Trotta)
    • 9091071 fix: prevent no-unreachable-loop crash when all loop types are ignored (#21116) (Pixel)
    • e23fafe fix: prefer-object-spread add semicolon when adding parenthesis (#21081) (synthex-byte)
    • 20b5ad0 fix: quadratic-time regex in prefer-template (#21096) (Milos Djermanovic)
    • 8b6f6c0 fix: apply ignore configs to computed methods in class-methods-use-this (#21094) (Pixel)
    • b2c608c fix: NewExpression with parenthesized callee in preserve-caught-error (#21083) (Francesco Trotta)

    Documentation

    • 6ddf858 docs: fix broken Specify Parser Options anchor link (#21106) (Minsu)
    • 784dfbe docs: Clarify no-eq-null description (#21120) (Park Harin)
    • 7ec733a docs: Fix typos and grammar in glossary (#21095) (Marry (Subin Yang))
    • 92bb13f docs: replace quake link (#21108) (Jung Hyeon Jun)
    • 68eb4a5 docs: fix broken Specify Globals anchor links in rule pages (#21103) (Minsu)
    • d28f697 docs: replace Code Climate CLI links with Qlty CLI links (#21099) (Jung Hyeon Jun)
    • eccc68d docs: correct --suppressions-location option description (#21093) (Ga eun Lee)
    • c5963f7 docs: Update README (GitHub Actions Bot)

    Chores

    • 4fbf46d test: pin webpack version to 5.108.4 (#21137) (Francesco Trotta)
    • 2d063e2 chore: update HTTP URLs to HTTPS in JSDoc and comments (#21101) (Bo Hyun Kim)
    • eccbe7b test: add error locations to no-class-assign (#21123) (devoil)
    • e7d1e43 ci: bump actions/setup-go from 6 to 7 (#21118) (dependabot[bot])
    • e9d66d0 ci: bump actions/setup-node from 6 to 7 (#21119) (dependabot[bot])
    • ee225b6 test: Add error location details to no-eq-null rule (#21117) (Park Harin)
    • 044a627 chore: update minimatch to ^10.2.5 (#21107) (김채영)
    • fb09aa8 chore: update ecosystem plugins (#21115) (ESLint Bot)
    • 5abd878 test: add error locations to no-proto (#21114) (Gihyeon Jeong / 정기현)
    • 9715887 test: Add error location details to no-div-regex (#21110) (Park Harin)
    • a746ec6 test: add error locations to no-new-wrappers (#21109) (Gihyeon Jeong / 정기현)
    • 8dde645 test: add error locations to no-ex-assign (#21102) (devoil)
    • 13ab0ec test: add error locations to no-label-var (#21098) (Gihyeon Jeong / 정기현)
    • a99906f test: Add error location details to no-delete-var rule (#21105) (Park Harin)
    • c47e8dc chore: add missing backticks to languages/js/index.js (#21104) (beeen)
    • 0174428 chore: add missing backticks to translate-cli-options.js (#21097) (dongkyu lee)
    • 3d36589 chore: add missing backticks to serialization.js (#21091) (이규환)
    • dcc9312 test: add error locations to eqeqeq (#21090) (Ga eun Lee)
    • 2710b18 ci: Add explicit permissions to rebuild-docs-sites workflow (#21089) (Marry (Subin Yang))
    • 5d2f866 chore: update dependency prettier to v3.9.5 (#21086) (renovate[bot])
    • d584e31 chore: fix failing ecosystem test for eslint-plugin-unicorn (#21084) (Francesco Trotta)
    • bf3eda0 chore: update ecosystem plugins (#21079) (ESLint Bot)

    v10.7.0

    Features

    • cf2a9bf feat: add errorClassNames option to preserve-caught-error rule (#21032) (sethamus)
    • f8b873a feat: max-nested-callbacks option for constructor callbacks (#21063) (fnx)

    ... (truncated)

    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 65 ++++++++++++++++++++------------------ frontend/package.json | 2 +- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a5abc659e3..300a409229 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -119,7 +119,7 @@ "@vitest/coverage-v8": "^3.2.4", "dotenv": "^16.4.7", "dpdm": "^3.14.0", - "eslint": "^10.0.2", + "eslint": "^10.8.0", "fake-indexeddb": "^6.2.5", "jsdom": "^27.0.0", "json-schema-to-typescript": "^15.0.4", @@ -1961,13 +1961,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -1976,22 +1976,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2023,9 +2023,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2033,13 +2033,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -9286,18 +9286,21 @@ } }, "node_modules/eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", - "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.3", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -9319,7 +9322,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -12905,13 +12908,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" diff --git a/frontend/package.json b/frontend/package.json index dfcfa2842c..1798abc6b2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -141,7 +141,7 @@ "@vitest/coverage-v8": "^3.2.4", "dotenv": "^16.4.7", "dpdm": "^3.14.0", - "eslint": "^10.0.2", + "eslint": "^10.8.0", "fake-indexeddb": "^6.2.5", "jsdom": "^27.0.0", "json-schema-to-typescript": "^15.0.4", From de93a1b5e2685f2a6022f54ed1eb888c9b5de469 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:20:33 +0000 Subject: [PATCH 029/154] build(deps): bump org.sonarqube from 7.2.3.7755 to 7.3.1.8318 (#7271) Bumps org.sonarqube from 7.2.3.7755 to 7.3.1.8318. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.sonarqube&package-manager=gradle&previous-version=7.2.3.7755&new-version=7.3.1.8318)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 31fa6b1bdb..de0861d8fa 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { id "com.diffplug.spotless" version "8.8.0" id "com.github.jk1.dependency-license-report" //id "nebula.lint" version "19.0.3" - id "org.sonarqube" version "7.2.3.7755" + id "org.sonarqube" version "7.3.1.8318" } import com.github.jk1.license.render.* From ad8830b6459a74fd4f6b3b3cb479b97ae8dc9df4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:20:38 +0000 Subject: [PATCH 030/154] build(deps): bump com.sun.xml.bind:jaxb-core from 4.0.7 to 4.0.9 (#7270) Bumps com.sun.xml.bind:jaxb-core from 4.0.7 to 4.0.9. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.sun.xml.bind:jaxb-core&package-manager=gradle&previous-version=4.0.7&new-version=4.0.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/build.gradle b/app/core/build.gradle index 33aa679994..5e90672f75 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -57,7 +57,7 @@ dependencies { // veraPDF still uses javax.xml.bind, not the new jakarta namespace implementation 'javax.xml.bind:jaxb-api:2.3.1' implementation 'com.sun.xml.bind:jaxb-impl:2.3.9' - implementation 'com.sun.xml.bind:jaxb-core:4.0.7' + implementation 'com.sun.xml.bind:jaxb-core:4.0.9' // CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7) implementation "com.google.code.gson:gson:${gsonVersion}" From 74c53001cf4e652d96d2948d0b1058a827b98e24 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:05:24 +0100 Subject: [PATCH 031/154] fix(saas): give auth-bootstrap data fetching a single owner (#7194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## The problem Three pieces of user data (pro status, avatar metadata, profile picture) were being fetched from four different places: `initializeAuth` on mount, the `SIGNED_IN` handler, the `TOKEN_REFRESHED` handler, and the post-upgrade path. On a fresh login the first two both see a session, so everything got fetched twice. It didn't stop after login either — Supabase re-fires `SIGNED_IN` on token refresh and tab-visibility wakeups, so a refresh that emitted both events cost around 7 Supabase reads. ## The fix All four call sites now go through one `loadUserData(session)` that is idempotent per identity. The guard key is `user.id` + `is_anonymous`: - not the access token, which changes on every refresh and would defeat the guard entirely - the anonymous flag matters because a guest to authenticated upgrade keeps the same user id, and that is the one case where the data genuinely does need reloading **Per login: 6 fetches to 3. A repeat `SIGNED_IN` or `TOKEN_REFRESHED` fetches nothing.** The tests count real calls rather than asserting on shape. ## Two behaviour changes worth naming - `initializeAuth` now awaits the full load, so the initial spinner also waits on the profile-picture URL. Net login is still faster, since an entire duplicate pass is gone. - A tab-wake `SIGNED_IN` no longer revalidates entitlements. That revalidation was accidental rather than designed — `refreshProStatus()` is the intended path, and post-checkout is already handled by `CheckoutContext`. ## Scope Supabase-origin traffic only. This does not touch the ~20 authenticated requests hitting `SupabaseAuthenticationFilter`, because those go to the Stirling backend rather than the hosted Supabase project. That is a separate problem and is unmeasured, so it needs measuring before anything is optimised. Remaining items (a double `/api/v1/team/my` fetch, an effect keyed on `[user]` identity in `FolderContext`, the `portalAccess` spinner flash, and caching the auth filter's per-request Postgres round-trips) are tracked separately. ## Verification ``` npx tsc --noEmit --project editor/src/saas/tsconfig.json # exit 0 npx eslint --max-warnings=0 editor/src/saas/auth # exit 0 npx prettier --check editor/src/saas/auth/ # clean npx vitest run --project saas # 75 passed (20 files) ``` --- .../src/saas/auth/AuthProvider.test.tsx | 339 ++++++++++++++++++ frontend/editor/src/saas/auth/UseSession.tsx | 165 +++++---- 2 files changed, 427 insertions(+), 77 deletions(-) create mode 100644 frontend/editor/src/saas/auth/AuthProvider.test.tsx diff --git a/frontend/editor/src/saas/auth/AuthProvider.test.tsx b/frontend/editor/src/saas/auth/AuthProvider.test.tsx new file mode 100644 index 0000000000..588507d59c --- /dev/null +++ b/frontend/editor/src/saas/auth/AuthProvider.test.tsx @@ -0,0 +1,339 @@ +import { act, render, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Session, User } from "@supabase/supabase-js"; + +/** + * Request-count tests for {@link AuthProvider}'s data loading. It used to fetch + * pro status, avatar metadata and the picture from two places at once, and + * Supabase re-fires SIGNED_IN on token refresh and tab wakeups, so it kept + * happening. These pin the call counts. + */ + +type AuthCallback = (event: string, session: Session | null) => void; + +const rpc = vi.fn(); +const createSignedUrl = vi.fn(); +const storageFrom = vi.fn((_bucket: string) => ({ createSignedUrl })); +const getSession = vi.fn(); +const onAuthStateChange = vi.fn(); +const unsubscribe = vi.fn(); + +vi.mock("@app/auth/supabase", () => ({ + supabase: { + auth: { + getSession: () => getSession(), + onAuthStateChange: (cb: AuthCallback) => onAuthStateChange(cb), + refreshSession: vi + .fn() + .mockResolvedValue({ data: { session: null }, error: null }), + signOut: vi.fn().mockResolvedValue({ error: null }), + }, + rpc: (...args: unknown[]) => rpc(...args), + storage: { from: (bucket: string) => storageFrom(bucket) }, + }, + debugAuthEvents: vi.fn(), +})); + +const syncOAuthAvatar = vi.fn(); +const getProfilePictureMetadata = vi.fn(); + +vi.mock("@app/services/avatarSyncService", () => ({ + syncOAuthAvatar: (...args: unknown[]) => syncOAuthAvatar(...args), + getProfilePictureMetadata: (...args: unknown[]) => + getProfilePictureMetadata(...args), + getProviderAvatarUrl: () => null, +})); + +const synchronizeUserUpgrade = vi.fn(); + +vi.mock("@app/services/userService", () => ({ + synchronizeUserUpgrade: (...args: unknown[]) => + synchronizeUserUpgrade(...args), +})); + +// Imported after the mocks so the provider picks them up. +const { AuthProvider, useAuth } = await import("./UseSession"); + +/** Surfaces `loading` so a test can assert on it rather than on the container. */ +function LoadingProbe() { + const { loading } = useAuth(); + return {String(loading)}; +} + +const USER_ID = "11111111-2222-3333-4444-555555555555"; + +function makeSession( + overrides: { token?: string; userId?: string; anonymous?: boolean } = {}, +): Session { + const user = { + id: overrides.userId ?? USER_ID, + email: "someone@example.com", + is_anonymous: overrides.anonymous ?? false, + app_metadata: { provider: "google" }, + user_metadata: { full_name: "Some One" }, + } as unknown as User; + + return { + access_token: overrides.token ?? "token-1", + refresh_token: "refresh-1", + expires_in: 3600, + token_type: "bearer", + user, + } as unknown as Session; +} + +/** Total requests the provider makes per user-data load. */ +function callCounts() { + return { + proStatus: rpc.mock.calls.length, + metadata: getProfilePictureMetadata.mock.calls.length, + picture: createSignedUrl.mock.calls.length, + avatarSync: syncOAuthAvatar.mock.calls.length, + }; +} + +function renderProvider() { + let authCallback: AuthCallback = () => {}; + onAuthStateChange.mockImplementation((cb: AuthCallback) => { + authCallback = cb; + return { data: { subscription: { unsubscribe } } }; + }); + + const utils = render( + + + , + ); + + /** + * Deliver an auth event and let its work finish. The provider defers with + * setTimeout(0), so a microtask flush is not enough: without draining real + * macrotasks the assertions run before any refetch and prove nothing. + */ + const fire = async (event: string, s: Session | null) => { + await act(async () => { + authCallback(event, s); + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + }; + + return { ...utils, fire }; +} + +describe("AuthProvider user-data loading", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useRealTimers(); + sessionStorage.clear(); + + rpc.mockResolvedValue({ data: true, error: null }); + createSignedUrl.mockResolvedValue({ + data: { signedUrl: "https://example.test/avatar" }, + error: null, + }); + getProfilePictureMetadata.mockResolvedValue(null); + syncOAuthAvatar.mockResolvedValue(false); + synchronizeUserUpgrade.mockResolvedValue(undefined); + getSession.mockResolvedValue({ + data: { session: makeSession() }, + error: null, + }); + }); + + it("fetches each piece of user data exactly once on login", async () => { + const { fire } = renderProvider(); + + await waitFor(() => expect(createSignedUrl).toHaveBeenCalled()); + // The SIGNED_IN that follows a fresh login must not repeat the work. + await fire("SIGNED_IN", makeSession()); + + expect(callCounts()).toEqual({ + proStatus: 1, + metadata: 1, + picture: 1, + avatarSync: 1, + }); + }); + + it("does not refetch when SIGNED_IN repeats with a new access token", async () => { + const { fire } = renderProvider(); + await waitFor(() => expect(createSignedUrl).toHaveBeenCalled()); + const before = callCounts(); + + // What a tab-visibility wakeup or token refresh looks like: same user, + // different token. + await fire("SIGNED_IN", makeSession({ token: "token-2" })); + + expect(callCounts()).toEqual(before); + }); + + it("does not refetch on TOKEN_REFRESHED for the same identity", async () => { + const { fire } = renderProvider(); + await waitFor(() => expect(createSignedUrl).toHaveBeenCalled()); + const before = callCounts(); + + await fire("TOKEN_REFRESHED", makeSession({ token: "token-3" })); + + expect(callCounts()).toEqual(before); + }); + + it("keeps loading false across repeat auth events", async () => { + // Guards the Landing -> HomePage unmount: toggling `loading` on a wakeup + // would tear down the tree on every tab switch. + const { fire, getByTestId } = renderProvider(); + await waitFor(() => + expect(getByTestId("loading").textContent).toBe("false"), + ); + + await fire("SIGNED_IN", makeSession({ token: "token-4" })); + expect(getByTestId("loading").textContent).toBe("false"); + + await fire("TOKEN_REFRESHED", makeSession({ token: "token-5" })); + expect(getByTestId("loading").textContent).toBe("false"); + + expect(callCounts().proStatus).toBe(1); + }); + + it("clears the initial spinner without waiting for the avatar upload", async () => { + // syncOAuthAvatar re-uploads the provider image on a first login. Gating + // `loading` on it would stall account creation behind an image upload. + let releaseSync = () => {}; + syncOAuthAvatar.mockImplementationOnce( + () => + new Promise((resolve) => { + releaseSync = () => resolve(false); + }), + ); + + const { getByTestId } = renderProvider(); + + await waitFor(() => + expect(getByTestId("loading").textContent).toBe("false"), + ); + // The picture read chains behind the sync, so it has not run yet either. + expect(createSignedUrl).not.toHaveBeenCalled(); + + await act(async () => { + releaseSync(); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + }); + + it("refetches after a guest upgrade, which keeps the same user id", async () => { + // The upgrade path is the one case where the id is unchanged but the data + // must be reloaded - hence keying on is_anonymous, not the id alone. + getSession.mockResolvedValue({ + data: { session: makeSession({ anonymous: true }) }, + error: null, + }); + sessionStorage.setItem("pendingUpgrade", "true"); + sessionStorage.setItem("upgradeProvider", "google"); + + const { fire } = renderProvider(); + await waitFor(() => expect(rpc).toHaveBeenCalledTimes(1)); + + await fire("USER_UPDATED", makeSession({ anonymous: false })); + + await waitFor(() => expect(rpc).toHaveBeenCalledTimes(2)); + expect(synchronizeUserUpgrade).toHaveBeenCalledWith("google"); + }); + + it("does not drop an upgrade that lands while the guest load is in flight", async () => { + // Coalescing on "something is in flight" alone would hand the upgrade the + // guest's promise and never fetch the real user's data. + getSession.mockResolvedValue({ + data: { session: makeSession({ anonymous: true }) }, + error: null, + }); + sessionStorage.setItem("pendingUpgrade", "true"); + sessionStorage.setItem("upgradeProvider", "google"); + + let releaseGuestLoad = () => {}; + const guestLoadBlocked = new Promise((resolve) => { + releaseGuestLoad = resolve; + }); + rpc.mockImplementationOnce(async () => { + await guestLoadBlocked; + return { data: false, error: null }; + }); + + const { fire } = renderProvider(); + await waitFor(() => expect(rpc).toHaveBeenCalledTimes(1)); + + // Deliver the upgrade with the guest load still deliberately unsettled, so + // the guard genuinely has an in-flight load to reason about. + await fire("USER_UPDATED", makeSession({ anonymous: false })); + await waitFor(() => expect(rpc).toHaveBeenCalledTimes(2), { + timeout: 1000, + }); + + // Let the abandoned guest load settle inside act, so its trailing state + // updates do not land after the test finishes. + await act(async () => { + releaseGuestLoad(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + }); + + it("reloads for the same user after a sign-out", async () => { + const { fire } = renderProvider(); + await waitFor(() => expect(createSignedUrl).toHaveBeenCalled()); + + await fire("SIGNED_OUT", null); + await fire("SIGNED_IN", makeSession()); + + await waitFor(() => expect(rpc).toHaveBeenCalledTimes(2)); + }); + + it("keeps the initial spinner up when SIGNED_IN wins the race with initializeAuth", async () => { + // initializeAuth yields at `await getSession()`, so SIGNED_IN can land + // first. Marking the identity loaded up front would then clear the spinner + // while pro status was still in flight. + const session = makeSession(); + + let releaseSession = () => {}; + getSession.mockReturnValueOnce( + new Promise<{ data: { session: Session }; error: null }>((resolve) => { + releaseSession = () => resolve({ data: { session }, error: null }); + }), + ); + + let releaseProStatus = () => {}; + rpc.mockImplementationOnce( + () => + new Promise<{ data: boolean; error: null }>((resolve) => { + releaseProStatus = () => resolve({ data: true, error: null }); + }), + ); + + const { getByTestId, fire } = renderProvider(); + + // SIGNED_IN lands first and starts the load; pro status stays unsettled. + await fire("SIGNED_IN", session); + expect(rpc).toHaveBeenCalledTimes(1); + + // initializeAuth must now adopt that in-flight load, not short-circuit. + await act(async () => { + releaseSession(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + expect(getByTestId("loading").textContent).toBe("true"); + // Adopted, not restarted. + expect(rpc).toHaveBeenCalledTimes(1); + + await act(async () => { + releaseProStatus(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + await waitFor(() => + expect(getByTestId("loading").textContent).toBe("false"), + ); + expect(rpc).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/editor/src/saas/auth/UseSession.tsx b/frontend/editor/src/saas/auth/UseSession.tsx index 087fa99b65..101ed58d06 100644 --- a/frontend/editor/src/saas/auth/UseSession.tsx +++ b/frontend/editor/src/saas/auth/UseSession.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, + useRef, useState, ReactNode, useCallback, @@ -247,6 +248,78 @@ export function AuthProvider({ children }: { children: ReactNode }) { await fetchProfilePictureMetadata(); }, [fetchProfilePictureMetadata]); + // Refs, not state: the auth effect below has an empty dep array. + const loadedForRef = useRef(null); + const inFlightRef = useRef<{ key: string; promise: Promise } | null>( + null, + ); + + /** + * Sole owner of the per-user data fetching: mount-time init and every auth + * event route through here. Idempotent per identity, since Supabase re-fires + * SIGNED_IN and TOKEN_REFRESHED on token refresh and tab wakeups; pass + * `force` for a genuine reload. The returned promise excludes the profile + * picture, so awaiting it never blocks on an image download. + */ + const loadUserData = useCallback( + ( + sessionToLoad: Session | null, + opts?: { force?: boolean }, + ): Promise => { + const user = sessionToLoad?.user; + if (!user) { + loadedForRef.current = null; + return Promise.resolve(); + } + + // Not the access token, which changes every refresh. The anonymous flag + // matters: a guest upgrade keeps the same id and must still refetch. + const key = `${user.id}:${Boolean(user.is_anonymous)}`; + if (!opts?.force && loadedForRef.current === key) + return Promise.resolve(); + + // The second of a concurrent init/SIGNED_IN pair adopts this promise so it + // still awaits the load. A forced reload must not: the guest upgrade + // would be silently dropped. + if (!opts?.force && inFlightRef.current?.key === key) + return inFlightRef.current.promise; + + const run = (async () => { + // Off the awaited path: a first login re-uploads the provider avatar. + // The signed-URL read chains behind it because reading first 404s and + // silently falls back to the provider photo. + const avatarSync = syncOAuthAvatar(user).catch((err) => { + console.debug("[Auth Debug] Failed to sync OAuth avatar:", err); + return false; + }); + void avatarSync + .then(() => fetchProfilePicture(sessionToLoad)) + .catch((err) => { + console.debug("[Auth Debug] Failed to fetch profile picture:", err); + }); + + await Promise.all([ + fetchProStatus(sessionToLoad), + fetchProfilePictureMetadata(sessionToLoad), + ]); + // Only on success: set up front, a concurrent caller short-circuits on + // it and returns to a still-empty state. Also lets a failure retry. + loadedForRef.current = key; + })() + .catch((err) => { + console.debug("[Auth Debug] Failed to load user data:", err); + }) + .finally(() => { + // Only clear our own entry; a newer load may have superseded us. + if (inFlightRef.current?.promise === run) inFlightRef.current = null; + }); + + inFlightRef.current = { key, promise: run }; + return run; + }, + [fetchProStatus, fetchProfilePictureMetadata, fetchProfilePicture], + ); + const refreshSession = async () => { try { setLoading(true); @@ -312,23 +385,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { }); setSession(data.session); - // Fetch pro status, profile picture metadata, and profile picture using the session from the response - if (data.session?.user) { - // Sync OAuth avatar in background; fetch the picture once the - // sync settles instead of guessing with a fixed delay. - syncOAuthAvatar(data.session.user) - .catch((err) => { - console.debug( - "[Auth Debug] Failed to sync OAuth avatar on init:", - err, - ); - return false; - }) - .then(() => fetchProfilePicture(data.session)); - - await fetchProStatus(data.session); - await fetchProfilePictureMetadata(data.session); - } + // Awaited so the spinner does not clear before pro status is known. + await loadUserData(data.session); } } catch (err) { console.error( @@ -374,58 +432,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsPro(null); setProfilePictureUrl(null); setProfilePictureMetadata(null); - } else if (event === "SIGNED_IN") { - console.debug("[Auth Debug] User signed in successfully"); - if (newSession?.user) { - // Note: we deliberately do NOT toggle `loading` here. Supabase - // also fires SIGNED_IN on tab visibility / token-refresh wakeups - // (per its docs: "SIGNED_IN is fired when a user signs in OR - // when the access token is refreshed"), and gating the UI on - // `loading` would unmount Landing -> HomePage every time the - // user switches tabs back. Initial-mount loading is handled by - // `initializeAuth` above; downstream fetches expose their own - // null/loading states. - - // Sync OAuth avatar in background (don't block other fetches) - const avatarSync = syncOAuthAvatar(newSession.user).catch( - (err) => { - console.debug( - "[Auth Debug] Failed to sync OAuth avatar:", - err, - ); - return false; - }, - ); - - // Fetch user data in parallel - Promise.all([ - fetchProStatus(newSession), - fetchProfilePictureMetadata(newSession), - ]).then(() => { - // Fetch the picture once the avatar sync settles. - avatarSync.then(() => { - fetchProfilePicture(newSession).finally(() => { - console.debug( - "[Auth Debug] User data fully loaded after sign in", - ); - }); - }); - }); - } - } else if (event === "TOKEN_REFRESHED") { - console.debug("[Auth Debug] Token refreshed"); - // Optionally refresh pro status, profile picture metadata, and profile picture on token refresh - if (newSession?.user) { - Promise.all([ - fetchProStatus(newSession), - fetchProfilePictureMetadata(newSession), - fetchProfilePicture(newSession), - ]).then(() => { - console.debug( - "[Auth Debug] User data refreshed after token refresh", - ); - }); - } + loadedForRef.current = null; + } else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") { + console.debug("[Auth Debug] Signed in or token refreshed"); + // Deliberately does not touch `loading`: Supabase also fires + // SIGNED_IN on tab wakeups, and gating the UI on it would unmount + // Landing -> HomePage on every tab switch. Pinned by a test. + void loadUserData(newSession); } else if (event === "USER_UPDATED") { console.debug("[Auth Debug] User updated"); @@ -454,14 +467,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { "[Auth Debug] User upgrade synchronized successfully", ); - // Refresh pro status, profile picture metadata, and profile picture after upgrade - if (newSession?.user) { - return Promise.all([ - fetchProStatus(newSession), - fetchProfilePictureMetadata(newSession), - fetchProfilePicture(newSession), - ]); - } + // Forced: same user id, so the guest's data must be replaced. + return loadUserData(newSession, { force: true }); }) .then(() => { console.debug( @@ -484,6 +491,10 @@ export function AuthProvider({ children }: { children: ReactNode }) { mounted = false; subscription.unsubscribe(); }; + // Empty and load-bearing: must subscribe once. The closures are recreated + // when `session` changes, so listing deps would re-subscribe on every auth + // event; every call above passes its session explicitly instead. No lint + // rule enforces this, so do not "fix" these deps. }, []); const { t } = useTranslation(); From 86d4a344767b183a395dc12495eda85fb10958c6 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 6 Aug 2026 15:54:44 +0200 Subject: [PATCH 032/154] deps: Upgrade posthog-js to 1.405.2 (#7115) # Description of Changes - Updated the frontend `posthog-js` dependency from `^1.268.0` to `^1.405.2`. - Refreshed `frontend/package-lock.json` and updated related PostHog transitive dependencies. - Removed obsolete OpenTelemetry and protobuf-related transitive packages no longer required by the newer PostHog version. - The existing PostHog APIs used by Stirling-PDF remain compatible. --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- frontend/package-lock.json | 46 ++++++++++++++++++++++---------------- frontend/package.json | 2 +- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 300a409229..546e2ef705 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -70,7 +70,7 @@ "pdfjs-dist": "^5.4.149", "peerjs": "^1.5.5", "pixelmatch": "^7.1.0", - "posthog-js": "^1.268.0", + "posthog-js": "^1.405.2", "qrcode.react": "^4.2.0", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -3154,12 +3154,12 @@ } }, "node_modules/@posthog/core": { - "version": "1.39.3", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.39.3.tgz", - "integrity": "sha512-oR+B8Q5O61N+W2+HVOBG9dbxAT/+OVxX+XvpNj6KYgptN2EB14JiKQ9Rm7DNpErNTLV7WApZEK/URkZgldOxfg==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.44.0.tgz", + "integrity": "sha512-uE+mdKvetxNQC6gWQf4MHIH8bGt+JN6z7ho0A4G3t9G1VGQTx9wHYHNwkKf3gzi+1oAXS9ej2VVY4pjWUU2PWg==", "license": "MIT", "dependencies": { - "@posthog/types": "^1.392.0" + "@posthog/types": "^1.397.0" } }, "node_modules/@posthog/react": { @@ -3179,9 +3179,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.392.0", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.392.0.tgz", - "integrity": "sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==", + "version": "1.397.1", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.397.1.tgz", + "integrity": "sha512-W/LpWbKVaaUnfZKuFuHa+Dg03D+fC87cM+PQbG+59JcSPW8F0JcBtSoXmpfrqbpuxUToMo+gktutrUkAb/KQBw==", "license": "MIT" }, "node_modules/@puppeteer/browsers": { @@ -14053,17 +14053,17 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.396.4", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.396.4.tgz", - "integrity": "sha512-PycBmwKQD1T7YFYrGRb8rjQET/UVnexgUy8gVe6UBEhwHXEIhZF4na5VakJbn4zu1wg4tzjt8r7PA4VLu6bDjg==", - "license": "SEE LICENSE IN LICENSE", + "version": "1.405.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.405.2.tgz", + "integrity": "sha512-KPbbAX4EKM8UTU13gW/fZHyonfVee+GIj9QyiXtI2N4ooku69eZzfgk3CxQOUcnuvCz6xaQaUlvRmIsROxTCvw==", + "license": "(Apache-2.0 AND MIT)", "dependencies": { - "@posthog/core": "^1.39.3", - "@posthog/types": "^1.392.0", - "core-js": "^3.38.1", + "@posthog/core": "^1.44.0", + "@posthog/types": "^1.397.1", + "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", - "preact": "^10.29.2", + "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } @@ -14082,13 +14082,21 @@ } }, "node_modules/preact": { - "version": "10.29.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", - "integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==", + "version": "10.29.7", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz", + "integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } } }, "node_modules/prelude-ls": { diff --git a/frontend/package.json b/frontend/package.json index 1798abc6b2..f48fad8571 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -67,7 +67,7 @@ "pdfjs-dist": "^5.4.149", "peerjs": "^1.5.5", "pixelmatch": "^7.1.0", - "posthog-js": "^1.268.0", + "posthog-js": "^1.405.2", "qrcode.react": "^4.2.0", "react": "^19.2.8", "react-dom": "^19.2.8", From fa2eb6712445c741ff7b5167420ed5b28505bd96 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 6 Aug 2026 16:56:12 +0200 Subject: [PATCH 033/154] deps(frontend): align dependency scopes and remove redundant ESLint packages (#6991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes This change reorganizes frontend dependencies by moving development-only packages into `devDependencies`, removing obsolete packages, and updating several development tooling dependencies to newer versions. ### What was changed - Moved runtime-independent packages to `devDependencies`: - `@iconify/react` - `globals` - Removed unused TypeScript ESLint packages: - `@typescript-eslint/eslint-plugin` - `@typescript-eslint/parser` - Updated development dependencies: - `@iconify-json/material-symbols` → `1.2.83` - `@iconify/utils` → `3.1.4` - `globals` → `17.7.0` These changes reduce redundant dependency declarations and ensure packages are classified according to their actual usage. The main challenge was distinguishing direct dependencies from packages already provided transitively by frontend tooling. --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- frontend/package-lock.json | 716 ++++++++++++++++++++++++++++++------- frontend/package.json | 14 +- 2 files changed, 594 insertions(+), 136 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 546e2ef705..6f19db35b7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,6 @@ "@embedpdf/plugin-zoom": "^2.14.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@iconify/react": "^6.0.2", "@mantine/core": "^8.3.1", "@mantine/dates": "^8.3.1", "@mantine/dropzone": "^8.3.1", @@ -62,7 +61,6 @@ "autoprefixer": "^10.4.21", "axios": "^1.15.0", "d3": "^7.9.0", - "globals": "^17.5.0", "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.0", "jszip": "^3.10.1", @@ -89,8 +87,9 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@iconify-json/material-symbols": "^1.2.53", - "@iconify/utils": "^3.1.0", + "@iconify-json/material-symbols": "^1.2.83", + "@iconify/react": "^6.0.2", + "@iconify/utils": "^3.1.4", "@playwright/test": "^1.55.0", "@storybook/addon-a11y": "^9.1.20", "@storybook/addon-docs": "^9.1.20", @@ -115,12 +114,13 @@ "@typescript-eslint/parser": "^8.65.0", "@typescript/native": "npm:typescript@^7.0.2", "@vitejs/plugin-react-swc": "^4.1.0", - "@vitest/browser": "^3.2.6", - "@vitest/coverage-v8": "^3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/coverage-v8": "3.2.7", "dotenv": "^16.4.7", "dpdm": "^3.14.0", "eslint": "^10.8.0", "fake-indexeddb": "^6.2.5", + "globals": "^17.7.0", "jsdom": "^27.0.0", "json-schema-to-typescript": "^15.0.4", "license-checker": "^25.0.1", @@ -142,7 +142,7 @@ "vite-plugin-compression2": "^2.5.3", "vite-plugin-static-copy": "^3.1.4", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.2.4" + "vitest": "3.2.7" } }, "node_modules/@acemir/cssom": { @@ -2170,9 +2170,9 @@ } }, "node_modules/@iconify-json/material-symbols": { - "version": "1.2.63", - "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.63.tgz", - "integrity": "sha512-R4PS/l8K6j+dk2P2MoYFLJgfbZ4YDo6XjCOpX6b1tvX+BhiSpSOjc1b6cnb/mvWe+JWBKlt4pcvPNiAijFLPnA==", + "version": "1.2.83", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.83.tgz", + "integrity": "sha512-4I2rfNlaoyn4zIcdJDxUMuPV1pVp8Tgwy+eJvyAZuOpmPtOsniQ8Dug6wzRD5s9KLyQA3smFvuBphVdYG7NWQA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2183,6 +2183,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@iconify/react/-/react-6.0.2.tgz", "integrity": "sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg==", + "dev": true, "license": "MIT", "dependencies": { "@iconify/types": "^2.0.0" @@ -2198,18 +2199,19 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", - "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", "dev": true, "license": "MIT", "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", - "mlly": "^1.8.0" + "import-meta-resolve": "^4.2.0" } }, "node_modules/@inquirer/ansi": { @@ -6433,16 +6435,16 @@ } }, "node_modules/@vitest/browser": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.6.tgz", - "integrity": "sha512-CNjSynGBtAVOMTfQITv6Bc8da4/XTU1izorocbDStjUsynXcgx2FHVssh+10a8bKd/BxoqDdQtuSbYHfk302Wg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.7.tgz", + "integrity": "sha512-gIzazUkbQfv6T1rJHOLhMMKQnplKAvvQ7QNGaFwI6oCsp4z2aSDZCojGpX3QX3+MYsvJdyy/8BRIYVEbAkMkEA==", "dev": true, "license": "MIT", "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", - "@vitest/mocker": "3.2.6", - "@vitest/utils": "3.2.6", + "@vitest/mocker": "3.2.7", + "@vitest/utils": "3.2.7", "magic-string": "^0.30.17", "sirv": "^3.0.1", "tinyrainbow": "^2.0.0", @@ -6453,7 +6455,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "3.2.6", + "vitest": "3.2.7", "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" }, "peerDependenciesMeta": { @@ -6469,13 +6471,13 @@ } }, "node_modules/@vitest/browser/node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6496,9 +6498,9 @@ } }, "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -6509,9 +6511,9 @@ } }, "node_modules/@vitest/browser/node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6522,13 +6524,13 @@ } }, "node_modules/@vitest/browser/node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -6537,9 +6539,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", - "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", "dev": true, "license": "MIT", "dependencies": { @@ -6561,8 +6563,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.6", - "vitest": "3.2.6" + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6628,13 +6630,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", - "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -6643,9 +6645,9 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -6656,13 +6658,13 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -6671,13 +6673,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", - "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6686,9 +6688,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -8078,13 +8080,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -10193,9 +10188,10 @@ } }, "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12962,17 +12958,14 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" + "engines": { + "node": ">=10" } }, "node_modules/mrmime": { @@ -13716,18 +13709,6 @@ "pixelmatch": "bin/pixelmatch" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/playwright": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", @@ -17409,9 +17390,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17427,6 +17408,490 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/tsx/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -17512,13 +17977,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -18097,20 +18555,20 @@ } }, "node_modules/vitest": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", - "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.6", - "@vitest/mocker": "3.2.6", - "@vitest/pretty-format": "^3.2.6", - "@vitest/runner": "3.2.6", - "@vitest/snapshot": "3.2.6", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -18140,8 +18598,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.6", - "@vitest/ui": "3.2.6", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, @@ -18170,15 +18628,15 @@ } }, "node_modules/vitest/node_modules/@vitest/expect": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", - "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -18187,13 +18645,13 @@ } }, "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -18214,9 +18672,9 @@ } }, "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -18227,9 +18685,9 @@ } }, "node_modules/vitest/node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18240,13 +18698,13 @@ } }, "node_modules/vitest/node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, diff --git a/frontend/package.json b/frontend/package.json index f48fad8571..bc01367cef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,6 @@ "@embedpdf/plugin-zoom": "^2.14.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@iconify/react": "^6.0.2", "@mantine/core": "^8.3.1", "@mantine/dates": "^8.3.1", "@mantine/dropzone": "^8.3.1", @@ -59,7 +58,6 @@ "autoprefixer": "^10.4.21", "axios": "^1.15.0", "d3": "^7.9.0", - "globals": "^17.5.0", "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.0", "jszip": "^3.10.1", @@ -111,8 +109,9 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@iconify-json/material-symbols": "^1.2.53", - "@iconify/utils": "^3.1.0", + "@iconify-json/material-symbols": "^1.2.83", + "@iconify/react": "^6.0.2", + "@iconify/utils": "^3.1.4", "@playwright/test": "^1.55.0", "@storybook/addon-a11y": "^9.1.20", "@storybook/addon-docs": "^9.1.20", @@ -137,12 +136,13 @@ "@typescript-eslint/parser": "^8.65.0", "@typescript/native": "npm:typescript@^7.0.2", "@vitejs/plugin-react-swc": "^4.1.0", - "@vitest/browser": "^3.2.6", - "@vitest/coverage-v8": "^3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/coverage-v8": "3.2.7", "dotenv": "^16.4.7", "dpdm": "^3.14.0", "eslint": "^10.8.0", "fake-indexeddb": "^6.2.5", + "globals": "^17.7.0", "jsdom": "^27.0.0", "json-schema-to-typescript": "^15.0.4", "license-checker": "^25.0.1", @@ -164,7 +164,7 @@ "vite-plugin-compression2": "^2.5.3", "vite-plugin-static-copy": "^3.1.4", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.2.4" + "vitest": "3.2.7" }, "depcheck": { "ignoreMatches": [ From 2cf6db99ceb0b8321f29f4a4cde18dbb4baec1e0 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:47:09 +0100 Subject: [PATCH 034/154] Fix timing-fragile Valkey rate-limit boundary test (#7302) # Description of Changes Fix timing-fragile Valkey rate-limit boundary test --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../valkey/LiveValkeyIntegrationTest.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java index c26528a715..0e6cb2f4de 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java @@ -298,24 +298,44 @@ class LiveValkeyIntegrationTest { ValkeyRateLimitStore store = newRateLimitStore(factoryA); String key = "boundary-" + java.util.UUID.randomUUID(); long capacity = 5; - Duration window = Duration.ofMillis(500); + // refillGreedy tops the bucket up continuously, one token every window/capacity. A 500ms + // window left the drain loop only 100ms before a 6th token appeared, so a slow Valkey + // round-trip broke the count; 4s spaces refills 800ms apart, clear of any burst. + Duration window = Duration.ofSeconds(4); + long refillIntervalMs = window.toMillis() / capacity; + long drainStart = System.nanoTime(); int firstAllowed = 0; for (int i = 0; i < 10; i++) { if (store.tryConsume(key, capacity, window).allowed()) firstAllowed++; } - assertEquals(capacity, firstAllowed, "must allow exactly capacity tokens initially"); + long drainMs = (System.nanoTime() - drainStart) / 1_000_000; + // Refill never pauses, so a slow drain earns extra tokens honestly - allow exactly the + // number the elapsed time can have produced and no more. + long earned = drainMs / refillIntervalMs; + assertTrue( + firstAllowed >= capacity && firstAllowed <= capacity + earned, + "initial burst must be capacity (" + + capacity + + ") plus at most the " + + earned + + " token(s) refilled during a " + + drainMs + + "ms drain, got " + + firstAllowed); - Thread.sleep(window.toMillis() + 50); + // A fixed-window limiter would hand back a whole fresh capacity at the boundary; a token + // bucket hands back one token per refill interval. + Thread.sleep(refillIntervalMs + 200); int secondAllowed = 0; - long start = System.nanoTime(); - for (int i = 0; i < 20 && (System.nanoTime() - start) < 20_000_000L; i++) { + for (int i = 0; i < 10; i++) { if (store.tryConsume(key, capacity, window).allowed()) secondAllowed++; } assertTrue( - secondAllowed <= capacity, - "token-bucket must not let a fresh full capacity be consumed instantly across" - + " the boundary; got " + secondAllowed >= 1 && secondAllowed < capacity, + "one refill interval must yield about one token, not a fresh full window of " + + capacity + + "; got " + secondAllowed); } From fd1c955648c8c2ba28d382d1ba0db01b260e8c93 Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:52:13 +0200 Subject: [PATCH 035/154] refactor(ui): redesign VersionTimeline UI (#7162) # Description of Changes I felt the old VersionTimeline was a bit too crowded/not very "good" looking so i had a crack at redesigning it. Mainly aimed for: - less info - less crowding - more spacing ### New image ### Old image --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [X] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../public/locales/en-US/translation.toml | 3 +- .../core/components/filesPage/FilesPage.css | 174 ++++++----- .../filesPage/VersionHistoryModal.tsx | 16 +- .../components/filesPage/VersionTimeline.tsx | 275 +++++++++--------- 4 files changed, 256 insertions(+), 212 deletions(-) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index feb42149f7..ea4f0399b9 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -3977,6 +3977,7 @@ refresh = "Refresh from server" remove = "Delete" removeVersion = "Remove this version" rename = "Rename" +renamed = "Renamed" renameFolder = "Rename folder" resizeFolderTree = "Resize folder tree (arrow keys, Shift for bigger steps; double-click to auto-fit)" save = "Save" @@ -4079,9 +4080,7 @@ count = "Files" folder = "Folder" labels = "Labels" modified = "Modified" -name = "Name" size = "Size" -toolHistoryAtVersion = "Cumulative tool chain" totalSize = "Total size" type = "Type" versionHistory = "Version journey" diff --git a/frontend/editor/src/core/components/filesPage/FilesPage.css b/frontend/editor/src/core/components/filesPage/FilesPage.css index 9de4a276aa..4815a1b31e 100644 --- a/frontend/editor/src/core/components/filesPage/FilesPage.css +++ b/frontend/editor/src/core/components/filesPage/FilesPage.css @@ -956,178 +956,204 @@ like a commit graph; clicking a row's summary toggles its expanded detail (cumulative tool chain, full meta). Long chains (> 6) collapse the middle behind a "Show N earlier versions" button. */ +/* Version Timeline styling */ .files-page-details-version-timeline { display: flex; flex-direction: column; - gap: 0.4rem; - padding: 0.55rem 0.7rem 0.7rem; - background: var(--c-surface); - border: 1px solid var(--c-border-subtle); - border-radius: 0.5rem; + gap: 0.6rem; + padding: 0.5rem 0; + background: transparent; } + .files-page-details-version-timeline-label { display: flex; align-items: center; - gap: 0.35rem; - font-size: 0.72rem; + gap: 0.4rem; + font-size: 0.75rem; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--c-text-subtle); + margin-bottom: 0.85rem; } + .files-page-details-version-timeline-count { margin-left: auto; font-weight: 600; - color: var(--c-text-muted, var(--c-text-subtle)); + color: var(--c-primary); text-transform: none; letter-spacing: 0; } + .files-page-details-version-timeline-list { - list-style: none; - margin: 0; - padding: 0; + list-style: none !important; + margin: 0 !important; + padding: 1.1rem 0 0 0 !important; display: flex; flex-direction: column; + gap: 1.25rem; } + .files-page-details-version-timeline-row, .files-page-details-version-timeline-ellipsis { display: flex; - gap: 0.6rem; - padding: 0.15rem 0; + gap: 0.85rem; + padding: 0; position: relative; + list-style: none !important; } + +.files-page-details-version-timeline-row::before, +.files-page-details-version-timeline-ellipsis::before { + content: none !important; +} + .files-page-details-version-timeline-rail { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; - width: 0.8rem; - padding-top: 0.45rem; + width: 1rem; + padding-top: 0.85rem; } + .files-page-details-version-timeline-rail-dot { - width: 0.55rem; - height: 0.55rem; + width: 0.65rem; + height: 0.65rem; border-radius: 50%; background: var(--c-bg-raised); border: 2px solid var(--c-border-strong, var(--c-border-subtle)); - z-index: 1; + z-index: 2; flex-shrink: 0; + transition: all 0.2s ease; } + .files-page-details-version-timeline-rail-dot.is-active { background: var(--c-primary); border-color: var(--c-primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-primary) 25%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-primary) 30%, transparent); } + .files-page-details-version-timeline-rail-dot.is-ellipsis { - width: 0.35rem; - height: 0.35rem; + width: 0.4rem; + height: 0.4rem; background: var(--c-text-subtle); border-color: transparent; } + .files-page-details-version-timeline-rail-line { width: 2px; flex: 1; background: var(--c-border-subtle); - min-height: 0.6rem; - margin-top: 2px; + min-height: 1.25rem; + margin-top: 6px; } + .files-page-details-version-timeline-body { flex: 1; min-width: 0; display: flex; flex-direction: column; - gap: 0.2rem; - padding: 0.25rem 0.3rem 0.4rem; - border-radius: 0.35rem; - transition: background-color 0.12s ease; + gap: 0.5rem; + padding: 1rem 1.25rem; + border-radius: 0.65rem; + background: var(--c-surface); + border: 1px solid var(--c-border-subtle); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + transition: + border-color 0.15s ease, + background-color 0.15s ease, + box-shadow 0.15s ease; } + +.files-page-details-version-timeline-body:hover { + border-color: var(--c-border-strong, var(--c-border-subtle)); +} + .files-page-details-version-timeline-row.is-active .files-page-details-version-timeline-body { - background: color-mix(in srgb, var(--c-primary) 10%, transparent); + border-color: color-mix(in srgb, var(--c-primary) 40%, transparent); + background: color-mix(in srgb, var(--c-primary) 5%, var(--c-surface)); + box-shadow: 0 2px 6px color-mix(in srgb, var(--c-primary) 12%, transparent); } -.files-page-details-version-timeline-summary { + +.files-page-details-version-timeline-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.files-page-details-version-timeline-tool-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--c-text); +} + +.files-page-details-version-timeline-card-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding-top: 0.1rem; +} + +.files-page-details-version-timeline-expand-btn { appearance: none; background: none; border: 0; padding: 0; - margin: 0; + cursor: pointer; display: flex; align-items: center; - gap: 0.4rem; - cursor: pointer; - text-align: left; - color: inherit; - font: inherit; } -.files-page-details-version-timeline-summary:hover - .files-page-details-version-timeline-chevron { + +.files-page-details-version-timeline-expand-btn:hover span { color: var(--c-text); } -.files-page-details-version-timeline-delta { - font-size: 0.82rem; - color: var(--c-text); - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-flex; - align-items: baseline; - gap: 0.25rem; -} -.files-page-details-version-timeline-delta.is-origin { - font-weight: 400; - color: var(--c-text-subtle); - font-style: italic; -} -.files-page-details-version-timeline-delta-plus { - color: var(--c-primary); - font-weight: 700; -} -.files-page-details-version-timeline-spacer { - flex: 1; -} + .files-page-details-version-timeline-chevron { color: var(--c-text-subtle); transition: transform 0.15s ease; } + .files-page-details-version-timeline-chevron.is-expanded { transform: rotate(180deg); - color: var(--c-text); -} -.files-page-details-version-timeline-meta-line { - display: flex; - align-items: center; - gap: 0.35rem; - font-size: 0.7rem; - color: var(--c-text-subtle); + color: var(--c-primary); } + .files-page-details-version-timeline-expanded { display: flex; flex-direction: column; gap: 0.4rem; - margin-top: 0.45rem; + margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px dashed var(--c-border-subtle); } + .files-page-details-version-timeline-toolchain { display: flex; flex-direction: column; gap: 0.2rem; } + .files-page-details-version-timeline-toolchain-label { font-size: 0.65rem; color: var(--c-text-subtle); text-transform: uppercase; letter-spacing: 0.05em; } + .files-page-details-version-timeline-ellipsis-btn, .files-page-details-version-timeline-collapse-btn { appearance: none; background: none; border: 1px dashed var(--c-border-subtle); - border-radius: 0.3rem; - padding: 0.25rem 0.5rem; + border-radius: 0.4rem; + padding: 0.35rem 0.7rem; margin: 0.1rem 0; - font-size: 0.72rem; + font-size: 0.75rem; color: var(--c-text-subtle); cursor: pointer; text-align: left; @@ -1135,11 +1161,13 @@ border-color 0.12s ease, color 0.12s ease; } + .files-page-details-version-timeline-ellipsis-btn:hover, .files-page-details-version-timeline-collapse-btn:hover { color: var(--c-text); - border-color: var(--c-border-strong, var(--c-text-subtle)); + border-color: var(--c-primary); } + .files-page-details-version-timeline-collapse-btn { align-self: flex-start; margin-top: 0.2rem; diff --git a/frontend/editor/src/core/components/filesPage/VersionHistoryModal.tsx b/frontend/editor/src/core/components/filesPage/VersionHistoryModal.tsx index 077924155d..2bef30c763 100644 --- a/frontend/editor/src/core/components/filesPage/VersionHistoryModal.tsx +++ b/frontend/editor/src/core/components/filesPage/VersionHistoryModal.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Center, Loader, Modal, Text } from "@mantine/core"; +import HistoryIcon from "@mui/icons-material/History"; +import { Center, Group, Loader, Modal, Text } from "@mantine/core"; import type { FileId } from "@app/types/file"; import type { StirlingFileStub } from "@app/types/fileContext"; @@ -99,7 +100,17 @@ export function VersionHistoryModal({ onClose={onClose} centered size="md" - title={t("filesPage.field.versionHistory", "Version journey")} + title={ + + + + {t("filesPage.field.versionHistory", "Version journey")} + + + } > {loading ? (
    @@ -111,6 +122,7 @@ export function VersionHistoryModal({ currentId={file.id} onAddToWorkspace={handleAddToWorkspace} onRemove={handleRemove} + hideHeader /> ) : ( diff --git a/frontend/editor/src/core/components/filesPage/VersionTimeline.tsx b/frontend/editor/src/core/components/filesPage/VersionTimeline.tsx index 47495fe61b..049fc2365a 100644 --- a/frontend/editor/src/core/components/filesPage/VersionTimeline.tsx +++ b/frontend/editor/src/core/components/filesPage/VersionTimeline.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Badge, Menu } from "@mantine/core"; +import { Badge, Group, Menu, Text } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { ActionIcon } from "@app/ui/ActionIcon"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; @@ -8,16 +8,14 @@ import DeleteIcon from "@mui/icons-material/Delete"; import DownloadIcon from "@mui/icons-material/Download"; import HistoryIcon from "@mui/icons-material/History"; import MoreVertIcon from "@mui/icons-material/MoreVert"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { FileId, ToolOperation } from "@app/types/file"; import { ToolId } from "@app/types/toolId"; import { StirlingFileStub } from "@app/types/fileContext"; import { formatFileSize, getFileDate } from "@app/utils/fileUtils"; import { downloadFileFromStorage } from "@app/utils/downloadUtils"; -import ToolChain from "@app/components/shared/ToolChain"; -/** Small label/value row; shared with FileDetailsPanel. */ +/** Small label/value row with crisp flex alignment and colon separation. */ export function DetailField({ label, value, @@ -26,9 +24,31 @@ export function DetailField({ value: string; }) { return ( -
    - {label} - {value} +
    + + {label}: + + + {value} +
    ); } @@ -60,7 +80,7 @@ export interface VersionTimelineProps { hideHeader?: boolean; } -/** Version timeline with per-row tool deltas and collapse-when-long. */ +/** Clean, spacious version timeline with minimal clutter. */ export function VersionTimeline({ chain, currentId, @@ -69,7 +89,6 @@ export function VersionTimeline({ hideHeader = false, }: VersionTimelineProps) { const { t } = useTranslation(); - const [expandedIds, setExpandedIds] = useState>(new Set()); const [showAllCollapsed, setShowAllCollapsed] = useState(false); // Newest-first ordering. @@ -113,15 +132,6 @@ export function VersionTimeline({ return [...head, { kind: "ellipsis", hidden }, ...tail]; }, [collapsible, showAllCollapsed, ordered]); - const toggleExpand = (id: FileId) => { - setExpandedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; - return (
    {!hideHeader && ( @@ -135,7 +145,10 @@ export function VersionTimeline({
    )} -
      +
        {rows.map((row, idx) => { const isLast = idx === rows.length - 1; if (row.kind === "ellipsis") { @@ -143,6 +156,7 @@ export function VersionTimeline({
      • @@ -152,7 +166,7 @@ export function VersionTimeline({
        -
        - {formatFileSize(v.size)} - {v.lastModified ? ( - <> - · - - {getFileDate({ lastModified: v.lastModified })} - - - ) : null} - {/* Kebab on every row - the original/active version also - needs download + open-in-workspace. */} - - - - e.stopPropagation()} - > - - - - - } - onClick={() => onAddToWorkspace([v.id])} - > - {t( - "filesPage.openVersionInWorkspace", - "Open in workspace", - )} - - } - onClick={() => { - void downloadFileFromStorage(v); - }} - > - {t( - "filesPage.downloadVersion", - "Download this version", - )} - - - } - onClick={() => onRemove([v.id])} - > - {t("filesPage.removeVersion", "Remove this version")} - - - -
        - {isExpanded && ( - // Filename + full cumulative tool chain. -
        - - {v.toolHistory && v.toolHistory.length > 0 && ( -
        - - {t( - "filesPage.field.toolHistoryAtVersion", - "Cumulative tool chain", + + {delta ? ( + + ) : ( + t("filesPage.versionOrigin", "Original upload") + )} + + + + {!isActive && ( + + + - -
        - )} -
        + onClick={(e) => e.stopPropagation()} + > + + + + + } + onClick={() => onAddToWorkspace([v.id])} + > + {t( + "filesPage.openVersionInWorkspace", + "Open in workspace", + )} + + } + onClick={() => { + void downloadFileFromStorage(v); + }} + > + {t( + "filesPage.downloadVersion", + "Download this version", + )} + + + } + onClick={() => onRemove([v.id])} + > + {t("filesPage.removeVersion", "Remove this version")} + + + + )} +
    + + {/* Quiet Meta Line: File Size · Date */} + + {formatFileSize(v.size)} + {v.lastModified && ( + <> · {getFileDate({ lastModified: v.lastModified })} + )} + + + {/* Show filename ONLY if original upload or if name changed */} + {(isOriginal || nameChanged) && ( + + {nameChanged + ? `${t("filesPage.renamed", "Renamed")}: ` + : `${t("filesPage.file", "File")}: `} + + {v.name} + + )} ); })} - + {collapsible && showAllCollapsed && ( + + {/* Active Scale Display */} +
    + + {t("scaleSettings.activeScale", "Active Scale")}:{" "} + {currentScale && currentScale.ratio + ? generateScaleLabel(currentScale.ratio, currentScale.unit) + : currentScale && !currentScale.ratio + ? `${currentScale.unit} (custom)` + : t("scaleSettings.noneSet", "No custom scale set")} + +
    + + {/* Calibration Mode */} + + + {/* Reset Button */} + {currentScale && ( + + )} + + ); +} diff --git a/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx b/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx index 14a308e0f3..d5d39c4c0c 100644 --- a/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx +++ b/frontend/editor/src/core/components/viewer/useViewerWorkbenchBarButtons.tsx @@ -26,11 +26,19 @@ import StraightenIcon from "@mui/icons-material/Straighten"; import LayersIcon from "@mui/icons-material/Layers"; import VolumeUpIcon from "@mui/icons-material/VolumeUp"; import StopIcon from "@mui/icons-material/Stop"; +import SettingsIcon from "@mui/icons-material/Settings"; import { useViewerReadAloud } from "@app/components/viewer/useViewerReadAloud"; +import { RulerScaleSettingsButton } from "@app/components/viewer/RulerScaleSettingsButton"; +import type { MeasureScale } from "@app/utils/measurementTypes"; export function useViewerWorkbenchBarButtons( isRulerActive?: boolean, setIsRulerActive?: (v: boolean) => void, + customScale?: MeasureScale | null, + setCustomScale?: (scale: MeasureScale | null) => void, + isScaleCalibrationActive?: boolean, + startScaleCalibration?: () => void, + cancelScaleCalibration?: () => void, ) { const { t, i18n } = useTranslation(); const viewer = useViewer(); @@ -118,11 +126,36 @@ export function useViewerWorkbenchBarButtons( const annotationsLabel = t("workbenchBar.annotations", "Annotations"); const formFillLabel = t("workbenchBar.formFill", "Fill Form"); const rulerLabel = t("workbenchBar.ruler", "Ruler / Measure"); + const rulerSettingsLabel = t("workbenchBar.rulerSettings", "Scale Settings"); const readAloudLabel = t("workbenchBar.readAloud", "Read Aloud"); const readAloudSpeedLabel = t("workbenchBar.readAloudSpeed", "Speed"); const isFormFillActive = (selectedTool as string) === "formFill"; + const handleStartScaleCalibration = useCallback(() => { + startScaleCalibration?.(); + setIsRulerActive?.(true); + if (isPanning) { + viewer.panActions.disablePan(); + setIsPanning(false); + } + }, [isPanning, setIsRulerActive, startScaleCalibration, viewer.panActions]); + + const handleCancelScaleCalibration = useCallback(() => { + cancelScaleCalibration?.(); + }, [cancelScaleCalibration]); + + const handleApplyRulerScale = useCallback( + (scale: MeasureScale) => { + setCustomScale?.(scale); + }, + [setCustomScale], + ); + + const handleResetRulerScale = useCallback(() => { + setCustomScale?.(null); + }, [setCustomScale]); + // Filter languages based on available voices const filteredLanguages = useMemo( () => @@ -234,6 +267,32 @@ export function useViewerWorkbenchBarButtons( } }, }, + // Ruler scale settings button - only visible when ruler is active + ...(isRulerActive + ? [ + { + id: "viewer-ruler-settings", + icon: , + tooltip: rulerSettingsLabel, + ariaLabel: rulerSettingsLabel, + section: "top" as const, + order: 25.5, + render: ({ disabled }: { disabled?: boolean }) => ( + + ), + }, + ] + : []), { id: "viewer-rotate-left", icon: , @@ -553,8 +612,15 @@ export function useViewerWorkbenchBarButtons( formFillLabel, isFormFillActive, rulerLabel, + rulerSettingsLabel, isRulerActive, setIsRulerActive, + handleStartScaleCalibration, + handleCancelScaleCalibration, + handleApplyRulerScale, + handleResetRulerScale, + customScale, + isScaleCalibrationActive, readAloudLabel, readAloudSpeedLabel, isReadingAloud, diff --git a/frontend/editor/src/core/contexts/ViewerContext.tsx b/frontend/editor/src/core/contexts/ViewerContext.tsx index 5e5d08dbb4..78535b41ac 100644 --- a/frontend/editor/src/core/contexts/ViewerContext.tsx +++ b/frontend/editor/src/core/contexts/ViewerContext.tsx @@ -176,6 +176,9 @@ export interface ViewerContextType { registerImmediatePanUpdate: ( callback: (isPanning: boolean) => void, ) => () => void; + registerImmediateRotationUpdate: ( + callback: (rotation: number) => void, + ) => () => void; // Internal - for bridges to trigger immediate updates triggerImmediateScrollUpdate: ( @@ -188,6 +191,7 @@ export interface ViewerContextType { isDualPage?: boolean, ) => void; triggerImmediatePanUpdate: (isPanning: boolean) => void; + triggerImmediateRotationUpdate: (rotation: number) => void; // Action handlers - call EmbedPDF APIs directly scrollActions: ScrollActions; @@ -310,6 +314,10 @@ export const ViewerProvider: React.FC = ({ children }) => { register: registerImmediatePanUpdate, trigger: triggerImmediatePanInternal, } = useImmediateNotifier<[boolean]>(); + const { + register: registerImmediateRotationUpdate, + trigger: triggerImmediateRotationInternal, + } = useImmediateNotifier<[number]>(); const triggerImmediateZoomUpdate = useCallback( (percent: number) => { @@ -339,6 +347,13 @@ export const ViewerProvider: React.FC = ({ children }) => { [triggerImmediatePanInternal], ); + const triggerImmediateRotationUpdate = useCallback( + (rotation: number) => { + triggerImmediateRotationInternal(rotation); + }, + [triggerImmediateRotationInternal], + ); + const registerBridge = useCallback( ( type: K, @@ -638,10 +653,12 @@ export const ViewerProvider: React.FC = ({ children }) => { registerImmediateScrollUpdate, registerImmediateSpreadUpdate, registerImmediatePanUpdate, + registerImmediateRotationUpdate, triggerImmediateScrollUpdate, triggerImmediateZoomUpdate, triggerImmediateSpreadUpdate, triggerImmediatePanUpdate, + triggerImmediateRotationUpdate, // Actions scrollActions, diff --git a/frontend/editor/src/core/hooks/useMeasurementManager.ts b/frontend/editor/src/core/hooks/useMeasurementManager.ts new file mode 100644 index 0000000000..f6f6b9d6b1 --- /dev/null +++ b/frontend/editor/src/core/hooks/useMeasurementManager.ts @@ -0,0 +1,293 @@ +import { + useCallback, + useEffect, + useRef, + useState, + type RefObject, +} from "react"; +import type { + Measurement, + MeasureScale, + PageMeasureScales, +} from "@app/utils/measurementTypes"; +import type { RulerOverlayHandle } from "@app/components/viewer/RulerOverlay"; +import { + loadSessionMap, + saveSessionMap, + validateMeasureScale, + validateMeasurement, +} from "@app/utils/measurementUtils"; +import type { StirlingFile } from "@app/types/fileContext"; +import { isStirlingFile, getFormFillFileId } from "@app/types/fileContext"; +import { extractPageMeasureScales } from "@app/utils/pdfMeasurementExtraction"; +import type { ScaleCalibrationMeasurement } from "@app/components/viewer/ScaleCalibrationDialog"; + +// ─── Hook: useMeasurementManager ────────────────────────────────────────────── + +interface EffectiveFileLike { + file: Blob | File; + url: string | null; +} + +type ViewerFile = StirlingFile | File | null | undefined; + +interface UseMeasurementManagerProps { + currentFile: ViewerFile; + effectiveFile: EffectiveFileLike | null | undefined; + rulerOverlayRef: RefObject; +} + +interface UseMeasurementManagerReturn { + isRulerActive: boolean; + setIsRulerActive: (v: boolean) => void; + pageMeasureScales: PageMeasureScales | null; + customScale: MeasureScale | null; + handleSetCustomScale: (scale: MeasureScale | null) => void; + isScaleCalibrationActive: boolean; + scaleCalibrationMeasurement: ScaleCalibrationMeasurement | null; + startScaleCalibration: () => void; + cancelScaleCalibration: () => void; + handleScaleCalibrationMeasurement: ( + measurement: ScaleCalibrationMeasurement, + ) => void; + applyScaleCalibration: (scale: MeasureScale) => void; +} + +export function useMeasurementManager({ + currentFile, + effectiveFile, + rulerOverlayRef, +}: UseMeasurementManagerProps): UseMeasurementManagerReturn { + const [isRulerActive, setIsRulerActive] = useState(false); + const [pageMeasureScales, setPageMeasureScales] = + useState(null); + const [customScale, setCustomScale] = useState(null); + const [isScaleCalibrationActive, setIsScaleCalibrationActive] = + useState(false); + const [scaleCalibrationMeasurement, setScaleCalibrationMeasurement] = + useState(null); + const [scalesByFileId, setScalesByFileId] = useState< + Map + >(new Map()); + const [measurementsByFileId, setMeasurementsByFileId] = useState< + Map + >(new Map()); + + const restoredFileKeyRef = useRef(null); + + const getStableFileKey = useCallback((file: ViewerFile): string | null => { + if (!file) return null; + if (isStirlingFile(file)) { + return file.fileId; + } + return getFormFillFileId(file); + }, []); + + const currentFileKey = getStableFileKey(currentFile); + + function persistSessionValue( + storageKey: string, + fileKey: string, + value: MeasureScale | Measurement[] | null, + label: string, + ) { + try { + saveSessionMap(storageKey, fileKey, value); + } catch (error) { + console.error(`[Measurement] Failed to persist ${label}:`, error); + } + } + + function readStoredScale(fileKey: string): MeasureScale | null | undefined { + const storedMap = loadSessionMap("stirling_scales"); + if (!(fileKey in storedMap)) { + return undefined; + } + + const storedValue = storedMap[fileKey]; + return validateMeasureScale(storedValue) ? storedValue : null; + } + + function readStoredMeasurements(fileKey: string): Measurement[] | undefined { + const storedMap = loadSessionMap("stirling_measurements"); + if (!(fileKey in storedMap)) { + return undefined; + } + + const storedValue = storedMap[fileKey]; + if (!Array.isArray(storedValue)) { + return []; + } + + return storedValue.filter((measurement) => + validateMeasurement(measurement), + ); + } + + function persistScale(fileKey: string, scale: MeasureScale | null) { + persistSessionValue("stirling_scales", fileKey, scale, "scale"); + } + + function persistMeasurements(fileKey: string, value: Measurement[]) { + persistSessionValue( + "stirling_measurements", + fileKey, + value, + "measurements", + ); + } + + const handleSetCustomScale = useCallback( + (scale: MeasureScale | null) => { + const fileKey = currentFileKey; + + if (fileKey) { + setScalesByFileId((prev) => new Map(prev).set(fileKey, scale)); + persistScale(fileKey, scale); + } + + setCustomScale(scale); + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(false); + }, + [currentFileKey], + ); + + const handleSetRulerActive = useCallback((active: boolean) => { + setIsRulerActive(active); + if (!active) { + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(false); + } + }, []); + + const startScaleCalibration = useCallback(() => { + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(true); + setIsRulerActive(true); + }, []); + + const cancelScaleCalibration = useCallback(() => { + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(false); + }, []); + + const handleScaleCalibrationMeasurement = useCallback( + (measurement: ScaleCalibrationMeasurement) => { + setScaleCalibrationMeasurement(measurement); + setIsScaleCalibrationActive(false); + }, + [], + ); + + const applyScaleCalibration = useCallback( + (scale: MeasureScale) => { + handleSetCustomScale(scale); + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(false); + }, + [handleSetCustomScale], + ); + + useEffect(() => { + if (!currentFileKey) { + setPageMeasureScales(null); + setCustomScale(null); + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(false); + setIsRulerActive(false); + rulerOverlayRef.current?.clearAll(true); + restoredFileKeyRef.current = null; + return; + } + + if (restoredFileKeyRef.current === currentFileKey) { + return; + } + restoredFileKeyRef.current = currentFileKey; + setScaleCalibrationMeasurement(null); + setIsScaleCalibrationActive(false); + + const storedScale = readStoredScale(currentFileKey); + const savedScale = + storedScale === undefined + ? (scalesByFileId.get(currentFileKey) ?? null) + : storedScale; + + setCustomScale(savedScale); + + const storedMeasurements = readStoredMeasurements(currentFileKey); + const savedMeasurements = + storedMeasurements === undefined + ? (measurementsByFileId.get(currentFileKey) ?? []) + : storedMeasurements; + + rulerOverlayRef.current?.clearAll(true); + rulerOverlayRef.current?.restoreMeasurements(savedMeasurements); + }, [currentFileKey, measurementsByFileId, rulerOverlayRef, scalesByFileId]); + + useEffect(() => { + const fileBlob = effectiveFile?.file; + if (!fileBlob || !currentFileKey) { + setPageMeasureScales(null); + return; + } + + setPageMeasureScales(null); + + let cancelled = false; + extractPageMeasureScales(fileBlob) + .then((scales) => { + if (!cancelled) { + setPageMeasureScales(scales); + } + }) + .catch((error) => { + if (!cancelled) { + console.warn("[Measurement] Failed to load PDF scales", error); + setPageMeasureScales(null); + } + }); + + return () => { + cancelled = true; + }; + }, [currentFileKey, effectiveFile?.file]); + + useEffect(() => { + if (!rulerOverlayRef.current || !currentFileKey) return; + + const unsubscribe = rulerOverlayRef.current.onMeasurementsChange( + (newMeasurements: Measurement[]) => { + const validMeasurements = newMeasurements.filter((measurement) => + validateMeasurement(measurement), + ); + + setMeasurementsByFileId((prev) => + new Map(prev).set(currentFileKey, validMeasurements), + ); + persistMeasurements(currentFileKey, validMeasurements); + }, + ); + + return () => { + if (typeof unsubscribe === "function") { + unsubscribe(); + } + }; + }, [currentFileKey, rulerOverlayRef]); + + return { + isRulerActive, + setIsRulerActive: handleSetRulerActive, + pageMeasureScales, + customScale, + handleSetCustomScale, + isScaleCalibrationActive, + scaleCalibrationMeasurement, + startScaleCalibration, + cancelScaleCalibration, + handleScaleCalibrationMeasurement, + applyScaleCalibration, + }; +} diff --git a/frontend/editor/src/core/utils/measurementPreferences.ts b/frontend/editor/src/core/utils/measurementPreferences.ts new file mode 100644 index 0000000000..c339ed64f8 --- /dev/null +++ b/frontend/editor/src/core/utils/measurementPreferences.ts @@ -0,0 +1,24 @@ +// Persist calibration unit preference across sessions +const STORAGE_KEY_LAST_CALIBRATION_UNIT = "stirling_calibration_last_unit"; + +export function getLastCalibrationUnit(defaultUnit: string): string { + try { + const stored = localStorage.getItem(STORAGE_KEY_LAST_CALIBRATION_UNIT); + return stored && stored.trim() ? stored : defaultUnit; + } catch { + // Storage unavailable - private browsing or quota exceeded + return defaultUnit; + } +} + +export function setLastCalibrationUnit(unit: string): void { + try { + localStorage.setItem(STORAGE_KEY_LAST_CALIBRATION_UNIT, unit); + } catch (error) { + // Storage unavailable - preference won't be retained + console.debug( + "[MeasurementPreferences] Unable to persist unit preference:", + error, + ); + } +} diff --git a/frontend/editor/src/core/utils/measurementTypes.ts b/frontend/editor/src/core/utils/measurementTypes.ts new file mode 100644 index 0000000000..caa47ff876 --- /dev/null +++ b/frontend/editor/src/core/utils/measurementTypes.ts @@ -0,0 +1,45 @@ +// Page coordinates with absolute page index +export interface PagePoint { + pageIndex: number; + x: number; + y: number; +} + +// Real-world units per PDF point (factor) vs. architectural ratio for display +export interface MeasureScale { + factor: number; // Real-world units per PDF point + ratio: number | null; // Architectural ratio (e.g., 100 for "1:100") - display only + unit: string; // m, cm, mm, km, ft, in, yd, mi +} + +export type MeasureScaleLike = MeasureScale; + +// Calibration result with full context for audit trail +export interface CalibrationMetadata { + pdfDistancePts: number; // PDF space distance in points + realDistance: number; // User-specified real-world distance + scale: MeasureScale; // Resulting calculated scale + timestamp: string; // ISO 8601 format + unitUsed: string; // Unit active during calibration +} + +// Single measurement between two page points on same page +export interface Measurement { + id: string; + start: PagePoint; + end: PagePoint; +} + +// Viewport area with its own scale (for multi-region PDFs) +export interface ViewportScale { + bbox: [number, number, number, number] | null; // PDF user space or null for entire page + scale: MeasureScale; +} + +// Scale information for a single page with all viewports +export interface PageScaleInfo { + viewports: ViewportScale[]; + pageHeight: number; // PDF points - used to flip screen-y to PDF-y +} + +export type PageMeasureScales = Map; diff --git a/frontend/editor/src/core/utils/measurementUtils.test.ts b/frontend/editor/src/core/utils/measurementUtils.test.ts new file mode 100644 index 0000000000..33cf3f5c2d --- /dev/null +++ b/frontend/editor/src/core/utils/measurementUtils.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, test } from "vitest"; +import { + POINT_TO_UNIT, + calculateCalibratedScale, + calculateScaleFactor, + convertUnit, + deriveRatioFromFactor, + parsePresetRatio, +} from "@app/utils/measurementUtils"; + +describe("measurementUtils", () => { + describe("calculateScaleFactor", () => { + test("calculates real-world units per PDF point from a scale ratio", () => { + expect(calculateScaleFactor(100, "m")).toBeCloseTo(POINT_TO_UNIT.m * 100); + expect(calculateScaleFactor(50, " cm ")).toBeCloseTo( + POINT_TO_UNIT.cm * 50, + ); + expect(calculateScaleFactor(12, "FT")).toBeCloseTo(POINT_TO_UNIT.ft * 12); + }); + + test("rejects invalid scale ratios", () => { + expect(() => calculateScaleFactor(0, "m")).toThrow("Invalid scale ratio"); + expect(() => calculateScaleFactor(-1, "m")).toThrow( + "Invalid scale ratio", + ); + expect(() => calculateScaleFactor(Number.NaN, "m")).toThrow( + "Invalid scale ratio", + ); + expect(() => calculateScaleFactor(Number.POSITIVE_INFINITY, "m")).toThrow( + "Invalid scale ratio", + ); + }); + + test("rejects unsupported units", () => { + expect(() => calculateScaleFactor(100, "px")).toThrow("Unsupported unit"); + }); + }); + + describe("convertUnit", () => { + test("converts representative metric and imperial values", () => { + expect(convertUnit(1, "m", "cm")).toBeCloseTo(100); + expect(convertUnit(12, "in", "ft")).toBeCloseTo(1); + expect(convertUnit(3, "ft", "yd")).toBeCloseTo(1); + expect(convertUnit(1, "ft", "m")).toBeCloseTo(0.3048); + }); + + test("returns null for invalid values or unsupported units", () => { + expect(convertUnit(Number.NaN, "m", "cm")).toBeNull(); + expect(convertUnit(Number.POSITIVE_INFINITY, "m", "cm")).toBeNull(); + expect(convertUnit(1, "px", "cm")).toBeNull(); + expect(convertUnit(1, "m", "px")).toBeNull(); + }); + }); + + describe("parsePresetRatio", () => { + test("parses supported preset ratios", () => { + expect(parsePresetRatio("1:5")).toBe(5); + expect(parsePresetRatio("1:100")).toBe(100); + expect(parsePresetRatio(" 1 : 150 ")).toBe(150); + }); + + test("returns null for malformed or non-positive presets", () => { + expect(parsePresetRatio("2:100")).toBeNull(); + expect(parsePresetRatio("1:0")).toBeNull(); + expect(parsePresetRatio("1:-10")).toBeNull(); + expect(parsePresetRatio("1:not-a-number")).toBeNull(); + expect(parsePresetRatio("bad")).toBeNull(); + expect(parsePresetRatio("1:10:20")).toBeNull(); + }); + }); + + describe("deriveRatioFromFactor", () => { + test("recovers the scale ratio from a factor and unit", () => { + const factor = calculateScaleFactor(100, "m"); + + expect(deriveRatioFromFactor(factor, "m")).toBeCloseTo(100); + }); + + test("returns null for invalid factors or unsupported units", () => { + expect(deriveRatioFromFactor(0, "m")).toBeNull(); + expect(deriveRatioFromFactor(-1, "m")).toBeNull(); + expect(deriveRatioFromFactor(Number.NaN, "m")).toBeNull(); + expect(deriveRatioFromFactor(1, "px")).toBeNull(); + }); + }); + + describe("calculateCalibratedScale", () => { + test("calculates a calibrated scale from a known physical distance", () => { + const scale = calculateCalibratedScale(72, 1, "in"); + + expect(scale.factor).toBeCloseTo(POINT_TO_UNIT.in); + expect(scale.ratio).toBeCloseTo(1); + expect(scale.unit).toBe("in"); + }); + + test("calculates architectural ratios for metric calibration", () => { + const scale = calculateCalibratedScale(72, 0.0254, "m"); + + expect(scale.factor).toBeCloseTo(POINT_TO_UNIT.m); + expect(scale.ratio).toBeCloseTo(1); + expect(scale.unit).toBe("m"); + }); + + test("rejects invalid calibration inputs", () => { + expect(() => calculateCalibratedScale(0, 1, "m")).toThrow( + "Invalid PDF distance", + ); + expect(() => calculateCalibratedScale(72, 0, "m")).toThrow( + "Invalid real-world distance", + ); + expect(() => calculateCalibratedScale(72, 1, "px")).toThrow( + "Unsupported unit", + ); + }); + }); +}); diff --git a/frontend/editor/src/core/utils/measurementUtils.ts b/frontend/editor/src/core/utils/measurementUtils.ts new file mode 100644 index 0000000000..e35645fb2b --- /dev/null +++ b/frontend/editor/src/core/utils/measurementUtils.ts @@ -0,0 +1,398 @@ +// PDF point to real-world unit conversions + +import type { + Measurement, + MeasureScale, + PagePoint, + CalibrationMetadata, +} from "@app/utils/measurementTypes"; + +// 1 PDF point in meters (1/72 inch) +const POINT_TO_METERS = 0.0254 / 72; + +// Conversion factors: units per PDF point +export const POINT_TO_UNIT = { + m: POINT_TO_METERS, + cm: POINT_TO_METERS * 100, + mm: POINT_TO_METERS * 1000, + km: POINT_TO_METERS / 1000, + ft: POINT_TO_METERS / 0.3048, + in: POINT_TO_METERS / 0.0254, + yd: POINT_TO_METERS / 0.9144, + mi: POINT_TO_METERS / 1609.344, +} as const; + +// Valid measurement units from POINT_TO_UNIT +export type MeasurementUnit = keyof typeof POINT_TO_UNIT; + +function normalizeUnit(unit: string): string { + return unit.toLowerCase().trim(); +} + +function isMeasurementUnit(unit: string): unit is MeasurementUnit { + return Object.hasOwn(POINT_TO_UNIT, unit); +} + +export function getUnitFactor(unit: string): number | undefined { + const normalized = normalizeUnit(unit); + if (!isMeasurementUnit(normalized)) { + return undefined; + } + return POINT_TO_UNIT[normalized]; +} + +export function calculateScaleFactor(ratio: number, unit: string): number { + if (!Number.isFinite(ratio) || ratio <= 0) { + throw new Error(`Invalid scale ratio: ${ratio}`); + } + + const normalized = normalizeUnit(unit); + if (!isMeasurementUnit(normalized)) { + throw new Error(`Unsupported unit: ${unit}`); + } + + return POINT_TO_UNIT[normalized] * ratio; +} + +export function generateScaleLabel(ratio: number | null, unit: string): string { + if (ratio === null || ratio === undefined) { + return unit; + } + const display = Number.isInteger(ratio) + ? ratio.toString() + : ratio.toFixed(2).replace(/\.?0+$/, ""); + return `1:${display} (${unit})`; +} + +// Imperial units +const IMPERIAL_UNITS = ["ft", "in", "yd", "mi"] as const; +export function isImperialUnit(unit: string): boolean { + const normalized = normalizeUnit(unit); + return isMeasurementUnit(normalized) + ? (IMPERIAL_UNITS as readonly MeasurementUnit[]).includes(normalized) + : false; +} + +export function convertUnit( + value: number, + sourceUnit: string, + targetUnit: string, +): number | null { + if (!Number.isFinite(value)) { + return null; + } + + const src = normalizeUnit(sourceUnit); + const tgt = normalizeUnit(targetUnit); + + if (!isMeasurementUnit(src) || !isMeasurementUnit(tgt)) { + return null; + } + + const sourceFactor = POINT_TO_UNIT[src]; + const targetFactor = POINT_TO_UNIT[tgt]; + + return value * (targetFactor / sourceFactor); +} + +export function parsePresetRatio(preset: string): number | null { + const parts = preset.split(":"); + + // Must have exactly 2 parts and first part must be "1" + if (parts.length !== 2 || parts[0].trim() !== "1") { + return null; + } + + const value = Number(parts[1]); + return Number.isFinite(value) && value > 0 ? value : null; +} + +// UI dropdown options - shared across components +export const UNIT_OPTIONS = [ + { value: "m", label: "Meters (m)" }, + { value: "cm", label: "Centimeters (cm)" }, + { value: "mm", label: "Millimeters (mm)" }, + { value: "km", label: "Kilometers (km)" }, + { value: "ft", label: "Feet (ft)" }, + { value: "in", label: "Inches (in)" }, + { value: "yd", label: "Yards (yd)" }, + { value: "mi", label: "Miles (mi)" }, +] as const; + +const MAX_SESSION_ENTRIES = 50; +const TRIMMED_SESSION_ENTRIES = 40; + +/** + * Detect quota exceeded errors across browser implementations. + * Handles: name "QuotaExceededError", code 22 (legacy), "NS_ERROR_DOM_QUOTA_REACHED" + * + * Note: DOMException may not be instanceof Error in all browsers, + * so we check by shape and properties rather than type. + * Note: DOMException.code is deprecated but kept for legacy browser support. + */ +function isQuotaExceededError(error: unknown): boolean { + if (error === null || error === undefined) return false; + + // Check if it's a DOMException when available (standard) + if (typeof DOMException !== "undefined" && error instanceof DOMException) { + if (error.name === "QuotaExceededError") return true; + } + + // Fallback: check by shape for any object with name/code properties + if (typeof error === "object") { + const err = error as Record; + + // Modern standard: check name property (works in all modern browsers) + if (err.name === "QuotaExceededError") return true; + if (err.name === "NS_ERROR_DOM_QUOTA_REACHED") return true; + + // Legacy support: check deprecated code property for very old browsers + // Use Object.hasOwn for safe own-property check + if (Object.hasOwn(err, "code") && err.code === 22) return true; + } + + return false; +} + +// Load entries from sessionStorage +export function loadSessionMap(key: string): Record { + try { + const raw = sessionStorage.getItem(key); + if (!raw) return {}; + + const data = JSON.parse(raw); + if (typeof data !== "object" || data === null || Array.isArray(data)) { + return {}; + } + + return data as Record; + } catch { + // Silently return empty object on parse error + try { + sessionStorage.removeItem(key); + } catch { + // Ignore cleanup errors + } + return {}; + } +} + +// Save entry to sessionStorage with quota management. +export function saveSessionMap( + key: string, + fileKey: string, + value: MeasureScale | Measurement[] | null, +): void { + if (!fileKey) return; + + try { + const existing: Record = { + ...loadSessionMap(key), + }; + + // Delete first to move fileKey to end (maintains insertion order recency) + delete existing[fileKey]; + existing[fileKey] = value; + + // Trim back below the max to avoid pruning again on every subsequent save. + const keys = Object.keys(existing); + if (keys.length > MAX_SESSION_ENTRIES) { + const entriesToDelete = keys.slice( + 0, + keys.length - TRIMMED_SESSION_ENTRIES, + ); + entriesToDelete.forEach((k) => delete existing[k]); + } + + sessionStorage.setItem(key, JSON.stringify(existing)); + } catch (e) { + // Quota exceeded - try clearing and retrying (handles cross-browser error variants) + if (isQuotaExceededError(e)) { + try { + sessionStorage.removeItem(key); + // Retry with fresh storage + const fresh: Record = { [fileKey]: value }; + sessionStorage.setItem(key, JSON.stringify(fresh)); + } catch { + // Silently ignore if retry fails - data loss is acceptable + } + } + // Silently ignore other storage errors + } +} + +// Validation helpers + +export function validatePagePoint(obj: unknown): obj is PagePoint { + if (typeof obj !== "object" || obj === null) return false; + + const pt = obj as Record; + return ( + typeof pt.pageIndex === "number" && + Number.isFinite(pt.pageIndex) && + pt.pageIndex >= 0 && + typeof pt.x === "number" && + Number.isFinite(pt.x) && + typeof pt.y === "number" && + Number.isFinite(pt.y) + ); +} + +// MeasureScale can be null (reset) or valid object +export function validateMeasureScale(obj: unknown): obj is MeasureScale | null { + // null is allowed (reset to default) + if (obj === null) return true; + + if (typeof obj !== "object") return false; + + const s = obj as Record; + + // Validate factor: must be positive finite number + if ( + typeof s.factor !== "number" || + !Number.isFinite(s.factor) || + s.factor <= 0 + ) { + return false; + } + + // Validate ratio: optional, but if present must be positive finite number + if ( + s.ratio !== null && + (typeof s.ratio !== "number" || !Number.isFinite(s.ratio) || s.ratio <= 0) + ) { + return false; + } + + // Validate unit: must be non-empty string and exist in POINT_TO_UNIT + if (typeof s.unit !== "string" || s.unit.trim().length === 0) { + return false; + } + + const normalized = normalizeUnit(s.unit); + if (!isMeasurementUnit(normalized)) { + return false; + } + + return true; +} + +// Reject cross-page measurements +export function validateMeasurement(obj: unknown): obj is Measurement { + if (typeof obj !== "object" || obj === null) return false; + + const m = obj as Record; + + // Validate structure + if ( + !( + typeof m.id === "string" && + m.id.trim().length > 0 && + validatePagePoint(m.start) && + validatePagePoint(m.end) + ) + ) { + return false; + } + + // Reject cross-page measurements + const start = m.start as PagePoint; + const end = m.end as PagePoint; + if (start.pageIndex !== end.pageIndex) { + return false; + } + + return true; +} + +export function formatPaperDistance(distancePts: number): string { + if (!Number.isFinite(distancePts) || distancePts < 0) { + return "0 mm"; + } + + const inches = distancePts / 72; + const mm = inches * 25.4; + + if (mm < 100) { + return `${mm.toFixed(1)} mm`; + } + if (mm < 1000) { + return `${(mm / 10).toFixed(1)} cm`; + } + return `${(mm / 1000).toFixed(2)} m`; +} + +export function validateRealDistance(value: unknown): number | null { + if (value === null || value === undefined || value === "") { + return null; + } + + const num = typeof value === "number" ? value : Number(value); + + if (!Number.isFinite(num) || num <= 0) { + return null; + } + + return num; +} + +export function deriveRatioFromFactor( + factor: number, + unit: string, +): number | null { + if (!Number.isFinite(factor) || factor <= 0) { + return null; + } + + const baseFactor = getUnitFactor(unit); + if (!baseFactor) { + return null; + } + + // ratio = factor / baseFactor + const ratio = factor / baseFactor; + return Number.isFinite(ratio) && ratio > 0 ? ratio : null; +} + +export function calculateCalibratedScale( + pdfDistancePts: number, + realDistance: number, + unit: string, +): MeasureScale { + if (!Number.isFinite(pdfDistancePts) || pdfDistancePts <= 0) { + throw new Error("Invalid PDF distance (must be positive)"); + } + + if (!Number.isFinite(realDistance) || realDistance <= 0) { + throw new Error("Invalid real-world distance (must be positive)"); + } + + const baseFactor = getUnitFactor(unit); + if (!baseFactor) { + throw new Error(`Unsupported unit: ${unit}`); + } + + const factor = realDistance / pdfDistancePts; + const ratio = deriveRatioFromFactor(factor, unit); + + return { + factor, + ratio, + unit, + }; +} + +export function createCalibrationMetadata( + pdfDistancePts: number, + realDistance: number, + scale: MeasureScale, + unitUsed: string, +): CalibrationMetadata { + return { + pdfDistancePts, + realDistance, + scale, + timestamp: new Date().toISOString(), + unitUsed, + }; +} diff --git a/frontend/editor/src/core/utils/pdfMeasurementExtraction.ts b/frontend/editor/src/core/utils/pdfMeasurementExtraction.ts new file mode 100644 index 0000000000..3c40e3209f --- /dev/null +++ b/frontend/editor/src/core/utils/pdfMeasurementExtraction.ts @@ -0,0 +1,215 @@ +import type { + PDFArray, + PDFDict, + PDFHexString, + PDFName, + PDFNumber, + PDFString, +} from "@cantoo/pdf-lib"; +import type { + MeasureScale, + PageMeasureScales, + PageScaleInfo, + ViewportScale, +} from "@app/utils/measurementTypes"; +import { getUnitFactor } from "@app/utils/measurementUtils"; + +type PdfMeasurementObjects = Pick< + typeof import("@cantoo/pdf-lib"), + | "PDFArray" + | "PDFDict" + | "PDFHexString" + | "PDFName" + | "PDFNumber" + | "PDFString" +>; + +function asPdfArray( + value: unknown, + { PDFArray }: PdfMeasurementObjects, +): PDFArray | null { + return value instanceof PDFArray ? value : null; +} + +function asPdfDict( + value: unknown, + { PDFDict }: PdfMeasurementObjects, +): PDFDict | null { + return value instanceof PDFDict ? value : null; +} + +function asPdfNumber( + value: unknown, + { PDFNumber }: PdfMeasurementObjects, +): PDFNumber | null { + return value instanceof PDFNumber ? value : null; +} + +function asPdfText( + value: unknown, + { PDFHexString, PDFName, PDFString }: PdfMeasurementObjects, +): PDFHexString | PDFName | PDFString | null { + if ( + value instanceof PDFString || + value instanceof PDFHexString || + value instanceof PDFName + ) { + return value; + } + return null; +} + +function lookupArray( + dict: PDFDict, + key: string, + pdfObjects: PdfMeasurementObjects, +): PDFArray | null { + return asPdfArray(dict.lookup(pdfObjects.PDFName.of(key)), pdfObjects); +} + +function lookupDict( + dict: PDFDict, + key: string, + pdfObjects: PdfMeasurementObjects, +): PDFDict | null { + return asPdfDict(dict.lookup(pdfObjects.PDFName.of(key)), pdfObjects); +} + +function lookupNumber( + dict: PDFDict, + key: string, + pdfObjects: PdfMeasurementObjects, +): number | null { + return ( + asPdfNumber( + dict.lookup(pdfObjects.PDFName.of(key)), + pdfObjects, + )?.asNumber() ?? null + ); +} + +function lookupText( + dict: PDFDict, + key: string, + pdfObjects: PdfMeasurementObjects, +): string | null { + return ( + asPdfText( + dict.lookup(pdfObjects.PDFName.of(key)), + pdfObjects, + )?.decodeText() ?? null + ); +} + +function readArrayNumber( + array: PDFArray, + index: number, + pdfObjects: PdfMeasurementObjects, +): number | null { + return asPdfNumber(array.lookup(index), pdfObjects)?.asNumber() ?? null; +} + +function readBBox( + bboxArray: PDFArray | null, + pdfObjects: PdfMeasurementObjects, +): ViewportScale["bbox"] { + if (!bboxArray || bboxArray.size() < 4) { + return null; + } + + const x0 = readArrayNumber(bboxArray, 0, pdfObjects); + const y0 = readArrayNumber(bboxArray, 1, pdfObjects); + const x1 = readArrayNumber(bboxArray, 2, pdfObjects); + const y1 = readArrayNumber(bboxArray, 3, pdfObjects); + + if (x0 === null || y0 === null || x1 === null || y1 === null) { + return null; + } + + return [x0, y0, x1, y1]; +} + +function parseScale( + measureDict: PDFDict | null, + pdfObjects: PdfMeasurementObjects, +): MeasureScale | null { + if (!measureDict) return null; + + const fmtArray = + lookupArray(measureDict, "D", pdfObjects) ?? + lookupArray(measureDict, "X", pdfObjects); + if (!fmtArray || fmtArray.size() === 0) return null; + + const firstFmt = asPdfDict(fmtArray.lookup(0), pdfObjects); + if (!firstFmt) return null; + + const factor = lookupNumber(firstFmt, "C", pdfObjects); + if (factor === null || factor <= 0) return null; + + const unit = lookupText(firstFmt, "U", pdfObjects)?.trim().toLowerCase(); + if (!unit) return null; + + const baseFactor = getUnitFactor(unit); + if (!baseFactor) return null; + + const ratio = factor / baseFactor; + return { factor, ratio, unit }; +} + +export async function extractPageMeasureScales( + file: Blob, +): Promise { + try { + const pdfLib = await import("@cantoo/pdf-lib"); + const { PDFDocument, PDFArray, PDFDict, PDFName } = pdfLib; + const pdfDoc = await PDFDocument.load(await file.arrayBuffer(), { + ignoreEncryption: true, + }); + + const result: PageMeasureScales = new Map(); + + for (let i = 0; i < pdfDoc.getPageCount(); i++) { + const page = pdfDoc.getPage(i); + const pageHeight = page.getHeight(); + const viewports: ViewportScale[] = []; + + const vpObj = page.node.lookup(PDFName.of("VP")); + if (vpObj instanceof PDFArray) { + for (let j = 0; j < vpObj.size(); j++) { + const vpEntry = vpObj.lookup(j); + if (!(vpEntry instanceof PDFDict)) continue; + + const scale = parseScale( + lookupDict(vpEntry, "Measure", pdfLib), + pdfLib, + ); + if (!scale) continue; + + viewports.push({ + bbox: readBBox(lookupArray(vpEntry, "BBox", pdfLib), pdfLib), + scale, + }); + } + } + + if (viewports.length === 0) { + const scale = parseScale( + lookupDict(page.node, "Measure", pdfLib), + pdfLib, + ); + if (scale) { + viewports.push({ bbox: null, scale }); + } + } + + if (viewports.length > 0) { + result.set(i, { viewports, pageHeight } satisfies PageScaleInfo); + } + } + + return result.size > 0 ? result : null; + } catch (error) { + console.warn("[Measurement] Failed to extract PDF scales", error); + return null; + } +} From 82e1bd62a2ad166312656b657f35978dbe59970a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:27:23 +0100 Subject: [PATCH 043/154] Move CODEOWNERS to review teams (#7325) # Description of Changes CODEOWNERS now points at review teams (`maintainers`, `backend-reviewers`, `frontend-reviewers`, `devops-reviewers`, `all`) instead of individual usernames, so membership is managed in the org rather than in this file. Ludy87 and balazs-szucs stay listed by hand since outside collaborators cannot be team members. including the deploy and demo-comment allowlists. --- ## Checklist ### General - [x] 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/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) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .github/CODEOWNERS | 36 ++++++++++++------- .github/config/repo_devs.json | 1 - .github/workflows/PR-Auto-Deploy-V2.yml | 2 +- .../workflows/PR-Demo-Comment-with-react.yml | 1 - 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 14f6ca750f..a2e3241c65 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,18 +1,28 @@ -# All PRs must be approved by Frooodle or Ludy87 -* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh +# Review ownership is assigned to teams where possible. +# Teams can only contain org members, so outside collaborators are listed by hand. +# +# @Stirling-Tools/maintainers - Frooodle, jbrunton96, ConnorYoh +# @Stirling-Tools/backend-reviewers - Frooodle, jbrunton96, ConnorYoh +# @Stirling-Tools/frontend-reviewers - Frooodle, jbrunton96, ConnorYoh, reecebrowne, EthanHealy01 +# @Stirling-Tools/devops-reviewers - Frooodle, jbrunton96, ConnorYoh +# @Stirling-Tools/all - all of the above +# +# Outside collaborators (need Write access to count as owners): @Ludy87 @balazs-szucs + +# Default owners for everything +* @Stirling-Tools/maintainers @Ludy87 # Backend -/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh @balazs-szucs +/app/** @Stirling-Tools/backend-reviewers @Ludy87 @balazs-szucs -#V2 frontend -/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @balazs-szucs -/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 @balazs-szucs +# V2 frontend +/frontend/** @Stirling-Tools/frontend-reviewers @balazs-szucs +/app/core/src/main/resources/static/** @Stirling-Tools/frontend-reviewers @Ludy87 @balazs-szucs -#V2 docker -/docker/backend/** @Frooodle @Ludy87 @DarioGii -/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 -/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 +# V2 docker +/docker/backend/** @Stirling-Tools/devops-reviewers @Ludy87 +/docker/frontend/** @Stirling-Tools/frontend-reviewers @Stirling-Tools/devops-reviewers @Ludy87 +/docker/compose/** @Stirling-Tools/frontend-reviewers @Stirling-Tools/devops-reviewers @Ludy87 - -#GHA (All users) -/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 @balazs-szucs +# GHA (all users) +/.github/** @Stirling-Tools/all @Ludy87 @balazs-szucs diff --git a/.github/config/repo_devs.json b/.github/config/repo_devs.json index 8b0bb97a81..597a84dade 100644 --- a/.github/config/repo_devs.json +++ b/.github/config/repo_devs.json @@ -11,7 +11,6 @@ "LaserKaspar", "sbplat", "reecebrowne", - "DarioGii", "ConnorYoh", "EthanHealy01", "jbrunton96", diff --git a/.github/workflows/PR-Auto-Deploy-V2.yml b/.github/workflows/PR-Auto-Deploy-V2.yml index 6b37029f90..cd99f6a4cc 100644 --- a/.github/workflows/PR-Auto-Deploy-V2.yml +++ b/.github/workflows/PR-Auto-Deploy-V2.yml @@ -86,7 +86,7 @@ jobs: fi fi else - auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs") + auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs") is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done if [ "$is_auth" = true ]; then should=true diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 3826897a39..d1e2000b82 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -54,7 +54,6 @@ jobs: github.event.comment.user.login == 'Ludy87' || github.event.comment.user.login == 'balazs-szucs' || github.event.comment.user.login == 'reecebrowne' || - github.event.comment.user.login == 'DarioGii' || github.event.comment.user.login == 'EthanHealy01' || github.event.comment.user.login == 'jbrunton96' || github.event.comment.user.login == 'ConnorYoh' From 2c74b5bf81be0853e0e8503402834e21b7508a42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:01:07 +0100 Subject: [PATCH 044/154] build(deps): bump js-yaml from 4.2.0 to 4.3.1 in /devTools (#7323) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.1.
    Changelog

    Sourced from js-yaml's changelog.

    4.3.1 - 2026-07-31

    Security

    • [backport] Remove quadratic complexity from !!omap duplicate key detection.

    4.3.0 - 2026-06-27

    Added

    • [backport] Added maxTotalMergeKeys (10000) loader option to limit the total number of keys processed by YAML merge (<<) across one load() / loadAll() call.

    Fixed

    • Restore umd builds back to es5.

    Removed

    • [backport] maxMergeSeqLength replaced with maxTotalMergeKeys for limiting YAML merge processing.
    Commits
    • 86e91b8 4.3.1 released
    • c3cc4b0 Backport quadratic complexity fix for !!omap
    • 33d05b5 4.3.0 released
    • 663bfab Drop demo publish, to not override new v5 one.
    • 1cb8c7b Add v4-legacy tag for publish
    • 02f27af Restore umd builds back to es5
    • 8be84ed Fix es5 compatibility
    • 59423c6 Replace maxMergeSeqLength option with maxTotalMergeKeys (more robust). Ba...
    • 6842ef6 doc polish
    • See full diff in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=4.2.0&new-version=4.3.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- devTools/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devTools/package-lock.json b/devTools/package-lock.json index 39db3d2046..394237a684 100644 --- a/devTools/package-lock.json +++ b/devTools/package-lock.json @@ -894,9 +894,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { From 1867c8f285e92adf7fa7dc3f50c41b05cbd572e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:01:11 +0100 Subject: [PATCH 045/154] build(deps): bump js-yaml from 4.2.0 to 4.3.1 in /frontend (#7322) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.1.
    Changelog

    Sourced from js-yaml's changelog.

    4.3.1 - 2026-07-31

    Security

    • [backport] Remove quadratic complexity from !!omap duplicate key detection.

    4.3.0 - 2026-06-27

    Added

    • [backport] Added maxTotalMergeKeys (10000) loader option to limit the total number of keys processed by YAML merge (<<) across one load() / loadAll() call.

    Fixed

    • Restore umd builds back to es5.

    Removed

    • [backport] maxMergeSeqLength replaced with maxTotalMergeKeys for limiting YAML merge processing.
    Commits
    • 86e91b8 4.3.1 released
    • c3cc4b0 Backport quadratic complexity fix for !!omap
    • 33d05b5 4.3.0 released
    • 663bfab Drop demo publish, to not override new v5 one.
    • 1cb8c7b Add v4-legacy tag for publish
    • 02f27af Restore umd builds back to es5
    • 8be84ed Fix es5 compatibility
    • 59423c6 Replace maxMergeSeqLength option with maxTotalMergeKeys (more robust). Ba...
    • 6842ef6 doc polish
    • See full diff in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=4.2.0&new-version=4.3.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 500 +------------------------------------ 1 file changed, 3 insertions(+), 497 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6f19db35b7..762cee13c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11150,9 +11150,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -12968,16 +12968,6 @@ "node": ">=10" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -17408,490 +17398,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, "node_modules/tsx/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", From cff6549a40340b01e8be7211af5be3ab34ee3ba6 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:36:46 +0100 Subject: [PATCH 046/154] fix(frontend): keep file persistence working when IndexedDB refuses blobs (#7314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes Fixes the WebKit nightly failures ([run 31067620195](https://github.com/Stirling-Tools/Stirling-PDF/actions/runs/31067620195/attempts/1)): 8 tests failed on `stubbed-webkit` only, and every one of them logs the same thing in its trace: ``` IndexedDB add error: UnknownError: Error preparing Blob/File data to be stored in object store ``` ## What broke `storeStirlingFile` stores the `File` itself in IndexedDB, so multi-GB uploads are persisted by reference and never materialize in JS memory. That came in with #7175 (`data: stirlingFile` replacing `data: await stirlingFile.arrayBuffer()`), which is a real memory win and worth keeping. WebKit refuses blob values whenever it can't write the blob's backing file, and rejects the request with the error above. The rejection was only `console.error`d, so on WebKit **no upload ever persisted**, and everything that reads the bytes back behaved as if the upload never happened: - `file-state-across-tools` — file gone after navigating; the sidebar shows "No files yet" - `compare` — `FileSelectorPicker: upload failed`, so the slot stays `data-slot-state="empty"` - `classification-grouping` / `classification-heuristic-upload` — the label backfill and thumbnails read from IDB (`not in IndexedDB (likely remote-only stub)`), so files land in "Recent" with no category headers Chromium and Firefox store blobs fine, and PR CI only runs the `stubbed` (chromium) project, so nightly was the only gate that could catch it. ## The fix Try the blob first, keep a fallback: - `storeStirlingFile`'s `add` is extracted into `addFileRecord` so it can run twice - if the value was a Blob and the failure is `UnknownError` / `DataCloneError`, re-add the record with an `ArrayBuffer` copy and set `blobValuesSupported = false`, so later files in that session go straight to the copy path instead of losing the blob attempt every time - deliberately narrow: `QuotaExceededError` and `ConstraintError` still propagate, because a copy would fail the same way and retrying would hide the real cause - dropped two internal `console.error`s: every caller already reports (`addFiles`, `FileSelectorPicker`, `zipFileService` collects into `result.errors`), so they were duplicate noise Every writer goes through `storeStirlingFile` (uploads, the file picker, zip extraction, folder automation, `IndexedDBContext`), so this one seam covers all of them. The read paths already accept either shape (`new Blob([record.data], ...)`). Net effect: Chromium and Firefox keep the no-copy path; engines that refuse blobs degrade to the pre-#7175 behaviour instead of silently losing files. On such an engine a very large file can still exhaust renderer memory — the fallback warns about exactly that. Fixing that properly means chunked storage, which is out of scope here. ## Verification Reproduced and confirmed the cause by A/B on a branch that predates #7175: as-is 8/8 pass on WebKit, and applying only #7175's `data: stirlingFile` line reproduces the exact CI failure set. | Check | Result | |---|---| | `stubbed-webkit`: the 8 nightly failures + `classification-heuristic-upload` | 9 passed | | `stubbed-webkit`: `files-page`, `page-editor-rotation`, `encrypted-pdf-unlock` | 32 passed, 1 skipped | | `stubbed` (chromium): the same specs + `files-page` | 35 passed, 1 skipped | | Frontend unit suite | 210 files, 1797 passed | | `typecheck:core`, `typecheck:proprietary`, eslint, prettier | clean | New unit coverage in `fileStorage.blobFallback.test.ts` pins the contract over `fake-indexeddb` with `add` instrumented to count blob vs copy attempts: blob path when accepted, blob-then-copy when refused (and readable back), one attempt only for later files, and quota not retried. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Testing (if applicable) - [x] Frontend typecheck (core + proprietary), eslint, prettier, the unit suite, and the affected Playwright specs on chromium and webkit all pass Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- .../services/fileStorage.blobFallback.test.ts | 146 ++++++++++++++++++ .../editor/src/core/services/fileStorage.ts | 57 +++++-- 2 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 frontend/editor/src/core/services/fileStorage.blobFallback.test.ts diff --git a/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts b/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts new file mode 100644 index 0000000000..d8530af026 --- /dev/null +++ b/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, test, afterEach, beforeEach, vi } from "vitest"; +import "fake-indexeddb/auto"; +import { expectConsole } from "@app/tests/failOnConsole"; + +/** + * Regression test for the WebKit nightly breakage introduced with the + * large-file OOM fix (#7175): `storeStirlingFile` began putting the `File` + * itself into IndexedDB (persisted by reference, so multi-GB uploads never + * materialize in JS memory). WebKit refuses blob values whenever it can't write + * the blob's backing file and rejects the request with `UnknownError: Error + * preparing Blob/File data to be stored in object store`, so on WebKit every + * upload silently failed to persist: files vanished on navigation, Compare + * slots never filled, and the classification backfill had no bytes to read. + * + * The service now retries such a rejection with an ArrayBuffer copy and stops + * offering blobs for the rest of the session. + */ + +const nativeAdd = IDBObjectStore.prototype.add; + +/** What each `add` attempt carried in `data` — the blob path or the copy path. */ +let attempts: Array<"blob" | "copy"> = []; + +/** An IDBRequest that fails asynchronously, the way WebKit rejects blob puts. */ +class FailingRequest extends EventTarget { + onerror: ((event: Event) => void) | null = null; + onsuccess: ((event: Event) => void) | null = null; + + constructor(readonly error: DOMException) { + super(); + queueMicrotask(() => this.onerror?.(new Event("error"))); + } +} + +/** + * Record every add attempt, optionally failing the blob-valued ones the way an + * engine without blob storage does. + */ +function instrumentAdd(options: { rejectBlobs: boolean }) { + IDBObjectStore.prototype.add = function ( + this: IDBObjectStore, + value: unknown, + key?: IDBValidKey, + ) { + const isBlob = (value as { data?: unknown } | null)?.data instanceof Blob; + attempts.push(isBlob ? "blob" : "copy"); + if (isBlob && options.rejectBlobs) { + return new FailingRequest( + new DOMException( + "Error preparing Blob/File data to be stored in object store", + "UnknownError", + ), + ) as unknown as IDBRequest; + } + return key === undefined + ? nativeAdd.call(this, value) + : nativeAdd.call(this, value, key); + } as typeof IDBObjectStore.prototype.add; +} + +/** + * A fresh service per test: whether the engine accepts blobs is remembered for + * the process lifetime by design, so tests must not inherit that decision from + * each other. + */ +async function freshFileStorage() { + vi.resetModules(); + const [{ fileStorage }, { createStirlingFile, createNewStirlingFileStub }] = + await Promise.all([ + import("@app/services/fileStorage"), + import("@app/types/fileContext"), + ]); + const store = async (name: string) => { + const file = new File(["%PDF-1.7 stirling"], name, { + type: "application/pdf", + }); + const stub = createNewStirlingFileStub(file); + await fileStorage.storeStirlingFile( + createStirlingFile(file, stub.id), + stub, + ); + return stub.id; + }; + return { fileStorage, store }; +} + +beforeEach(() => { + attempts = []; +}); + +afterEach(() => { + IDBObjectStore.prototype.add = nativeAdd; +}); + +describe("storeStirlingFile — blob-value fallback", () => { + test("stores the File by reference when the engine accepts blob values", async () => { + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + + const id = await store("by-reference.pdf"); + + expect(attempts).toEqual(["blob"]); + expect((await fileStorage.getStirlingFile(id))?.name).toBe( + "by-reference.pdf", + ); + }); + + test("falls back to a copy when the engine rejects blob values, and the file stays readable", async () => { + expectConsole.warn(/IndexedDB rejected a Blob value/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: true }); + + const id = await store("webkit.pdf"); + + expect(attempts).toEqual(["blob", "copy"]); + // Readable back is what every downstream consumer depends on: rehydration + // after navigation, thumbnails, the classification backfill. + expect((await fileStorage.getStirlingFile(id))?.name).toBe("webkit.pdf"); + }); + + test("remembers the rejection, so later files skip the doomed blob attempt", async () => { + expectConsole.warn(/IndexedDB rejected a Blob value/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: true }); + + await store("first.pdf"); + attempts = []; + const id = await store("second.pdf"); + + // Straight to the copy path — no repeated blob probe, and only the single + // warning expected above. + expect(attempts).toEqual(["copy"]); + expect((await fileStorage.getStirlingFile(id))?.name).toBe("second.pdf"); + }); + + test("does not retry a failure a copy can't fix (quota)", async () => { + const { store } = await freshFileStorage(); + IDBObjectStore.prototype.add = function (this: IDBObjectStore) { + attempts.push("blob"); + throw new DOMException("no space left", "QuotaExceededError"); + } as typeof IDBObjectStore.prototype.add; + + await expect(store("too-big.pdf")).rejects.toThrow(/no space left/); + expect(attempts).toEqual(["blob"]); + }); +}); diff --git a/frontend/editor/src/core/services/fileStorage.ts b/frontend/editor/src/core/services/fileStorage.ts index 4f82af3a8f..40f62642f0 100644 --- a/frontend/editor/src/core/services/fileStorage.ts +++ b/frontend/editor/src/core/services/fileStorage.ts @@ -63,9 +63,27 @@ export function legacyDerivedFromTool( return undefined; } +/** + * Can't persist a Blob/File value, so a copy would work? WebKit reports + * `UnknownError` ("Error preparing Blob/File data...") when it can't write the + * blob's backing file; a refused structured clone is `DataCloneError`. + * Narrow on purpose: retrying quota or duplicate-key failures would fail again + * and hide the real cause. + */ +function isBlobValueRejection(error: unknown): boolean { + const name = (error as DOMException | null)?.name; + return name === "UnknownError" || name === "DataCloneError"; +} + class FileStorageService { private readonly dbConfig = DATABASE_CONFIGS.FILES; private readonly storeName = "files"; + /** + * Whether this engine accepts Blob/File values in IndexedDB. Optimistic: the + * blob path avoids copying multi-GB files into JS memory, so we try it and + * remember the answer, rather than pre-emptively degrading everywhere. + */ + private blobValuesSupported = true; /** * Get database connection using centralized manager @@ -132,7 +150,10 @@ class FileStorageService { createdAt: stub.createdAt, // Store the File (a Blob) itself: IndexedDB persists it by reference and // streams to disk, so multi-GB files never materialize in JS memory. - data: stirlingFile, + // Engines that reject blob values fall back to a copy — see addFileRecord. + data: this.blobValuesSupported + ? stirlingFile + : await stirlingFile.arrayBuffer(), thumbnail: stub.thumbnailUrl, thumbnailStoredAt: stub.thumbnailUrl ? Date.now() : undefined, isLeaf: stub.isLeaf ?? true, @@ -160,6 +181,30 @@ class FileStorageService { classificationLabels: stub.classificationLabels, }; + try { + await this.addFileRecord(db, record); + } catch (error) { + // Recoverable: re-add as a copy, and stop offering blobs this session. + // Anything else is the caller's to report. + if (!(record.data instanceof Blob) || !isBlobValueRejection(error)) { + throw error; + } + this.blobValuesSupported = false; + console.warn( + "IndexedDB rejected a Blob value; falling back to in-memory copies for this session. " + + "Very large files may now exhaust renderer memory.", + error, + ); + record.data = await record.data.arrayBuffer(); + await this.addFileRecord(db, record); + } + } + + /** Single `add` of a file record. Rejects with the underlying IDB error. */ + private addFileRecord( + db: IDBDatabase, + record: StoredStirlingFileRecord, + ): Promise { return new Promise((resolve, reject) => { try { // Verify store exists before creating transaction @@ -174,15 +219,9 @@ class FileStorageService { const request = store.add(record); - request.onerror = () => { - console.error("IndexedDB add error:", request.error); - reject(request.error); - }; - request.onsuccess = () => { - resolve(); - }; + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(); } catch (error) { - console.error("Transaction error:", error); reject(error); } }); From 408f9ef1488cdd6a8f70e8dec122ad2e92f9318e Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 7 Aug 2026 14:49:19 +0100 Subject: [PATCH 047/154] Fix `any` type usages in frontend code (#7326) # Description of Changes Continued effort towards removing all uses of the `any` type in our frontend code. This PR fixes 10 more folders and removes them from the exclude list. All of them were really simple fixes. --- .../components/pageEditor/commands/pageCommands.ts | 1 - .../components/pageEditor/hooks/useEditorCommands.ts | 9 ++++----- .../pageEditor/hooks/useUndoManagerState.ts | 7 +++++-- .../components/shared/config/SettingsSearchBar.tsx | 2 +- .../shared/pageEditor/useFileItemDragDrop.ts | 8 +++++--- .../bookletImposition/BookletImpositionSettings.tsx | 11 +++++++---- .../components/tools/shared/ToolWorkflowTitle.tsx | 3 ++- .../components/tools/shared/renderToolButtons.tsx | 10 ++++++++-- .../core/hooks/signing/useSigningSessionController.ts | 5 +++-- .../adjustContrast/useAdjustContrastOperation.ts | 5 +++-- .../core/hooks/tools/convert/useConvertOperation.ts | 4 ++-- .../removePassword/useRemovePasswordOperation.test.ts | 2 +- frontend/eslint.config.mjs | 10 ---------- 13 files changed, 41 insertions(+), 36 deletions(-) diff --git a/frontend/editor/src/core/components/pageEditor/commands/pageCommands.ts b/frontend/editor/src/core/components/pageEditor/commands/pageCommands.ts index 1f80b940cd..db9385fc37 100644 --- a/frontend/editor/src/core/components/pageEditor/commands/pageCommands.ts +++ b/frontend/editor/src/core/components/pageEditor/commands/pageCommands.ts @@ -686,7 +686,6 @@ export class InsertFilesCommand extends DOMCommand { private insertedPages: PDFPage[] = []; private originalDocument: PDFDocument | null = null; private fileDataMap = new Map(); // Store file data for thumbnail generation - private originalProcessedFile: any = null; // Store original ProcessedFile for undo private insertedFileMap = new Map(); // Store inserted files for export constructor( diff --git a/frontend/editor/src/core/components/pageEditor/hooks/useEditorCommands.ts b/frontend/editor/src/core/components/pageEditor/hooks/useEditorCommands.ts index a790e94085..aca8390d64 100644 --- a/frontend/editor/src/core/components/pageEditor/hooks/useEditorCommands.ts +++ b/frontend/editor/src/core/components/pageEditor/hooks/useEditorCommands.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from "react"; import { BulkRotateCommand, DeletePagesCommand, + DOMCommand, PageBreakCommand, ReorderPagesCommand, SplitCommand, @@ -24,7 +25,7 @@ interface UsePageEditorCommandsParams { selectedPageIds: string[]; setSelectedPageIds: (ids: string[]) => void; getPageNumbersFromIds: (pageIds: string[]) => number[]; - executeCommandWithTracking: (command: any) => void; + executeCommandWithTracking: (command: DOMCommand) => void; updateFileOrderFromPages: (pages: PDFPage[]) => void; actions: FileActions; selectors: FileSelectors; @@ -145,10 +146,8 @@ export const usePageEditorCommands = ({ [executeCommandWithTracking, setSplitPositions], ); - const executeCommand = useCallback((command: any) => { - if (command && typeof command.execute === "function") { - command.execute(); - } + const executeCommand = useCallback((command: { execute: () => void }) => { + command.execute(); }, []); const handleRotate = useCallback( diff --git a/frontend/editor/src/core/components/pageEditor/hooks/useUndoManagerState.ts b/frontend/editor/src/core/components/pageEditor/hooks/useUndoManagerState.ts index 86fdf347d2..0e11aa1e80 100644 --- a/frontend/editor/src/core/components/pageEditor/hooks/useUndoManagerState.ts +++ b/frontend/editor/src/core/components/pageEditor/hooks/useUndoManagerState.ts @@ -1,6 +1,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { UndoManager } from "@app/components/pageEditor/commands/pageCommands"; +import { + DOMCommand, + UndoManager, +} from "@app/components/pageEditor/commands/pageCommands"; interface UseUndoManagerStateParams { setHasUnsavedChanges: (dirty: boolean) => void; @@ -29,7 +32,7 @@ export const useUndoManagerState = ({ }, [updateUndoRedoState]); const executeCommandWithTracking = useCallback( - (command: any) => { + (command: DOMCommand) => { undoManagerRef.current.executeCommand(command); setHasUnsavedChanges(true); }, diff --git a/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx b/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx index ea1819dfb2..397347dcf6 100644 --- a/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx +++ b/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx @@ -138,7 +138,7 @@ export const SettingsSearchBar: React.FC = ({ const translationPrefixes = getTranslationPrefixesForNavKey(item.key); const translationContent = translationPrefixes.flatMap((prefix) => flattenTranslationStrings( - t(prefix, { returnObjects: true, defaultValue: {} } as any), + t(prefix, { returnObjects: true, defaultValue: {} }), ), ); diff --git a/frontend/editor/src/core/components/shared/pageEditor/useFileItemDragDrop.ts b/frontend/editor/src/core/components/shared/pageEditor/useFileItemDragDrop.ts index b51b90737a..c1f5d9aaf3 100644 --- a/frontend/editor/src/core/components/shared/pageEditor/useFileItemDragDrop.ts +++ b/frontend/editor/src/core/components/shared/pageEditor/useFileItemDragDrop.ts @@ -111,8 +111,7 @@ export const useFileItemDragDrop = ({ if (!element) return; const rect = element.getBoundingClientRect(); - const clientY = - (source as any).element?.getBoundingClientRect().top || 0; + const clientY = source.element?.getBoundingClientRect().top || 0; const midpoint = rect.top + rect.height / 2; setDropPosition(clientY < midpoint ? "below" : "above"); @@ -121,7 +120,10 @@ export const useFileItemDragDrop = ({ setIsDragOver(false); const dropPos = dropPositionRef.current; setDropPosition("below"); - const sourceData = source.data as any; + const sourceData = source.data as { + type?: string; + fromIndex?: number; + }; if (sourceData?.type === "file-item") { const fromIndex = sourceData.fromIndex as number; let toIndex = indexRef.current; diff --git a/frontend/editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx b/frontend/editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx index fab1f3ff5e..1fbf447a21 100644 --- a/frontend/editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx +++ b/frontend/editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.tsx @@ -14,9 +14,9 @@ import ButtonSelector from "@app/components/shared/ButtonSelector"; interface BookletImpositionSettingsProps { parameters: BookletImpositionParameters; - onParameterChange: ( - key: keyof BookletImpositionParameters, - value: any, + onParameterChange: ( + key: K, + value: BookletImpositionParameters[K], ) => void; disabled?: boolean; } @@ -214,7 +214,10 @@ const BookletImpositionSettings = ({ )} value={parameters.gutterSize} onChange={(value) => - onParameterChange("gutterSize", value || 12) + onParameterChange( + "gutterSize", + typeof value === "number" ? value : 12, + ) } min={6} max={72} diff --git a/frontend/editor/src/core/components/tools/shared/ToolWorkflowTitle.tsx b/frontend/editor/src/core/components/tools/shared/ToolWorkflowTitle.tsx index bc5d362666..60f7448f7e 100644 --- a/frontend/editor/src/core/components/tools/shared/ToolWorkflowTitle.tsx +++ b/frontend/editor/src/core/components/tools/shared/ToolWorkflowTitle.tsx @@ -2,13 +2,14 @@ import React from "react"; import { Flex, Text, Divider } from "@mantine/core"; import LocalIcon from "@app/components/shared/LocalIcon"; import { Tooltip } from "@app/components/shared/Tooltip"; +import { TooltipTip } from "@app/types/tips"; export interface ToolWorkflowTitleProps { title: string; description?: string; tooltip?: { content?: React.ReactNode; - tips?: any[]; + tips?: TooltipTip[]; header?: { title: string; logo?: React.ReactNode; diff --git a/frontend/editor/src/core/components/tools/shared/renderToolButtons.tsx b/frontend/editor/src/core/components/tools/shared/renderToolButtons.tsx index b17cad7c61..d63230914e 100644 --- a/frontend/editor/src/core/components/tools/shared/renderToolButtons.tsx +++ b/frontend/editor/src/core/components/tools/shared/renderToolButtons.tsx @@ -2,7 +2,10 @@ import { Box } from "@mantine/core"; import ToolButton from "@app/components/tools/toolPicker/ToolButton"; import SubcategoryHeader from "@app/components/tools/shared/SubcategoryHeader"; -import { getSubcategoryLabel } from "@app/data/toolsTaxonomy"; +import { + getSubcategoryLabel, + type ToolRegistryEntry, +} from "@app/data/toolsTaxonomy"; import { TFunction } from "i18next"; import { SubcategoryGroup } from "@app/hooks/useToolSections"; import { ToolId } from "@app/types/toolId"; @@ -15,7 +18,10 @@ export const renderToolButtons = ( onSelect: (id: ToolId) => void, showSubcategoryHeader: boolean = true, disableNavigation: boolean = false, - searchResults?: Array<{ item: [string, any]; matchedText?: string }>, + searchResults?: Array<{ + item: [ToolId, ToolRegistryEntry]; + matchedText?: string; + }>, hasStars: boolean = false, ) => { // Create a map of matched text for quick lookup diff --git a/frontend/editor/src/core/hooks/signing/useSigningSessionController.ts b/frontend/editor/src/core/hooks/signing/useSigningSessionController.ts index 8fb1ddd5b6..e114a16f9d 100644 --- a/frontend/editor/src/core/hooks/signing/useSigningSessionController.ts +++ b/frontend/editor/src/core/hooks/signing/useSigningSessionController.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { isAxiosError } from "axios"; import apiClient from "@app/services/apiClient"; import { alert } from "@app/components/toast"; import { fileStorage } from "@app/services/fileStorage"; @@ -333,8 +334,8 @@ export function useSigningSessionController(enabled: boolean) { pdfFile = new File([pdfResponse.data], session.documentName, { type: "application/pdf", }); - } catch (pdfError: any) { - if (pdfError?.response?.status === 404) { + } catch (pdfError) { + if (isAxiosError(pdfError) && pdfError.response?.status === 404) { alert({ alertType: "warning", title: t("certSign.sessions.pdfNotReady", "PDF Not Ready"), diff --git a/frontend/editor/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts b/frontend/editor/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts index 18981b58d0..858ab8c7ef 100644 --- a/frontend/editor/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts +++ b/frontend/editor/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts @@ -13,9 +13,10 @@ import { pdfWorkerManager } from "@app/services/pdfWorkerManager"; import { createFileFromApiResponse } from "@app/utils/fileResponseUtils"; import { getPdfiumModule, saveRawDocument } from "@app/services/pdfiumService"; import { copyRgbaToBgraHeap } from "@app/utils/pdfiumBitmapUtils"; +import type { PDFDocumentProxy } from "pdfjs-dist"; async function renderPdfPageToCanvas( - pdf: any, + pdf: PDFDocumentProxy, pageNumber: number, scale: number, ): Promise { @@ -26,7 +27,7 @@ async function renderPdfPageToCanvas( canvas.height = viewport.height; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas 2D context unavailable"); - await page.render({ canvasContext: ctx, viewport }).promise; + await page.render({ canvasContext: ctx, canvas, viewport }).promise; return canvas; } diff --git a/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts index d6175e2b8b..d912cc4ede 100644 --- a/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/editor/src/core/hooks/tools/convert/useConvertOperation.ts @@ -210,8 +210,8 @@ export const buildConvertFormData = ( // Static function that can be used by both the hook and automation executor export const createFileFromResponse = ( - responseData: any, - headers: any, + responseData: Blob, + headers: Record, originalFileName: string, targetExtension: string, ): File => { diff --git a/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts b/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts index b60aa09762..8f2953afde 100644 --- a/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts +++ b/frontend/editor/src/core/hooks/tools/removePassword/useRemovePasswordOperation.test.ts @@ -85,7 +85,7 @@ describe("useRemovePasswordOperation", () => { const testFile = new File(["test content"], "test.pdf", { type: "application/pdf", }); - const formData = buildFormData(testParameters, testFile as any); + const formData = buildFormData(testParameters, testFile); // Verify the form data contains the file expect(formData.get("fileInput")).toBe(testFile); diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 256bdf640a..43fe732a4e 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -284,28 +284,18 @@ export default defineConfig( ignores: [ "editor/src/core/components/annotation/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/pageEditor/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/components/pageEditor/commands/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/components/pageEditor/hooks/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/shared/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/components/shared/config/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/shared/config/configSections/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/components/shared/pageEditor/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/tools/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/tools/addStamp/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/tools/automate/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/components/tools/bookletImposition/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/tools/certSign/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/tools/pdfTextEditor/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/components/tools/shared/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/components/viewer/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/contexts/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/contexts/file/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/contexts/viewer/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/hooks/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/hooks/signing/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/hooks/tools/adjustContrast/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/hooks/tools/convert/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/hooks/tools/removePassword/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/hooks/tools/shared/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/services/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/tools/annotate/useAnnotationSelection.ts", From 37a48aa7a700fc0d9e33a8dad87da4a0857b3e3a Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 7 Aug 2026 17:02:11 +0100 Subject: [PATCH 048/154] Replace ESLint and dpdm with Oxlint (#7330) # Description of Changes Smaller scope than #6689 to try and get this finished. Replace ESLint and dpdm with Oxlint, a TS linter written in Rust so its performance is dramatically better than the existing tools we use. ## Speed improvement - Current ESLint run: 13.76s - Current dpdm run: 3.59s - Total time: 17.35s - New Oxlint run: 0.90s So Oxlint is about a 20x speed improvement. ## Differences When I last tried to do this, we could recreate our rules identically with Oxlint, but that's not true any more. Oxlint has no current equivalent for ESLint's `no-restricted-syntax` rule, which we were using to ban usages of ` @@ -90,8 +90,8 @@ function MockChatContent({ maxWidth: "82%", background: m.role === "user" - ? "#3b82f6" - : "var(--c-surface-sunken, #f3f4f6)", + ? "var(--c-primary)" + : "var(--c-surface-sunken)", color: m.role === "user" ? "#fff" : "inherit", borderRadius: 10, padding: "8px 12px", @@ -108,17 +108,17 @@ function MockChatContent({
    What do you want to do? @@ -149,7 +149,7 @@ function ChatFABWidgetDemo({ width: "100%", height: "100%", overflow: "hidden", - background: "var(--c-bg, #f8f9fb)", + background: "var(--c-bg)", }} > {/* FAB button */} @@ -249,7 +249,7 @@ function ChatFABFullFlowDemo() { padding: "4px 10px", borderRadius: 6, background: - step === s ? "#3b82f6" : "var(--c-surface-sunken, #f3f4f6)", + step === s ? "var(--c-primary)" : "var(--c-surface-sunken)", color: step === s ? "#fff" : "inherit", fontWeight: step === s ? 600 : 400, }} diff --git a/frontend/editor/src/portal/data/Ops.stories.tsx b/frontend/editor/src/portal/data/Ops.stories.tsx index 8e59e5f5ef..502791e2d1 100644 --- a/frontend/editor/src/portal/data/Ops.stories.tsx +++ b/frontend/editor/src/portal/data/Ops.stories.tsx @@ -27,7 +27,7 @@ const STAGE_ORDER: OpKind[] = [ const STAGE_COLOUR: Record = { ingest: "var(--color-green)", validate: "var(--c-primary)", - modify: "#F97316", + modify: "var(--color-orange)", secure: "var(--color-red)", store: "var(--color-purple)", alert: "var(--color-amber)", From 59ed4f5fd117cac60997cc3148c02c09e0743f41 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:14:56 +0100 Subject: [PATCH 066/154] Fix automate unrunnable tools (#7311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes Fix automate unrunnable tools ## Problem - Remove Image failed in Automate with `Tool operation not supported: removeImage` - Its registry entry had `operationConfig: undefined` even though the config existed and was already tested - The Automate picker only filtered on `supportsAutomate`, never on `operationConfig` — so broken tools were selectable and failed only at run time ## Fixes - Wire up `removeImage` and `pageLayout` operation configs (both already existed, just never registered) - Exclude `validateSignature` (report tool, not on the operationConfig seam) and `scannerEffect` (no frontend implementation) via `supportsAutomate: false` - Picker now also filters on `operationConfig`, so this class of bug can't reach users again - `overlay-pdfs` returns 400 instead of 500 when overlay files or mode are missing - Fix `new URL().pathname` Windows path bug that stopped 2 test suites from loading --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../controller/api/PdfOverlayController.java | 20 +++++++++++ .../tools/automate/ToolSelector.tsx | 8 +++-- ...tomatableToolsHaveOperationConfig.test.tsx | 33 +++++++++++++++++++ .../core/data/useTranslatedToolRegistry.tsx | 11 ++++++- .../src/core/utils/toolIOCompat.test.ts | 5 ++- .../src/core/utils/toolIOLabels.test.ts | 5 ++- 6 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index 7f17738d98..1d369d282d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -61,6 +61,7 @@ public class PdfOverlayController { int overlayPos = request.getOverlayPosition(); MultipartFile[] overlayFiles = request.getOverlayFiles(); + validateOverlayFiles(overlayFiles); File[] overlayPdfFiles = new File[overlayFiles.length]; List tempFiles = new ArrayList<>(); // List to keep track of temporary files @@ -120,10 +121,29 @@ public class PdfOverlayController { } } + // Both fields are declared required, but @ModelAttribute binding leaves them null when the + // caller omits them, which would otherwise surface as a 500 instead of a 400. + private void validateOverlayFiles(MultipartFile[] overlayFiles) { + if (overlayFiles == null || overlayFiles.length == 0) { + throw ExceptionUtils.createIllegalArgumentException( + "error.overlayFilesRequired", "At least one overlay file is required"); + } + for (MultipartFile overlayFile : overlayFiles) { + if (overlayFile == null || overlayFile.isEmpty()) { + throw ExceptionUtils.createIllegalArgumentException( + "error.overlayFileEmpty", "Overlay files must not be empty"); + } + } + } + private Map prepareOverlayGuide( int basePageCount, File[] overlayFiles, String mode, int[] counts, List tempFiles) throws IOException { Map overlayGuide = new HashMap<>(); + if (mode == null) { + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidFormat", "Invalid {0} format: {1}", "overlay mode", "null"); + } switch (mode) { case "SequentialOverlay": sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles); diff --git a/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx b/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx index c5c3998bc0..ad7bb33a6e 100644 --- a/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx +++ b/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx @@ -34,13 +34,17 @@ export default function ToolSelector({ const [shouldAutoFocus, setShouldAutoFocus] = useState(false); const containerRef = useRef(null); - // Filter out excluded tools (like 'automate' itself) and tools that don't support automation + // Filter out excluded tools (like 'automate' itself), tools that don't support + // automation, and tools with no operationConfig - the executor resolves a step + // through operationConfig, so offering one without it fails only at run time. const baseFilteredTools = useMemo(() => { return ( Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][] ).filter( ([key, tool]) => - !excludeTools.includes(key) && getToolSupportsAutomate(tool), + !excludeTools.includes(key) && + getToolSupportsAutomate(tool) && + Boolean(tool.operationConfig), ); }, [toolRegistry, excludeTools]); diff --git a/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx b/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx new file mode 100644 index 0000000000..461c07ebdf --- /dev/null +++ b/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx @@ -0,0 +1,33 @@ +/** + * Registry invariant: the Automate picker offers a tool whenever it doesn't opt out via + * `supportsAutomate: false`, but automationExecutor resolves each step through the tool's + * `operationConfig`. A tool that is offered without one is selectable in the builder and + * only fails when the automation runs, with "Tool operation not supported: ". + * + * So a tool must either carry an operationConfig or declare supportsAutomate: false. + */ +import { describe, expect, test, vi } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { useTranslatedToolCatalog } from "@app/data/useTranslatedToolRegistry"; +import { getToolSupportsAutomate } from "@app/data/toolsTaxonomy"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string) => fallback ?? key, + i18n: { changeLanguage: vi.fn(), language: "en-US" }, + }), + Trans: ({ children }: { children?: unknown }) => children, +})); + +describe("automatable tools", () => { + test("every tool offered to Automate can be executed as a step", () => { + const { result } = renderHook(() => useTranslatedToolCatalog()); + + const offeredWithoutConfig = Object.entries(result.current.regularTools) + .filter(([, entry]) => entry && getToolSupportsAutomate(entry)) + .filter(([, entry]) => !entry.operationConfig) + .map(([id]) => id); + + expect(offeredWithoutConfig).toEqual([]); + }); +}); diff --git a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx index 16b69d6983..0a39da2fb9 100644 --- a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx @@ -50,6 +50,8 @@ import { changeMetadataOperationConfig } from "@app/hooks/tools/changeMetadata/u import { signOperationConfig } from "@app/hooks/tools/sign/useSignOperation"; import { cropOperationConfig } from "@app/hooks/tools/crop/useCropOperation"; import { removeAnnotationsOperationConfig } from "@app/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation"; +import { removeImageOperationConfig } from "@app/hooks/tools/removeImage/useRemoveImageOperation"; +import { pageLayoutOperationConfig } from "@app/hooks/tools/pageLayout/usePageLayoutOperation"; import { extractImagesOperationConfig } from "@app/hooks/tools/extractImages/useExtractImagesOperation"; import { replaceColorOperationConfig } from "@app/hooks/tools/replaceColor/useReplaceColorOperation"; import { removePagesOperationConfig } from "@app/hooks/tools/removePages/useRemovePagesOperation"; @@ -526,6 +528,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["validate-signature"], synonyms: getSynonyms(t, "validateSignature"), + // Reports on signatures rather than transforming the PDF, and its hook is + // not on the operationConfig seam, so it cannot run as an automation step. + supportsAutomate: false, automationSettings: null, }, @@ -755,6 +760,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["multi-page-layout"], + operationConfig: asRegistryConfig(pageLayoutOperationConfig), automationSettings: lazySettings( () => import("@app/components/tools/pageLayout/PageLayoutSettings"), ), @@ -967,7 +973,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-image-pdf"], - operationConfig: undefined, + operationConfig: asRegistryConfig(removeImageOperationConfig), synonyms: getSynonyms(t, "removeImage"), automationSettings: null, }, @@ -1196,6 +1202,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, endpoints: ["scanner-effect"], synonyms: getSynonyms(t, "scannerEffect"), + // No frontend implementation yet (component is null), so it has no + // operationConfig to execute as an automation step. + supportsAutomate: false, automationSettings: null, }, diff --git a/frontend/editor/src/core/utils/toolIOCompat.test.ts b/frontend/editor/src/core/utils/toolIOCompat.test.ts index c0e9d9efa3..b3fb117705 100644 --- a/frontend/editor/src/core/utils/toolIOCompat.test.ts +++ b/frontend/editor/src/core/utils/toolIOCompat.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { validateToolChain, @@ -24,7 +25,9 @@ interface SharedCase { /** Shared with the backend and engine, so it lives at the repo root. */ function casesFile(): string { - let current = dirname(new URL(import.meta.url).pathname); + // fileURLToPath, not URL.pathname: on Windows the latter yields "/C:/..." and + // resolving against it produces a "C:\C:\..." path that never matches. + let current = dirname(fileURLToPath(import.meta.url)); for (let i = 0; i < 12; i++) { const candidate = resolve(current, "testing/tool-io-cases.json"); try { diff --git a/frontend/editor/src/core/utils/toolIOLabels.test.ts b/frontend/editor/src/core/utils/toolIOLabels.test.ts index ac6b4c155b..98c529e677 100644 --- a/frontend/editor/src/core/utils/toolIOLabels.test.ts +++ b/frontend/editor/src/core/utils/toolIOLabels.test.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { TOOL_FORMATS, type ToolFormat } from "@app/types/toolIO"; import { @@ -9,7 +10,9 @@ import { /** The en-US `[toolFormat]` block, read straight from the locale file. */ function toolFormatLabels(): Record { - let current = dirname(new URL(import.meta.url).pathname); + // fileURLToPath, not URL.pathname: on Windows the latter yields "/C:/..." and + // resolving against it produces a "C:\C:\..." path that never matches. + let current = dirname(fileURLToPath(import.meta.url)); for (let i = 0; i < 12; i++) { try { const toml = readFileSync( From 35a861f4f80f2816b22ec57fef2c1a0a6b9906b2 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:42:44 +0100 Subject: [PATCH 067/154] feat(editor): move endpoint availability onto TanStack Query (#7285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes > Stacked on #7264, sibling of #7283. Independent of #7283 — the only overlap is two additive lines in `core/query/keys.ts` and `core/api/config.ts`. Either can merge first. ## The problem `useEndpointConfig` kept its own cache: a module-level `globalFetchDone` boolean, a mutable `globalEndpointCache` object, and a `resetGlobalCache()` called from the JWT listener. Which consumer mounted first decided who paid for the request, and nothing invalidated it except a page reload. ## End state One shared query for the whole availability map; each of the 12 consumers projects the endpoints it asked for. **251 lines to 101**, same return shape, no consumer changes. | | Before | After | |---|---|---| | Cross-consumer cache | `globalFetchDone` + mutable module object | query key | | Invalidation | `resetGlobalCache()` mutating that object | `invalidateQueries` | | Per-endpoint check | own `useState` triple | query keyed by endpoint | Behaviour kept deliberately: - **Unknown endpoints and any failure still read as enabled.** This fires before auth settles, and disabling every tool on a hiccup is worse than letting one call fail later. - **`retry` is off for the availability map.** The fallback *is* the answer, so retrying only doubles a request every logged-out visitor makes on load. ## Desktop is untouched `desktop/hooks/useEndpointConfig.ts` shadows this module entirely — no shared code, so core converting doesn't affect it and there's no half-migrated state. It's 482 lines of orchestration rather than fetching: dependency-ready gating, `tauriBackendService` and `selfHostedServerMonitor` subscriptions, a 2.5s timeout retry for backend startup, a legacy `?endpoints=` fallback for old servers, and SaaS-routing optimism that rewrites disabled endpoints to enabled. It also has no test coverage to convert against, and it decides whether tools appear at all in the desktop app. That's a different job from this one and wants its own review. Next PR. ## Testing 9 new tests: projection onto the requested subset, one request across consumers, unknown-endpoint fallback, failure fallback with no retry, empty-list no-fetch, JWT invalidation, and the three single-endpoint cases. `task frontend:check` green: 1677 tests across 192 files, typecheck on all five flavours, eslint, dpdm, prettier. Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> --- frontend/editor/src/core/api/config.ts | 30 ++ .../src/core/hooks/useEndpointConfig.test.tsx | 149 +++++++++ .../src/core/hooks/useEndpointConfig.ts | 282 ++++-------------- frontend/editor/src/core/query/keys.ts | 3 + 4 files changed, 248 insertions(+), 216 deletions(-) create mode 100644 frontend/editor/src/core/hooks/useEndpointConfig.test.tsx diff --git a/frontend/editor/src/core/api/config.ts b/frontend/editor/src/core/api/config.ts index f0ba730e8c..94caba2c82 100644 --- a/frontend/editor/src/core/api/config.ts +++ b/frontend/editor/src/core/api/config.ts @@ -1,6 +1,7 @@ import apiClient from "@app/services/apiClient"; import { getSimulatedAppConfig } from "@app/testing/serverExperienceSimulations"; import type { AppConfig } from "@app/types/appConfig"; +import type { EndpointAvailabilityDetails } from "@app/types/endpointAvailability"; /** Unauthenticated and unreachable both mean "assume login is on". */ export const DEFAULT_APP_CONFIG: AppConfig = { enableLogin: true }; @@ -23,6 +24,35 @@ export async function fetchAppConfig(): Promise { } } +export type EndpointAvailabilityMap = Record< + string, + EndpointAvailabilityDetails +>; + +/** + * Fires on app load before auth settles, so a 401 must not trigger the global + * login redirect. Callers treat a failure as "assume enabled". + */ +export async function fetchEndpointsAvailability(): Promise { + const response = await apiClient.get( + "/api/v1/config/endpoints-availability", + { suppressErrorToast: true, skipAuthRedirect: true }, + ); + return Object.fromEntries( + Object.entries(response.data).map(([name, detail]) => [ + name, + { enabled: detail?.enabled ?? true, reason: detail?.reason ?? null }, + ]), + ); +} + +export async function fetchEndpointEnabled(endpoint: string): Promise { + const response = await apiClient.get( + `/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, + ); + return response.data; +} + export interface FooterInfo { analyticsEnabled?: boolean; termsAndConditions?: string; diff --git a/frontend/editor/src/core/hooks/useEndpointConfig.test.tsx b/frontend/editor/src/core/hooks/useEndpointConfig.test.tsx new file mode 100644 index 0000000000..59010dacd5 --- /dev/null +++ b/frontend/editor/src/core/hooks/useEndpointConfig.test.tsx @@ -0,0 +1,149 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor, act } from "@testing-library/react"; +import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider"; +import { + useEndpointEnabled, + useMultipleEndpointsEnabled, +} from "@app/hooks/useEndpointConfig"; +import { + fetchEndpointEnabled, + fetchEndpointsAvailability, +} from "@app/api/config"; + +vi.mock("@app/api/config", () => ({ + fetchEndpointEnabled: vi.fn(), + fetchEndpointsAvailability: vi.fn(), +})); + +const mockOne = vi.mocked(fetchEndpointEnabled); +const mockAll = vi.mocked(fetchEndpointsAvailability); + +describe("useEndpointEnabled", () => { + beforeEach(() => vi.clearAllMocks()); + + it("reports null while loading, then the server's answer", async () => { + mockOne.mockResolvedValue(false); + + const { result } = renderHook(() => useEndpointEnabled("ocr-pdf"), { + wrapper: TestQueryProvider, + }); + + expect(result.current.enabled).toBeNull(); + await waitFor(() => expect(result.current.enabled).toBe(false)); + }); + + it("stays null on failure rather than claiming disabled", async () => { + mockOne.mockRejectedValue(new Error("boom")); + + const { result } = renderHook(() => useEndpointEnabled("ocr-pdf"), { + wrapper: TestQueryProvider, + }); + + await waitFor(() => expect(result.current.error).toBe("boom")); + expect(result.current.enabled).toBeNull(); + }); + + it("does not fetch without an endpoint", () => { + const { result } = renderHook(() => useEndpointEnabled(""), { + wrapper: TestQueryProvider, + }); + + expect(result.current.loading).toBe(false); + expect(mockOne).not.toHaveBeenCalled(); + }); +}); + +describe("useMultipleEndpointsEnabled", () => { + beforeEach(() => vi.clearAllMocks()); + + it("projects the shared map onto the requested endpoints", async () => { + mockAll.mockResolvedValue({ + "ocr-pdf": { enabled: false, reason: "DEPENDENCY" }, + "add-stamp": { enabled: true, reason: null }, + }); + + const { result } = renderHook( + () => useMultipleEndpointsEnabled(["ocr-pdf"]), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => + expect(result.current.endpointStatus).toEqual({ "ocr-pdf": false }), + ); + expect(result.current.endpointDetails["ocr-pdf"].reason).toBe("DEPENDENCY"); + }); + + it("serves every consumer from one request", async () => { + mockAll.mockResolvedValue({ "ocr-pdf": { enabled: true, reason: null } }); + + const { result } = renderHook( + () => ({ + a: useMultipleEndpointsEnabled(["ocr-pdf"]), + b: useMultipleEndpointsEnabled(["ocr-pdf", "add-stamp"]), + }), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => expect(result.current.a.loading).toBe(false)); + expect(mockAll).toHaveBeenCalledTimes(1); + }); + + it("treats unknown endpoints as enabled", async () => { + mockAll.mockResolvedValue({}); + + const { result } = renderHook( + () => useMultipleEndpointsEnabled(["brand-new-tool"]), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => + expect(result.current.endpointStatus).toEqual({ "brand-new-tool": true }), + ); + }); + + it("falls back to enabled when the check fails", async () => { + mockAll.mockRejectedValue( + Object.assign(new Error("unauthorised"), { response: { status: 401 } }), + ); + + const { result } = renderHook( + () => useMultipleEndpointsEnabled(["ocr-pdf", "add-stamp"]), + { wrapper: TestQueryProvider }, + ); + + await waitFor(() => + expect(result.current.endpointStatus).toEqual({ + "ocr-pdf": true, + "add-stamp": true, + }), + ); + // The fallback is the answer, so no retry. + expect(mockAll).toHaveBeenCalledTimes(1); + }); + + it("does not fetch for an empty endpoint list", () => { + const { result } = renderHook(() => useMultipleEndpointsEnabled([]), { + wrapper: TestQueryProvider, + }); + + expect(result.current.loading).toBe(false); + expect(mockAll).not.toHaveBeenCalled(); + }); + + it("refetches when a JWT becomes available", async () => { + mockAll.mockResolvedValue({ "ocr-pdf": { enabled: true, reason: null } }); + + const { result } = renderHook( + () => useMultipleEndpointsEnabled(["ocr-pdf"]), + { wrapper: TestQueryProvider }, + ); + await waitFor(() => expect(result.current.loading).toBe(false)); + + await act(async () => { + window.dispatchEvent(new CustomEvent("jwt-available")); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + await waitFor(() => expect(mockAll).toHaveBeenCalledTimes(2)); + }); +}); diff --git a/frontend/editor/src/core/hooks/useEndpointConfig.ts b/frontend/editor/src/core/hooks/useEndpointConfig.ts index 4615164787..488375c853 100644 --- a/frontend/editor/src/core/hooks/useEndpointConfig.ts +++ b/frontend/editor/src/core/hooks/useEndpointConfig.ts @@ -1,75 +1,50 @@ -import { useCallback, useEffect, useState } from "react"; -import { isAxiosError } from "axios"; -import apiClient from "@app/services/apiClient"; +import { useCallback, useMemo } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + fetchEndpointEnabled, + fetchEndpointsAvailability, +} from "@app/api/config"; +import { qk } from "@app/query/keys"; +import { CONFIG_STALE_TIME } from "@app/query/staleTime"; import { useJwtConfigSync } from "@app/hooks/useJwtConfigSync"; import type { EndpointAvailabilityDetails } from "@app/types/endpointAvailability"; -// Track whether we've done the global fetch to prevent duplicate requests -let globalFetchDone = false; -const globalEndpointCache: Record = {}; +const OPTIMISTIC: EndpointAvailabilityDetails = { enabled: true, reason: null }; -function resetGlobalCache() { - globalFetchDone = false; - Object.keys(globalEndpointCache).forEach( - (key) => delete globalEndpointCache[key], - ); +function message(error: unknown): string | null { + if (!error) return null; + return error instanceof Error ? error.message : "Unknown error occurred"; } -/** - * Hook to check if a specific endpoint is enabled - * This wraps the context for single endpoint checks - */ +/** Whether one endpoint is enabled. `null` while loading and on failure. */ export function useEndpointEnabled(endpoint: string): { enabled: boolean | null; loading: boolean; error: string | null; refetch: () => Promise; } { - const [enabled, setEnabled] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchEndpointStatus = async () => { - if (!endpoint) { - setEnabled(null); - setLoading(false); - return; - } - - try { - setLoading(true); - setError(null); - console.debug("[useEndpointConfig] Fetch endpoint status", { endpoint }); - - const response = await apiClient.get( - `/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, - ); - const isEnabled = response.data; - setEnabled(isEnabled); - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : "Unknown error occurred"; - setError(errorMessage); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchEndpointStatus(); - }, [endpoint]); + const { data, isPending, error, refetch } = useQuery({ + queryKey: qk.endpointEnabled(endpoint), + queryFn: () => fetchEndpointEnabled(endpoint), + enabled: Boolean(endpoint), + staleTime: CONFIG_STALE_TIME, + }); return { - enabled, - loading, - error, - refetch: fetchEndpointStatus, + enabled: data ?? null, + loading: Boolean(endpoint) && isPending, + error: message(error), + refetch: useCallback(async () => { + await refetch(); + }, [refetch]), }; } /** - * Hook to check multiple endpoints at once using batch API - * Returns a map of endpoint -> enabled status + * Availability for a set of endpoints, projected from one shared request for + * the whole map. Unknown endpoints and any failure read as enabled — this runs + * before auth settles, and disabling every tool on a hiccup is worse than + * letting a call fail later. */ export function useMultipleEndpointsEnabled(endpoints: string[]): { endpointStatus: Record; @@ -78,174 +53,49 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { error: string | null; refetch: () => Promise; } { - const [endpointStatus, setEndpointStatus] = useState>( - {}, - ); - const [endpointDetails, setEndpointDetails] = useState< - Record - >({}); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const queryClient = useQueryClient(); + const wanted = endpoints ?? []; - const fetchAllEndpointStatuses = useCallback( - async (force = false) => { - // Skip if already fetched globally and not forced - if (!force && globalFetchDone) { - console.debug("[useEndpointConfig] Using global cache"); - const cached = endpoints.reduce( - (acc, endpoint) => { - const cachedDetails = globalEndpointCache[endpoint]; - if (cachedDetails) { - acc.status[endpoint] = cachedDetails.enabled; - acc.details[endpoint] = cachedDetails; - } else { - acc.status[endpoint] = true; - } - return acc; - }, - { - status: {} as Record, - details: {} as Record, - }, - ); - setEndpointStatus(cached.status); - setEndpointDetails((prev) => ({ ...prev, ...cached.details })); - setLoading(false); - return; - } + const { data, isPending, error, refetch } = useQuery({ + queryKey: qk.endpointsAvailability(), + queryFn: fetchEndpointsAvailability, + enabled: wanted.length > 0, + staleTime: CONFIG_STALE_TIME, + // A failure already falls back to enabled, so a retry buys nothing and + // doubles a request that fires on load for every logged-out visitor. + retry: false, + }); - if (!endpoints || endpoints.length === 0) { - setEndpointStatus({}); - setEndpointDetails({}); - setLoading(false); - return; - } + const reload = useCallback(async () => { + await refetch(); + }, [refetch]); - try { - setLoading(true); - setError(null); - console.debug( - "[useEndpointConfig] Fetching all endpoint statuses from server", - ); - - // Fetch all endpoints at once; auto-fires on app load, so a 401 must - // fail silently instead of triggering the global login redirect. - const response = await apiClient.get< - Record - >(`/api/v1/config/endpoints-availability`, { - suppressErrorToast: true, - skipAuthRedirect: true, - }); - - // Populate global cache with all results - Object.entries(response.data).forEach(([endpoint, details]) => { - globalEndpointCache[endpoint] = { - enabled: details?.enabled ?? true, - reason: details?.reason ?? null, - }; - }); - globalFetchDone = true; - - // Return status for the requested endpoints - const fullStatus = endpoints.reduce( - (acc, endpoint) => { - const cachedDetails = globalEndpointCache[endpoint]; - if (cachedDetails) { - acc.status[endpoint] = cachedDetails.enabled; - acc.details[endpoint] = cachedDetails; - } else { - acc.status[endpoint] = true; - } - return acc; - }, - { - status: {} as Record, - details: {} as Record, - }, - ); - - setEndpointStatus(fullStatus.status); - setEndpointDetails((prev) => ({ ...prev, ...fullStatus.details })); - } catch (err: unknown) { - // On 401 (auth error), use optimistic fallback instead of disabling - if (isAxiosError(err) && err.response?.status === 401) { - console.warn( - "[useEndpointConfig] 401 error - using optimistic fallback", - ); - endpoints.forEach((endpoint) => { - globalEndpointCache[endpoint] = { enabled: true, reason: null }; - }); - const optimisticStatus = endpoints.reduce( - (acc, endpoint) => { - acc.status[endpoint] = true; - acc.details[endpoint] = { enabled: true, reason: null }; - return acc; - }, - { - status: {} as Record, - details: {} as Record, - }, - ); - setEndpointStatus(optimisticStatus.status); - setEndpointDetails((prev) => ({ - ...prev, - ...optimisticStatus.details, - })); - setLoading(false); - return; - } - - const errorMessage = - err instanceof Error ? err.message : "Unknown error occurred"; - setError(errorMessage); - console.error("[EndpointConfig] Failed to check endpoints:", err); - - // Fallback: assume all endpoints are enabled on error (optimistic) - const optimisticStatus = endpoints.reduce( - (acc, endpoint) => { - acc.status[endpoint] = true; - acc.details[endpoint] = { enabled: true, reason: null }; - return acc; - }, - { - status: {} as Record, - details: {} as Record, - }, - ); - setEndpointStatus(optimisticStatus.status); - setEndpointDetails((prev) => ({ - ...prev, - ...optimisticStatus.details, - })); - } finally { - setLoading(false); - } - }, - [endpoints.join(",")], + useJwtConfigSync( + useCallback(() => { + void queryClient.invalidateQueries({ + queryKey: qk.endpointsAvailability(), + }); + }, [queryClient]), ); - useEffect(() => { - fetchAllEndpointStatuses(); - }, [fetchAllEndpointStatuses]); - - // Re-fetch when auth state changes. Core implementation listens for the - // proprietary `jwt-available` event; the SaaS no-op override means the - // cache simply isn't invalidated on Supabase auth changes (today's behavior). - // If SaaS later needs that, wire it up inside saas/hooks/useJwtConfigSync.ts. - const handleAuthChange = useCallback(() => { - console.debug( - "[useEndpointConfig] Auth changed - clearing cache for refetch", - ); - resetGlobalCache(); - fetchAllEndpointStatuses(true); - }, [fetchAllEndpointStatuses]); - useJwtConfigSync(handleAuthChange); + const key = wanted.join(","); + const projected = useMemo(() => { + const status: Record = {}; + const details: Record = {}; + if (!data && !error) return { status, details }; + for (const endpoint of key ? key.split(",") : []) { + const detail = data?.[endpoint] ?? OPTIMISTIC; + status[endpoint] = detail.enabled; + details[endpoint] = detail; + } + return { status, details }; + }, [data, error, key]); return { - endpointStatus, - endpointDetails, - loading, - error, - refetch: () => fetchAllEndpointStatuses(true), + endpointStatus: projected.status, + endpointDetails: projected.details, + loading: wanted.length > 0 && isPending, + error: message(error), + refetch: reload, }; } diff --git a/frontend/editor/src/core/query/keys.ts b/frontend/editor/src/core/query/keys.ts index 6c6bacd552..a7a68ea256 100644 --- a/frontend/editor/src/core/query/keys.ts +++ b/frontend/editor/src/core/query/keys.ts @@ -1,6 +1,9 @@ /** Editor query keys: ["editor", , ...params]. */ export const qk = { appConfig: () => ["editor", "appConfig"] as const, + endpointsAvailability: () => ["editor", "endpointsAvailability"] as const, + endpointEnabled: (endpoint: string) => + ["editor", "endpointEnabled", endpoint] as const, footerInfo: () => ["editor", "footerInfo"] as const, groupEnabled: (group: string) => ["editor", "groupEnabled", group] as const, users: () => ["editor", "users"] as const, From 0ff4ef629cf294bf474700ffb505f748bbefb4aa Mon Sep 17 00:00:00 2001 From: James Brunton Date: Mon, 10 Aug 2026 17:05:05 +0100 Subject: [PATCH 068/154] New Pipeline UI redesign (#7202) # Description of Changes Supersedes #7144. Redesign the Processor New Pipeline page to use a graph-based interface. Far from perfect at this stage but I'm pretty happy with the interactions on the graph itself. The bar at the top needs some work to make it prettier and more clear what everything is for, but I'd rather get this in and do changes in a follow-up PR because this is big enough on its own and leaves us better than where we were before. image image image image image --- frontend/.storybook/a11y-baseline.dark.json | 29 +- frontend/.storybook/a11y-baseline.json | 38 +- .../public/locales/en-US/translation.toml | 75 +- .../useAddPasswordOperation.test.ts | 9 + .../addPassword/useAddPasswordOperation.ts | 2 +- .../hooks/tools/shared/toolAutomation.test.ts | 15 + .../core/hooks/tools/shared/toolAutomation.ts | 22 +- .../src/core/tests/stubbed/files-page.spec.ts | 6 +- frontend/editor/src/core/ui/CodeBlock.tsx | 2 +- frontend/editor/src/core/ui/NodeCard.css | 87 ++ .../editor/src/core/ui/NodeCard.stories.tsx | 53 + frontend/editor/src/core/ui/NodeCard.tsx | 81 ++ frontend/editor/src/core/ui/index.ts | 1 + frontend/editor/src/portal/api/http.ts | 15 + frontend/editor/src/portal/api/pipelines.ts | 52 +- .../editor/src/portal/components/AppShell.css | 4 + .../pipelines/DestinationPicker.tsx | 65 +- .../pipelines/PipelineDefinitionModal.css | 26 + .../PipelineDefinitionModal.stories.tsx | 48 + .../PipelineDefinitionModal.test.tsx | 37 + .../pipelines/PipelineDefinitionModal.tsx | 42 + .../components/pipelines/PipelineHeader.css | 133 +++ .../pipelines/PipelineHeader.stories.tsx | 118 +++ .../pipelines/PipelineHeader.test.tsx | 200 ++++ .../components/pipelines/PipelineHeader.tsx | 301 ++++++ .../pipelines/PipelineInspector.css | 34 + .../pipelines/PipelineInspector.stories.tsx | 71 ++ .../pipelines/PipelineInspector.test.tsx | 60 ++ .../pipelines/PipelineInspector.tsx | 79 ++ .../pipelines/ToolPicker.stories.tsx | 28 + .../components/pipelines/ToolPicker.tsx | 84 +- .../components/pipelines/graph/GraphEdge.css | 180 ++++ .../components/pipelines/graph/GraphEdge.tsx | 109 ++ .../components/pipelines/graph/GraphNode.css | 140 +++ .../components/pipelines/graph/GraphNode.tsx | 183 ++++ .../pipelines/graph/GraphPlaceholderNode.css | 44 + .../pipelines/graph/GraphPlaceholderNode.tsx | 30 + .../pipelines/graph/PipelineGraph.css | 79 ++ .../pipelines/graph/PipelineGraph.stories.tsx | 207 ++++ .../pipelines/graph/PipelineGraph.test.tsx | 455 ++++++++ .../pipelines/graph/PipelineGraph.tsx | 379 +++++++ .../pipelines/graph/pipelineLayout.test.ts | 147 +++ .../pipelines/graph/pipelineLayout.ts | 184 ++++ .../pipelines/graph/useChainDragDrop.test.ts | 91 ++ .../pipelines/graph/useChainDragDrop.ts | 230 ++++ .../src/portal/mocks/handlers/pipelines.ts | 23 + .../src/portal/views/PipelineBuilder.css | 446 +++----- .../portal/views/PipelineBuilder.stories.tsx | 26 +- .../src/portal/views/PipelineBuilder.test.tsx | 451 +++++++- .../src/portal/views/PipelineBuilder.tsx | 985 ++++++++++-------- .../editor/src/portal/views/Pipelines.css | 17 - 51 files changed, 5310 insertions(+), 913 deletions(-) create mode 100644 frontend/editor/src/core/ui/NodeCard.css create mode 100644 frontend/editor/src/core/ui/NodeCard.stories.tsx create mode 100644 frontend/editor/src/core/ui/NodeCard.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineInspector.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineInspector.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineInspector.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineInspector.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/GraphEdge.css create mode 100644 frontend/editor/src/portal/components/pipelines/graph/GraphEdge.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/GraphNode.css create mode 100644 frontend/editor/src/portal/components/pipelines/graph/GraphNode.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.css create mode 100644 frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.css create mode 100644 frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.test.ts create mode 100644 frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.ts create mode 100644 frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.test.ts create mode 100644 frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.ts diff --git a/frontend/.storybook/a11y-baseline.dark.json b/frontend/.storybook/a11y-baseline.dark.json index fa08401d09..fb461fe6d2 100644 --- a/frontend/.storybook/a11y-baseline.dark.json +++ b/frontend/.storybook/a11y-baseline.dark.json @@ -1828,6 +1828,21 @@ "editor/src/portal/components/infrastructure/SectionHeader.stories.tsx :: Short Sub": [ "color-contrast" ], + "editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx :: Default": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Failed Step": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Multiple Selected": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Nothing Selected": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Settings": [ + "color-contrast" + ], "editor/src/portal/components/pipelines/PipelineStepSettings.stories.tsx :: No Settings": [ "color-contrast" ], @@ -1845,6 +1860,12 @@ "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: Default": [ "color-contrast" ], + "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: Incompatible Preceding Output": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: No Matches": [ + "color-contrast" + ], "editor/src/portal/components/policies/CatalogueSummary.stories.tsx :: Default": [ "color-contrast" ], @@ -2235,9 +2256,13 @@ "color-contrast" ], "editor/src/portal/views/Pipelines.stories.tsx :: Default": [ - "color-contrast" + "color-contrast", + "empty-table-header" + ], + "editor/src/portal/views/Pipelines.stories.tsx :: Empty": [ + "color-contrast", + "empty-table-header" ], - "editor/src/portal/views/Pipelines.stories.tsx :: Empty": ["color-contrast"], "editor/src/portal/views/Policies.stories.tsx :: Default": ["color-contrast"], "editor/src/portal/views/Policies.stories.tsx :: Empty": ["color-contrast"], "editor/src/portal/views/Sources.stories.tsx :: Default": ["color-contrast"], diff --git a/frontend/.storybook/a11y-baseline.json b/frontend/.storybook/a11y-baseline.json index 91db120c19..df46eb9dc4 100644 --- a/frontend/.storybook/a11y-baseline.json +++ b/frontend/.storybook/a11y-baseline.json @@ -1404,8 +1404,7 @@ "color-contrast" ], "editor/src/core/ui/CodeBlock.stories.tsx :: Long Scrolling": [ - "color-contrast", - "scrollable-region-focusable" + "color-contrast" ], "editor/src/core/ui/CodeBlock.stories.tsx :: Playground": ["color-contrast"], "editor/src/core/ui/Collapsible.stories.tsx :: Accordion": [ @@ -1977,6 +1976,30 @@ "editor/src/portal/components/infrastructure/ModelsTab.stories.tsx :: Free": [ "color-contrast" ], + "editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx :: Default": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: Editing": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: Paused": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: Testing": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: With Failed Run": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: With Run Result": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Failed Step": [ + "color-contrast" + ], + "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Settings": [ + "color-contrast" + ], "editor/src/portal/components/pipelines/PipelineStepSettings.stories.tsx :: No Settings": [ "color-contrast" ], @@ -2294,13 +2317,14 @@ "editor/src/portal/views/Integrations.stories.tsx :: No Connections": [ "color-contrast" ], - "editor/src/portal/views/PipelineBuilder.stories.tsx :: Default": [ - "color-contrast" - ], "editor/src/portal/views/Pipelines.stories.tsx :: Default": [ - "color-contrast" + "color-contrast", + "empty-table-header" + ], + "editor/src/portal/views/Pipelines.stories.tsx :: Empty": [ + "color-contrast", + "empty-table-header" ], - "editor/src/portal/views/Pipelines.stories.tsx :: Empty": ["color-contrast"], "editor/src/portal/views/Sources.stories.tsx :: Default": ["color-contrast"], "editor/src/portal/views/Sources.stories.tsx :: Empty": ["color-contrast"], "editor/src/proprietary/auth/ui/AuthScreens.stories.tsx :: Signup": [ diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 0fffc04dbc..4f70f2b813 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7800,7 +7800,6 @@ title = "Pipelines" newPipeline = "New pipeline" [portal.pipelines.builder] -addStep = "Add tool" back = "Back to pipelines" cannotFollow = "Can't take {{produced}}" chooseAccount = "Choose an account" @@ -7813,59 +7812,64 @@ inputs = "Input" inputSource = "Input source" inputTrigger = "Trigger" keepEditing = "Keep editing" +moreActions = "More actions" needsConfiguring = "Needs setting up" +needsDestination = "No destination chosen" +needsSource = "No source chosen" needsUpload = "Needs an uploaded file" -noSources = "No sources connected yet. Create one to use it as an input." noToolMatches = "No tools match your search." -pipelineSettings = "Pipeline settings" searchTools = "Search tools" -selectToolBody = "Add a tool to build your pipeline." -selectToolTitle = "No tools yet" sendToSystem = "Send to another system" stepsIncompatible = "These steps can't run on what their prior step produces: {{tools}}." stepsNeedSetup = "These steps still need setting up before saving: {{tools}}." -toolSettings = "Tool settings" +testRun = "Test with a file" unknownStep = "Unrecognized operation, kept as-is." unsavedBody = "You have unsaved changes. Save them before leaving, or discard them?" unsavedTitle = "Unsaved changes" uploadUnsupported = "Uploaded files aren't supported in pipelines yet, so these steps can't be saved: {{tools}}." usesDefaults = "Runs with default settings" +viewDefinition = "View definition" [portal.pipelines.builder.diagnostic] -fan-in = "Combines every file from the previous step" -fan-out = "Runs once per file from the previous step" -format-mismatch = "Needs {{accepts}}, but the previous step produces {{produced}}" -output-uncertain = "May not run: the previous step's output depends on how it's set up" -source-mismatch = "Needs {{accepts}}, but this pipeline's input is {{produced}}" +fan-in = "Combines every incoming file" +fan-out = "Runs once per incoming file" +format-mismatch = "Sends {{produced}}, needs {{accepts}}" +output-uncertain = "May not run: output depends on setup" +source-mismatch = "Input is {{produced}}, needs {{accepts}}" undeclared-operation = "Can't check what this step accepts" [portal.pipelines.composer] -addTool = "Add tool" +addTool = "Add a tool" cancel = "Cancel" -chainEmpty = "Add a tool to start building your pipeline." create = "Create pipeline" editingUnsupported = "Displaying these tool params for editing is not supported yet." -moveDown = "Move down" -moveUp = "Move up" +editSource = "Edit source" name = "Name" namePlaceholder = "e.g. Redaction sweep" noToolSettings = "This tool has no configurable settings." -operations_one = "Operation ({{count}})" -operations_other = "Operations ({{count}})" output = "Destination" -removeStep = "Remove operation" save = "Save changes" scheduleEvery = "Run every" -sources = "Sources" -sourcesLoading = "Loading sources..." trigger = "Trigger" triggerManual = "Manual only" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "days" hours = "hours" minutes = "minutes" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Delete \"{{name}}\"? This can't be undone." cancel = "Cancel" @@ -7883,6 +7887,37 @@ connectSource = "Connect a source" description = "Create your first pipeline: pick the sources it runs over, chain the operations, and choose where output goes." title = "No pipelines yet" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Active" paused = "Paused" diff --git a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.test.ts b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.test.ts index 77958629b0..28e8e1baa6 100644 --- a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.test.ts +++ b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.test.ts @@ -147,6 +147,15 @@ describe("useAddPasswordOperation", () => { }); describe("addPassword mappers", () => { + test("falls back to the default key length when the stored step omits it", () => { + // A pipeline step saved without keyLength must not deserialize to + // undefined: the settings UI calls keyLength.toString() on it. + const restored = addPasswordFromApiParams({ + password: "user-pw", + } as never); + expect(restored.keyLength).toBe(128); + }); + test("round-trips backend params, including the flattened permissions", () => { // Baseline differs from the configured values so the round trip fails if // fromApiParams drops a field instead of reconstructing it. diff --git a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts index d68154c9b1..4e843c8da7 100644 --- a/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts +++ b/frontend/editor/src/core/hooks/tools/addPassword/useAddPasswordOperation.ts @@ -48,7 +48,7 @@ export const addPasswordFromApiParams = ( ): Partial => ({ password: apiParams.password ?? defaultParameters.password, ownerPassword: apiParams.ownerPassword ?? defaultParameters.ownerPassword, - keyLength: apiParams.keyLength, + keyLength: apiParams.keyLength ?? defaultParameters.keyLength, permissions: { preventAssembly: apiParams.preventAssembly ?? permissionsDefaults.preventAssembly, diff --git a/frontend/editor/src/core/hooks/tools/shared/toolAutomation.test.ts b/frontend/editor/src/core/hooks/tools/shared/toolAutomation.test.ts index 467e3ea5f7..e8d34ef9ad 100644 --- a/frontend/editor/src/core/hooks/tools/shared/toolAutomation.test.ts +++ b/frontend/editor/src/core/hooks/tools/shared/toolAutomation.test.ts @@ -153,6 +153,21 @@ describe("serialize/deserialize round-trip", () => { }); }); + test("a stored step missing fields falls back to defaults, not undefined", () => { + // Mappers echo absent stored fields as explicit undefined; settings UIs + // then crash on things like keyLength.toString(). Defaults must win. + const back = deserializeToolStep( + { operation: "/api/v1/misc/compress-pdf", parameters: {} }, + registry, + ); + expect(back.params.compressionLevel).toBe( + compressDefaults.compressionLevel, + ); + expect( + Object.values(back.params).every((value) => value !== undefined), + ).toBe(true); + }); + test("an unknown endpoint is preserved as an unmapped step", () => { const step = deserializeToolStep( { operation: "/api/v1/unknown/thing", parameters: { keep: true } }, diff --git a/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts b/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts index aab9e1ab10..b32f783b06 100644 --- a/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts +++ b/frontend/editor/src/core/hooks/tools/shared/toolAutomation.ts @@ -291,12 +291,22 @@ export function deserializeToolStep( if (!match) return unmappedStep(step); const [toolId, entry] = match; const config = entry.operationConfig; - const params: ErasedToolParams = config?.fromApiParams - ? { - ...(config.defaultParameters ?? {}), - ...config.fromApiParams(step.parameters as never), - } - : { ...(config?.defaultParameters ?? {}) }; + // Mappers echo missing stored fields as explicit `undefined`, which would + // clobber the default underneath; strip those so defaults always win. + const mapped = config?.fromApiParams + ? Object.fromEntries( + Object.entries( + config.fromApiParams(step.parameters as never) as Record< + string, + unknown + >, + ).filter(([, value]) => value !== undefined), + ) + : {}; + const params: ErasedToolParams = { + ...(config?.defaultParameters ?? {}), + ...mapped, + } as ErasedToolParams; // Validate against the generated endpoint set instead of casting the matched string. const operation = resolveEndpoint(config, params) ?? diff --git a/frontend/editor/src/core/tests/stubbed/files-page.spec.ts b/frontend/editor/src/core/tests/stubbed/files-page.spec.ts index 56687b2217..4c6e714ec0 100644 --- a/frontend/editor/src/core/tests/stubbed/files-page.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/files-page.spec.ts @@ -173,7 +173,7 @@ test.describe("Files page", () => { await gotoFilesPage(page); const cards = page.locator(".files-page-card:not(.is-folder)"); await cards.nth(0).click(); - await cards.nth(1).click({ modifiers: ["Control"] }); + await cards.nth(1).click({ modifiers: ["ControlOrMeta"] }); await expect(page.locator(".files-page-card.is-selected")).toHaveCount(2); // In multi-select (2+), plain-click ADDS instead of replacing. @@ -198,7 +198,7 @@ test.describe("Files page", () => { await expect(page.locator(".files-page-card-selector")).toHaveCount(0); // 2+ selected: checkboxes appear on every file card. - await cards.nth(1).click({ modifiers: ["Control"] }); + await cards.nth(1).click({ modifiers: ["ControlOrMeta"] }); await expect( page.locator(".files-page-card-selector").first(), ).toBeVisible(); @@ -488,7 +488,7 @@ test.describe("Files page", () => { const cards = page.locator(".files-page-card:not(.is-folder)"); await cards.nth(0).click(); // Drawer stays closed so the second click reaches the card. - await cards.nth(1).click({ modifiers: ["Control"] }); + await cards.nth(1).click({ modifiers: ["ControlOrMeta"] }); await expect(page.locator(".files-page-card.is-selected")).toHaveCount(2); }); }); diff --git a/frontend/editor/src/core/ui/CodeBlock.tsx b/frontend/editor/src/core/ui/CodeBlock.tsx index cff078057d..2d66bd2353 100644 --- a/frontend/editor/src/core/ui/CodeBlock.tsx +++ b/frontend/editor/src/core/ui/CodeBlock.tsx @@ -70,7 +70,7 @@ export function CodeBlock({ )}
    -
    +      
             {code}
           
    diff --git a/frontend/editor/src/core/ui/NodeCard.css b/frontend/editor/src/core/ui/NodeCard.css new file mode 100644 index 0000000000..7982e2581a --- /dev/null +++ b/frontend/editor/src/core/ui/NodeCard.css @@ -0,0 +1,87 @@ +/** + * NodeCard — a selectable labelled tile (icon badge + title + sub-line) on a raised surface. + * Shared surface, selection ring and content layout; feature-specific state is layered by callers. + */ + +.sui-node-card { + position: relative; + display: flex; + align-items: stretch; + box-sizing: border-box; + background: var(--c-surface); + border: 1px solid var(--c-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + transition: + border-color var(--motion-fast), + box-shadow var(--motion-fast), + opacity var(--motion-fast); +} + +/* The whole card selects. Re-assert the tile look over the shared Button base (which otherwise + imposes a fixed height, its own padding and an accent text colour). */ +.sui-node-card__select.sui-btn { + flex: 1; + min-width: 0; + height: auto; + min-height: 0; + border: none; + background: none; + padding: 0.625rem 0.75rem; + text-align: left; + font-weight: 400; + color: var(--c-text); + border-radius: inherit; +} + +/* Mantine wraps a button's children in its label element, so the glyph and text are laid out + there - a gap on the button root would only space the wrapper, not what is inside it. */ +.sui-node-card__select.sui-btn .mantine-Button-label { + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.625rem; + min-width: 0; + overflow: visible; +} + +.sui-node-card__text { + display: flex; + flex-direction: column; + min-width: 0; + gap: 0.0625rem; +} + +.sui-node-card__title { + font-size: 0.875rem; + font-weight: 500; + color: var(--c-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sui-node-card__detail { + font-size: 0.6875rem; + /* --c-text-subtle does not clear 4.5:1 at this size in either theme (axe: 4.39 light, 3.66 + dark); --c-text-muted is the next rung up and does. */ + color: var(--c-text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Selected: the primary ring. Wins over hover and the warning tone. */ +.sui-node-card.is-selected { + border-color: var(--c-primary); + box-shadow: 0 0 0 1px var(--c-primary); +} + +.sui-node-card:hover:not(.is-selected) { + border-color: var(--c-border-strong); +} + +.sui-node-card--warning:not(.is-selected) { + border-color: var(--c-warning); +} diff --git a/frontend/editor/src/core/ui/NodeCard.stories.tsx b/frontend/editor/src/core/ui/NodeCard.stories.tsx new file mode 100644 index 0000000000..acfe28f71d --- /dev/null +++ b/frontend/editor/src/core/ui/NodeCard.stories.tsx @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import TuneRoundedIcon from "@mui/icons-material/TuneRounded"; +import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; +import { NodeCard } from "@app/ui/NodeCard"; +import { ActionIcon } from "@app/ui/ActionIcon"; + +const meta = { + title: "UI/NodeCard", + component: NodeCard, + parameters: { layout: "padded" }, + args: { + icon: , + title: "Compress", + detail: "level 7", + onSelect: () => {}, + }, + decorators: [ + (Story) => ( +
    + +
    + ), + ], +} satisfies Meta; +export default meta; +type Story = StoryObj; + +/** The default tile: icon badge, title, one-line sub-detail, selectable. */ +export const Default: Story = {}; + +/** Selected — the primary ring the inspector points at. */ +export const Selected: Story = { args: { selected: true } }; + +/** Warning tone — an amber border, for a tile that needs attention. */ +export const Warning: Story = { + args: { tone: "warning", detail: "Needs setting up" }, +}; + +/** A trailing control (here a remove button) sits beside the select target, not nested in it. */ +export const WithTrailing: Story = { + args: { + trailing: ( + + + + ), + }, +}; diff --git a/frontend/editor/src/core/ui/NodeCard.tsx b/frontend/editor/src/core/ui/NodeCard.tsx new file mode 100644 index 0000000000..451cc8823b --- /dev/null +++ b/frontend/editor/src/core/ui/NodeCard.tsx @@ -0,0 +1,81 @@ +import type { HTMLAttributes, MouseEvent, ReactNode, Ref } from "react"; +import { Button } from "@app/ui/Button"; +import { IconBadge, type IconBadgeAccent } from "@app/ui/IconBadge"; +import "@app/ui/NodeCard.css"; + +/** Border tone. `selected` (a separate prop) overrides this with the primary ring. */ +export type NodeCardTone = "default" | "warning"; + +export interface NodeCardProps extends Omit< + HTMLAttributes, + "title" | "onSelect" +> { + /** Glyph shown in a tone-tinted badge at the leading edge. */ + icon: ReactNode; + iconAccent?: IconBadgeAccent; + title: ReactNode; + /** One-line summary under the title. Any node - a plain string, or a richer line. */ + detail?: ReactNode; + tone?: NodeCardTone; + selected?: boolean; + /** + * When given, the whole card is a single select button (aria-pressed tracks `selected`). Trailing + * controls stay siblings of that button, never nested inside it, so the card holds no invalid + * nested interactive elements. + */ + onSelect?: (event: MouseEvent) => void; + /** Controls rendered over the card's trailing edge (a remove button, a status glyph, ...). */ + trailing?: ReactNode; + ref?: Ref; +} + +/** + * A labelled tile: an icon badge, a title, and an optional sub-line, on a raised card surface that + * can be selected. The recurring "node" motif - a step in a graph, an item in a board - lifted into + * a primitive so its surface, selection ring and content layout are shared rather than re-styled per + * feature. Callers layer their own state (drag, run status, ...) via `className` and `trailing`. + */ +export function NodeCard({ + icon, + iconAccent, + title, + detail, + tone = "default", + selected = false, + onSelect, + trailing, + className, + ref, + ...rest +}: NodeCardProps) { + return ( +
    + + {trailing} +
    + ); +} diff --git a/frontend/editor/src/core/ui/index.ts b/frontend/editor/src/core/ui/index.ts index 45212d8038..b3e2ca6ac0 100644 --- a/frontend/editor/src/core/ui/index.ts +++ b/frontend/editor/src/core/ui/index.ts @@ -8,6 +8,7 @@ export * from "@app/ui/MethodBadge"; export * from "@app/ui/ToggleSwitch"; export * from "@app/ui/ProgressBar"; export * from "@app/ui/MetricCard"; +export * from "@app/ui/NodeCard"; export * from "@app/ui/NavItem"; export * from "@app/ui/NavSurface"; export * from "@app/ui/PanelHeader"; diff --git a/frontend/editor/src/portal/api/http.ts b/frontend/editor/src/portal/api/http.ts index 5c8afcd2cb..2ec00e9818 100644 --- a/frontend/editor/src/portal/api/http.ts +++ b/frontend/editor/src/portal/api/http.ts @@ -212,6 +212,20 @@ async function localForm( return unwrap(res); } +/** POST a multipart/form-data body (file uploads), via the localBackend seam. The Content-Type is + * deliberately left unset so the browser writes it with the multipart boundary. */ +async function localMultipart(path: string, body: FormData): Promise { + const res = await fetch(`${localBaseUrl()}${path}`, { + method: "POST", + headers: { Accept: "application/json", ...(await localAuthHeader()) }, + body, + }); + if (res.status === 401) { + onLocalUnauthorized(); + } + return unwrap(res); +} + // ──────────────────────────────────────────────────────────────────────────── // saas — hosted SaaS Java, admin's Supabase JWT // ──────────────────────────────────────────────────────────────────────────── @@ -302,6 +316,7 @@ export const apiClient = { local: { json: localJson, form: localForm, + multipart: localMultipart, blob: localBlob, }, /** Hosted SaaS Java. Admin's Supabase JWT auto-attached. */ diff --git a/frontend/editor/src/portal/api/pipelines.ts b/frontend/editor/src/portal/api/pipelines.ts index d6088d06ff..50bdc173c4 100644 --- a/frontend/editor/src/portal/api/pipelines.ts +++ b/frontend/editor/src/portal/api/pipelines.ts @@ -1,4 +1,5 @@ import { apiClient } from "@portal/api/http"; +import { type ToolApiStep } from "@app/hooks/tools/shared/toolAutomation"; /** * Pipelines service layer: the backend contract. @@ -122,7 +123,13 @@ export type PolicyRunStatus = | "FAILED" | "CANCELLED"; -/** A run's current state. Mirrors the backend `PolicyRunView` (outputs elided). */ +/** One file a run produced, downloadable via /api/v1/general/files/{fileId}. */ +export interface RunOutputFile { + fileId: string; + fileName: string | null; +} + +/** A run's current state. Mirrors the backend `PolicyRunView`. */ export interface PolicyRunView { runId: string; policyId: string | null; @@ -132,6 +139,11 @@ export interface PolicyRunView { /** Human-readable failure message; set when status is FAILED. */ error: string | null; errorCode: string | null; + /** + * Files the run produced, present once it completes. Whole-run, not per step: the backend keeps + * one flat list, so nothing here can be attributed to an individual step. + */ + outputs?: RunOutputFile[] | null; createdAt: number; } @@ -198,6 +210,44 @@ export async function triggerPipeline(id: string): Promise { ); } +/** What an ad-hoc test run posts: the steps as they stand, with no source and no trigger. */ +export interface TestRunDefinition { + name: string; + steps: ToolApiStep[]; + output: OutputSpec; +} + +/** + * POST /api/v1/policies/run: run a definition against one uploaded file now. The builder's test + * path - callers force an inline output so nothing reaches the pipeline's real destination, and + * the pipeline need not be saved first. + */ +export async function runPipelineTest( + definition: TestRunDefinition, + file: File, +): Promise<{ runId: string }> { + const form = new FormData(); + form.append( + "json", + new Blob([JSON.stringify(definition)], { type: "application/json" }), + ); + form.append("fileInput", file); + // The POST returns the identifier as `jobId`, but it is the same run id every other endpoint + // (fetchRun, fetchRunOutput) calls `runId`; normalise to that here so callers see one name. + const res = await apiClient.local.multipart<{ jobId: string }>( + "/api/v1/policies/run", + form, + ); + return { runId: res.jobId }; +} + +/** GET /api/v1/general/files/{id}: download one of a run's outputs. */ +export async function fetchRunOutput(fileId: string): Promise { + return apiClient.local.blob( + `/api/v1/general/files/${encodeURIComponent(fileId)}`, + ); +} + /** GET /api/v1/policies/run/{runId}: current status, error, and step cursor of a run. */ export async function fetchRun(runId: string): Promise { return apiClient.local.json( diff --git a/frontend/editor/src/portal/components/AppShell.css b/frontend/editor/src/portal/components/AppShell.css index 152c45f7c5..1657b56bdf 100644 --- a/frontend/editor/src/portal/components/AppShell.css +++ b/frontend/editor/src/portal/components/AppShell.css @@ -26,6 +26,10 @@ flex: 1 1 auto; min-height: 0; /* scroll instead of growing past the viewport */ overflow-y: auto; + /* Hold the scrollbar's width whether or not it is showing. Without this, a page that grows past + the viewport (an editor panel filling in, say) makes the bar appear and shunts everything + sideways as it does. */ + scrollbar-gutter: stable; animation: fadeInUp var(--motion-enter) both; } diff --git a/frontend/editor/src/portal/components/pipelines/DestinationPicker.tsx b/frontend/editor/src/portal/components/pipelines/DestinationPicker.tsx index de1187c3da..864668b764 100644 --- a/frontend/editor/src/portal/components/pipelines/DestinationPicker.tsx +++ b/frontend/editor/src/portal/components/pipelines/DestinationPicker.tsx @@ -1,15 +1,16 @@ import { useTranslation } from "react-i18next"; import AddRoundedIcon from "@mui/icons-material/AddRounded"; -import { Button, Select } from "@app/ui"; +import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; +import { ActionIcon, Button, FormField, Select } from "@app/ui"; /** * Picks the saved source a pipeline delivers its output to. A destination is just a * source used as a write target. The value stays a list ({@code outputIds}) because * the model supports several, but the product caps a pipeline at one destination * today, so this renders a single dropdown over the same locations the builder - * loaded (filtered to writable types by the caller). Creating a new one is delegated - * to {@code onCreateNew} (the builder navigates to the source builder, prompting - * about unsaved edits first). + * loaded (filtered to writable types by the caller). Creating and editing one are + * delegated to {@code onCreateNew} / {@code onEdit}, which open the source modal + * over the builder - mirroring the input row. */ interface DestinationOption { id: string; @@ -20,8 +21,10 @@ interface DestinationPickerProps { sources: DestinationOption[]; value: string[]; onChange: (outputIds: string[]) => void; - /** Leave the builder to create a new source location (navigate-away, like inputs). */ + /** Create a new source location to write to (opens the source modal). */ onCreateNew: () => void; + /** Edit the chosen destination's own settings (opens the source modal on it). */ + onEdit: (sourceId: string) => void; } export function DestinationPicker({ @@ -29,25 +32,45 @@ export function DestinationPicker({ value, onChange, onCreateNew, + onEdit, }: DestinationPickerProps) { const { t } = useTranslation(); + const chosen = value[0] ?? ""; + const hasSources = sources.length > 0; + // Mirrors the input row: the dropdown-plus-edit sits in a field, and "Connect source" lives on its + // own line below rather than inline. With nowhere to write to yet, only the connect button shows. return ( -
    -
    - onChange(id ? [id] : [])} + options={sources.map((source) => ({ + value: source.id, + label: source.name, + }))} + /> +
    + onEdit(chosen)} + > + + +
    + + )} - + ); } diff --git a/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.css b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.css new file mode 100644 index 0000000000..7f1984b4b1 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.css @@ -0,0 +1,26 @@ +/* The code is this modal's entire content, so it *is* the body rather than a window sitting inside + one. Framed, it drew a second box inside the panel's box - and the panel's own header already + says what the code is, which the code window's chrome was repeating. */ +.portal-definition__modal .sui-modal__body { + padding: 0; +} + +.portal-definition__code { + border: none; + border-radius: 0; + box-shadow: none; +} + +/* Traffic-light dots imitate a window frame; this code already sits in a real one. */ +.portal-definition__code .sui-code__dots { + display: none; +} + +/* Line the toolbar and the code up with the modal header's text. */ +.portal-definition__code .sui-code__chrome { + padding: 0.5rem 1.125rem; +} + +.portal-definition__code .sui-code__pre { + padding: 0.875rem 1.125rem; +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx new file mode 100644 index 0000000000..4800689d7d --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Button } from "@app/ui"; +import { PipelineDefinitionModal } from "@portal/components/pipelines/PipelineDefinitionModal"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineDefinitionModal", + component: PipelineDefinitionModal, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +const JSON_BODY = JSON.stringify( + { + name: "Claims redaction", + enabled: true, + inputs: [{ sourceId: "src-in", trigger: { type: "schedule" } }], + steps: [ + { operation: "/api/v1/misc/ocr-pdf", parameters: { language: "eng" } }, + { operation: "/api/v1/security/redact", parameters: { terms: 2 } }, + ], + outputIds: ["src-out"], + }, + null, + 2, +); + +/** Starts closed so the trigger can be exercised; click through to the tabs. */ +function Playground({ initialOpen = false }: { initialOpen?: boolean }) { + const [open, setOpen] = useState(initialOpen); + return ( + <> + + setOpen(false)} + json={JSON_BODY} + /> + + ); +} + +/** The definition as it opens from the header: JSON first, cURL a tab away. */ +export const Default: Story = { render: () => }; + +/** The trigger it opens from, so the closed state can be exercised too. */ +export const FromTrigger: Story = { render: () => }; diff --git a/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.test.tsx new file mode 100644 index 0000000000..32e55e3826 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.test.tsx @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from "vitest"; +import { render as baseRender, screen } from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import { PipelineDefinitionModal } from "@portal/components/pipelines/PipelineDefinitionModal"; + +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +const JSON_BODY = '{\n "name": "Claims"\n}'; + +describe("PipelineDefinitionModal", () => { + it("renders nothing while closed", () => { + render( + , + ); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("opens on the JSON tab", () => { + render(); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText(/"name": "Claims"/)).toBeInTheDocument(); + }); + + it("shows the definition alone - no tab strip to choose between", () => { + render(); + expect(screen.queryByRole("tablist")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.tsx b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.tsx new file mode 100644 index 0000000000..adcd19f5f5 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineDefinitionModal.tsx @@ -0,0 +1,42 @@ +import { useTranslation } from "react-i18next"; +import { CodeBlock, Modal } from "@app/ui"; +import "@portal/components/pipelines/PipelineDefinitionModal.css"; + +export interface PipelineDefinitionModalProps { + open: boolean; + onClose: () => void; + /** The pipeline as it would be saved, pretty-printed. Re-read while the modal is open. */ + json: string; +} + +/** + * The pipeline's definition as it would be saved. + * + * Pipeline-scoped, so it opens from the header rather than the node inspector, and a modal rather + * than a panel because a definition grows with the chain and needs the width. + */ +export function PipelineDefinitionModal({ + open, + onClose, + json, +}: PipelineDefinitionModalProps) { + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.css b/frontend/editor/src/portal/components/pipelines/PipelineHeader.css new file mode 100644 index 0000000000..f559e42795 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineHeader.css @@ -0,0 +1,133 @@ +/** + * The builder's opening section: identity above the rule, actions below it. + */ + +.portal-pipeline-header { + display: flex; + flex-direction: column; + gap: 0.875rem; + padding: 1.125rem; + background: var(--c-surface); + border: 1px solid var(--c-border-subtle); + border-radius: var(--radius-lg); +} + +/* Leaving the page and saving it are the same kind of decision, so they share a row - and the back + link is short, so the save pair always has room beside it. */ +.portal-pipeline-header__top { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +/* The back link is the shared Button restyled to a plain link, so re-assert that over the + design-system base (which imposes a fixed height, its own padding and an accent colour). */ +.portal-pipeline-header__back.sui-btn { + height: auto; + min-height: 0; + padding: 0; + font-size: 0.8125rem; + font-weight: 400; + color: var(--c-text-muted); +} + +.portal-pipeline-header__back.sui-btn:hover { + background: none; + color: var(--c-text); +} + +.portal-pipeline-header__identity { + display: flex; + align-items: center; + gap: 1.25rem; + flex-wrap: wrap; +} + +/* The shared Checkbox aligns its box to the top of the first text line, with a nudge tuned for its + own font size - that is for the label-plus-description case. This one is a single line, so centre + the box on it and leave the component's sizing alone (overriding the font size shifts the line + box and leaves the tick floating high). */ +.portal-pipeline-header__enabled.sui-check { + flex: none; + align-items: center; +} + +.portal-pipeline-header__enabled.sui-check .sui-check__box { + margin-top: 0; +} + +/* The name is the page's title, so it takes the room and reads at title size. */ +.portal-pipeline-header__name { + flex: 1 1 16rem; + min-width: 12rem; +} + +.portal-pipeline-header__name input { + font-size: 1rem; + font-weight: 500; +} + +/* Never let the labels squash: buttons hold their width and the row wraps instead of clipping. */ +.portal-pipeline-header__save { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + flex: none; +} + +.portal-pipeline-header__save .sui-btn { + flex: none; + white-space: nowrap; +} + +/* Operational actions: what you can do to this pipeline, kept off the identity row. */ +.portal-pipeline-header__actions { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + padding-top: 0.875rem; + border-top: 1px solid var(--c-border-subtle); +} + +/* Destructive, so it sits away from the rest rather than next in line. */ +.portal-pipeline-header__delete.sui-btn { + margin-left: auto; +} + +/* The last test run's outcome, beside the button that started it. Whole-pipeline, because the + backend reports one flat file list plus the step it stopped at - nothing per node to attach. */ +.portal-pipeline-header__result { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + padding-top: 0.875rem; + border-top: 1px solid var(--c-border-subtle); +} + +.portal-pipeline-header__result-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: var(--c-text); +} + +.portal-pipeline-header__result-icon.is-ok { + color: var(--c-success); +} + +.portal-pipeline-header__result-icon.is-bad { + color: var(--c-danger); +} + +/* Why the run failed, shown inline in the strip. Neutral text (the icon already carries the tone); + it may wrap to keep a long backend message readable rather than clipping it. */ +.portal-pipeline-header__result-error { + font-size: 0.8125rem; + color: var(--c-text-muted); + min-width: 0; +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx new file mode 100644 index 0000000000..41c73a5ade --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx @@ -0,0 +1,118 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + PipelineHeader, + type RunResultSummary, +} from "@portal/components/pipelines/PipelineHeader"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineHeader", + component: PipelineHeader, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +const noop = () => {}; + +/** The name and the enabled switch are live, so the section can be seen in both states. */ +function Playground({ + initialName, + isEdit, + initialEnabled = true, + runResult = null, + ...rest +}: { + initialName: string; + isEdit: boolean; + initialEnabled?: boolean; + runResult?: RunResultSummary | null; + saving?: boolean; + testing?: boolean; + running?: boolean; + canSave?: boolean; + stepCount?: number; +}) { + const [name, setName] = useState(initialName); + const [enabled, setEnabled] = useState(initialEnabled); + return ( + + ); +} + +/** An existing pipeline: everything is available. */ +export const Editing: Story = { + render: () => , +}; + +/** + * A pipeline that has never been saved. It can still be tested against a file, but there is + * nothing yet to run on a schedule, clear history for, or delete. + */ +export const New: Story = { + render: () => , +}; + +/** Paused: the pipeline exists but its trigger will not fire. */ +export const Paused: Story = { + render: () => ( + + ), +}; + +/** Mid test-run: the picker shows its own progress while the graph shows the steps. */ +export const Testing: Story = { + render: () => , +}; + +/** After a test run: the outcome and its files sit beside the button that started them. */ +export const WithRunResult: Story = { + render: () => ( + + ), +}; + +/** A failed run: the summary is here, the failing step's own message is on its node. */ +export const WithFailedRun: Story = { + render: () => ( + + ), +}; diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx new file mode 100644 index 0000000000..2c5552be07 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx @@ -0,0 +1,200 @@ +import { describe, expect, it, vi } from "vitest"; +import { + fireEvent, + render as baseRender, + screen, +} from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import { + PipelineHeader, + type PipelineHeaderProps, +} from "@portal/components/pipelines/PipelineHeader"; + +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +function renderHeader(overrides: Partial = {}) { + const handlers = { + onNameChange: vi.fn(), + onEnabledChange: vi.fn(), + onSave: vi.fn(), + onCancel: vi.fn(), + onBack: vi.fn(), + onTest: vi.fn(), + onRun: vi.fn(), + onClearHistory: vi.fn(), + onDelete: vi.fn(), + onViewDefinition: vi.fn(), + onDownloadOutput: vi.fn(), + }; + render( + , + ); + return handlers; +} + +describe("PipelineHeader", () => { + it("edits the pipeline's name and enabled state", () => { + const handlers = renderHeader(); + fireEvent.change( + screen.getByRole("textbox", { name: "portal.pipelines.composer.name" }), + { target: { value: "Renamed" } }, + ); + expect(handlers.onNameChange).toHaveBeenCalledWith("Renamed"); + + fireEvent.click(screen.getByRole("checkbox")); + expect(handlers.onEnabledChange).toHaveBeenCalledWith(false); + }); + + it("offers run, clear history and delete only once the pipeline exists", () => { + renderHeader({ isEdit: false }); + expect( + screen.queryByText("portal.pipelines.detail.run"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.detail.delete"), + ).not.toBeInTheDocument(); + // A test run needs no saved record, so it stays: it is how you check the steps as you build. + expect( + screen.getByText("portal.pipelines.builder.testRun"), + ).toBeInTheDocument(); + }); + + it("labels the save action for what it will do", () => { + renderHeader({ isEdit: false }); + expect( + screen.getByText("portal.pipelines.composer.create"), + ).toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.composer.save"), + ).not.toBeInTheDocument(); + }); + + it("blocks saving until the pipeline is valid", () => { + renderHeader({ canSave: false }); + expect( + screen.getByText("portal.pipelines.composer.save").closest("button"), + ).toBeDisabled(); + }); + + it("hands the chosen file to the test run", () => { + const handlers = renderHeader(); + const file = new File(["x"], "claim.pdf", { type: "application/pdf" }); + const input = + document.querySelector('input[type="file"]'); + expect(input).not.toBeNull(); + fireEvent.change(input as HTMLInputElement, { target: { files: [file] } }); + expect(handlers.onTest).toHaveBeenCalledWith(file); + }); + + it("will not offer a test run on a chain with no steps", () => { + renderHeader({ stepCount: 0 }); + expect( + screen.getByText("portal.pipelines.builder.testRun").closest("button"), + ).toBeDisabled(); + }); + + it("shows why a test run failed, not only that it did", () => { + renderHeader({ + runResult: { + status: "failed", + completedSteps: 1, + stepCount: 3, + error: "OCR failed: unreadable page", + }, + }); + expect(screen.getByText("OCR failed: unreadable page")).toBeInTheDocument(); + }); + + it("runs and deletes from the row, clears history from the tray", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByText("portal.pipelines.detail.run")); + expect(handlers.onRun).toHaveBeenCalled(); + fireEvent.click(screen.getByText("portal.pipelines.detail.delete")); + expect(handlers.onDelete).toHaveBeenCalled(); + + fireEvent.click( + screen.getByLabelText("portal.pipelines.builder.moreActions"), + ); + fireEvent.click(screen.getByText("portal.pipelines.detail.clearHistory")); + expect(handlers.onClearHistory).toHaveBeenCalled(); + }); + + it("leaves the page through cancel and back", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByText("portal.pipelines.composer.cancel")); + expect(handlers.onCancel).toHaveBeenCalled(); + fireEvent.click(screen.getByText("portal.pipelines.builder.back")); + expect(handlers.onBack).toHaveBeenCalled(); + }); + + it("keeps the occasional actions out of the row, behind a tray", () => { + renderHeader(); + // Running and testing earn a button each; reading the definition and wiping history do not. + expect( + screen.queryByText("portal.pipelines.builder.viewDefinition"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.detail.clearHistory"), + ).not.toBeInTheDocument(); + expect( + screen.getByLabelText("portal.pipelines.builder.moreActions"), + ).toBeInTheDocument(); + }); + + it("opens the definition from the tray", () => { + const handlers = renderHeader(); + fireEvent.click( + screen.getByLabelText("portal.pipelines.builder.moreActions"), + ); + fireEvent.click( + screen.getByText("portal.pipelines.builder.viewDefinition"), + ); + expect(handlers.onViewDefinition).toHaveBeenCalled(); + }); + + it("shows no run strip until a test has been run", () => { + renderHeader(); + expect( + screen.queryByText(/portal.pipelines.inspector.status/), + ).not.toBeInTheDocument(); + }); + + it("reports a finished run and downloads the file clicked", () => { + const handlers = renderHeader({ + runResult: { + status: "completed", + completedSteps: 2, + stepCount: 2, + outputs: [ + { fileId: "f1", fileName: "claim.pdf" }, + { fileId: "f2", fileName: null }, + ], + }, + }); + fireEvent.click(screen.getByText("claim.pdf")); + expect(handlers.onDownloadOutput).toHaveBeenCalledWith({ + fileId: "f1", + fileName: "claim.pdf", + }); + // A file the backend did not name still has to be reachable. + expect(screen.getByText("f2")).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx b/frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx new file mode 100644 index 0000000000..25bfbd044f --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx @@ -0,0 +1,301 @@ +import { useTranslation } from "react-i18next"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import HistoryRoundedIcon from "@mui/icons-material/HistoryRounded"; +import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded"; +import ScienceOutlinedIcon from "@mui/icons-material/ScienceOutlined"; +import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; +import MoreHorizRoundedIcon from "@mui/icons-material/MoreHorizRounded"; +import CheckCircleOutlineRoundedIcon from "@mui/icons-material/CheckCircleOutlineRounded"; +import DownloadRoundedIcon from "@mui/icons-material/DownloadRounded"; +import ErrorOutlineRoundedIcon from "@mui/icons-material/ErrorOutlineRounded"; +import { + ActionIcon, + Button, + Checkbox, + Dropdown, + FilePicker, + Input, + Spinner, +} from "@app/ui"; +import "@portal/components/pipelines/PipelineHeader.css"; + +/** One file a test run produced, downloadable from the result strip. */ +export interface RunOutputFile { + fileId: string; + fileName: string | null; +} + +/** + * A test run's outcome. Whole-pipeline, not per-node: the backend reports one flat list of files + * plus the step it stopped at, so there is no per-node output to attach to a node. + */ +export interface RunResultSummary { + status: "running" | "completed" | "failed"; + completedSteps: number; + stepCount: number; + error?: string | null; + outputs?: RunOutputFile[]; +} + +export interface PipelineHeaderProps { + name: string; + onNameChange: (name: string) => void; + enabled: boolean; + onEnabledChange: (enabled: boolean) => void; + /** False for a pipeline that has never been saved: it cannot yet be run, cleared or deleted. */ + isEdit: boolean; + /** How many steps the chain has, so an empty pipeline cannot offer a test that does nothing. */ + stepCount: number; + + canSave: boolean; + saving: boolean; + onSave: () => void; + onCancel: () => void; + onBack: () => void; + + /** Run the steps as they stand against one uploaded file, without saving or delivering. */ + onTest: (file: File) => void; + testing: boolean; + /** Run the saved pipeline against its real input, delivering to its real destination. */ + onRun: () => void; + running: boolean; + onClearHistory: () => void; + clearingHistory: boolean; + onDelete: () => void; + + /** Opens the definition (JSON + cURL), which is pipeline-scoped like the rest of this row. */ + onViewDefinition: () => void; + /** The last test run in this session, or null if there has not been one. */ + runResult: RunResultSummary | null; + onDownloadOutput: (output: RunOutputFile) => void; +} + +/** + * The pipeline's identity and its whole-pipeline actions, at the top of the builder. + * + * Split in two so neither half gets lost in a single crowded row: what the pipeline *is* (name, + * whether it is live) sits with the actions that leave the page, and what you can *do to it* sits + * below the rule. A test run is part of building, so it lives here rather than off in a corner - + * its progress shows on the graph's nodes and its results in the inspector. + */ +export function PipelineHeader({ + name, + onNameChange, + enabled, + onEnabledChange, + isEdit, + stepCount, + canSave, + saving, + onSave, + onCancel, + onBack, + onTest, + testing, + onRun, + running, + onClearHistory, + clearingHistory, + onDelete, + onViewDefinition, + runResult, + onDownloadOutput, +}: PipelineHeaderProps) { + const { t } = useTranslation(); + + return ( +
    +
    + +
    + + +
    +
    + +
    + onNameChange(e.target.value)} + /> + {/* A checkbox, not a switch: this is a form value that takes effect on save, and a switch + would imply it applies the moment it is flipped. No description - a second line beside + the single-line name field leaves the row ragged. */} + onEnabledChange(e.target.checked)} + label={t("portal.pipelines.builder.enabled")} + /> +
    + +
    + file && onTest(file)} + leftSection={} + > + {t("portal.pipelines.builder.testRun")} + + + {isEdit && ( + + )} + + {/* Occasional things - reading the definition, wiping the processed history - kept behind a + tray so they do not compete with running and testing, which is what this row is for. */} + + + + + + + + } + > + {t("portal.pipelines.builder.viewDefinition")} + + {isEdit && ( + + } + > + {t("portal.pipelines.detail.clearHistory")} + + )} + + + + {isEdit && ( + + )} +
    + + {runResult && ( + + )} +
    + ); +} + +interface RunResultStripProps { + result: RunResultSummary; + onDownload: (output: RunOutputFile) => void; +} + +/** What the last test run did, beside the button that started it. */ +function RunResultStrip({ result, onDownload }: RunResultStripProps) { + const { t } = useTranslation(); + const outputs = result.outputs ?? []; + + return ( +
    +
    + {result.status === "running" && } + {result.status === "completed" && ( + + )} + {result.status === "failed" && ( + + )} + + {t(`portal.pipelines.inspector.status.${result.status}`, { + done: result.completedSteps, + count: result.stepCount, + })} + +
    + + {/* The reason it failed, where the failure is announced - not only on the node, which the user + has to know to click. */} + {result.status === "failed" && result.error && ( + + {result.error} + + )} + + {outputs.map((output) => ( + + ))} +
    + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineInspector.css b/frontend/editor/src/portal/components/pipelines/PipelineInspector.css new file mode 100644 index 0000000000..0fc50597e8 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineInspector.css @@ -0,0 +1,34 @@ +/** + * The builder's right-hand panel: the selected node's settings. + */ + +.portal-inspector { + display: flex; + flex-direction: column; + gap: 0.875rem; + padding: 1.125rem; + background: var(--c-surface); + border: 1px solid var(--c-border-subtle); + border-radius: var(--radius-lg); + /* The builder caps its columns so the page itself does not scroll, which means a settings form + taller than the viewport has to scroll in here - otherwise its lower half is unreachable. */ + max-height: 100%; +} + +.portal-inspector__body { + display: flex; + flex-direction: column; + gap: 0.875rem; + min-height: 0; + overflow-y: auto; +} + +/* Names the node being edited, so the panel is not just a nameless form. */ +.portal-inspector__title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + color: var(--c-text); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineInspector.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineInspector.stories.tsx new file mode 100644 index 0000000000..ff4c8f3d21 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineInspector.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FormField, Input, Select } from "@app/ui"; +import { PipelineInspector } from "@portal/components/pipelines/PipelineInspector"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineInspector", + component: PipelineInspector, + parameters: { layout: "padded" }, + decorators: [ + (Story) => ( +
    + +
    + ), + ], +}; +export default meta; +type Story = StoryObj; + +/** Stands in for a node's real editor, which the builder supplies. */ +function StubSettings() { + return ( + <> + + + + + } onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") onClose(); @@ -104,36 +104,48 @@ export function ToolPicker({
    {group.label}
    - {group.tools.map((tool) => ( - - ))} + + ); + })} )) )} diff --git a/frontend/editor/src/portal/components/pipelines/graph/GraphEdge.css b/frontend/editor/src/portal/components/pipelines/graph/GraphEdge.css new file mode 100644 index 0000000000..a821b9910e --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/GraphEdge.css @@ -0,0 +1,180 @@ +/** + * One wire between two nodes, plus its insert affordance. The graph positions it; the wire is a + * 1px rule centred on the column with a filled arrowhead at the arriving end. + */ + +.portal-graph-edge { + position: absolute; + /* A wide drop target: the wire is a thin line, but a dragged step can be released anywhere across + the row, so the whole band between the nodes catches it. `left` is the column centre, so pull + back by half to keep the band centred on it. Line, insert and warning are placed absolutely + within. */ + width: 16rem; + transform: translateX(-50%); + --edge-color: var(--c-border-strong); +} + +.portal-graph-edge__line { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 1px; + transform: translateX(-50%); + background: var(--edge-color); +} + +/** + * Arrowhead at the arriving end, so the chain reads as directed. + */ +.portal-graph-edge__line::after { + content: ""; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + transform: translateX(-50%); + border-left: 0.1875rem solid transparent; + border-right: 0.1875rem solid transparent; + border-top: 0.3125rem solid var(--edge-color); +} + +/* Insert: the shared ActionIcon restyled to a small dot beside the wire (not on top of it, where it + hid the line). Hidden at rest - a solid plus on every wire reads as busy - and revealed only when + the pointer is over this wire's drop band (see the reveal rule below). When shown it is solid, not + faint: off to one side on the canvas it needs a real border and glyph to be seen at all. */ +.portal-graph-edge__insert.sui-ai { + position: absolute; + top: 50%; + /* Beside the wire: the column centre is 50%, nudge clear of the line and centre on the row. */ + left: 50%; + transform: translate(0.6rem, -50%); + display: flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; + border: 1px solid var(--c-border-strong); + background: var(--c-surface); + color: var(--c-text-muted); + opacity: 0; + transition: + opacity var(--motion-fast), + color var(--motion-fast), + border-color var(--motion-fast), + background var(--motion-fast); +} + +/* Optically centre the glyph in the circle: MUI's Add icon carries a hair of bottom bias. */ +.portal-graph-edge__insert.sui-ai svg { + display: block; +} + +/* On a warning wire the insert sits just past the pill, out of flow so the pill stays centred on the + wire whether or not the insert is showing (it reveals on hover like every other wire's). */ +.portal-graph-edge__note .portal-graph-edge__insert.sui-ai { + left: 100%; + margin-left: 0.375rem; + transform: translateY(-50%); +} + +/* Reveal the insert when the pointer is anywhere in this wire's drop band, or it has keyboard focus. + Revealing is not highlighting: it comes in at its resting weight and only goes primary once the + pointer is on the button itself (below) - not from anywhere in the wide band. */ +.portal-graph-edge:hover .portal-graph-edge__insert.sui-ai, +.portal-graph-edge__insert.sui-ai:focus-visible { + opacity: 1; +} + +.portal-graph-edge__insert.sui-ai:hover, +.portal-graph-edge__insert.sui-ai:focus-visible { + color: var(--c-accent-fg, var(--c-primary)); + border-color: var(--c-primary); +} + +/* A wire with no slot of its own (either side of the placeholder): line only. */ +.portal-graph-edge.is-plain { + --edge-color: var(--c-border-subtle); +} + +/** + * The pairing does not make much sense. Advisory: the wire still accepts drops and the chain still + * runs - the order stays the user's choice. + */ +.portal-graph-edge.has-warning { + --edge-color: var(--c-warning); +} + +/* The note and its insert ride together, centred on the wire, so a warned pairing keeps a way to + take a fixing step between its ends. Grows to its content and may overhang the band, which the + wider graph column absorbs. */ +.portal-graph-edge__note { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 0.375rem; +} + +.portal-graph-edge__warning { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.0625rem 0.375rem; + border-radius: var(--radius-pill); + border: 1px solid var(--c-warning); + /* Matches the shared Banner's warning treatment: tinted ground, amber border and glyph, ordinary + text. Amber words would not clear 4.5:1 at this size, and the tint is what makes the neutral + text read as part of a warning rather than as stray body copy. */ + background: color-mix(in srgb, var(--c-warning) 12%, var(--c-surface)); + color: var(--c-text); + font-size: 0.6875rem; + line-height: 1.4; +} + +.portal-graph-edge__warning svg { + color: var(--c-warning); + flex: none; +} + +/* A blocking pairing: the chain cannot run in this order, so it must not read as the same gentle + advice as an odd-but-workable one. Same shape, danger tone - including the wire and its head, + which follow --edge-color. */ +.portal-graph-edge.is-blocking { + --edge-color: var(--c-danger); +} + +.portal-graph-edge.is-blocking .portal-graph-edge__warning { + border-color: var(--c-danger); + background: color-mix(in srgb, var(--c-danger) 12%, var(--c-surface)); +} + +.portal-graph-edge.is-blocking .portal-graph-edge__warning svg { + color: var(--c-danger); +} + +.portal-graph-edge__warning-label { + white-space: nowrap; +} + +/* While a step is being dragged, the insert is beside the point - the wire itself is the target - + and it would only clutter the row and collide with the drag hint. Hide it until the drag ends. + The wire itself stays at rest until the step is actually over it: lighting every wire the moment + a drag starts is noise, not a cue. */ +.portal-graph-edge.is-available .portal-graph-edge__insert.sui-ai { + display: none; +} + +/* The step is over this wire and would land here on release: only then does the wire go primary. */ +.portal-graph-edge.is-over { + --edge-color: var(--c-primary); +} + +.portal-graph-edge.is-over .portal-graph-edge__line { + width: 2px; +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/GraphEdge.tsx b/frontend/editor/src/portal/components/pipelines/graph/GraphEdge.tsx new file mode 100644 index 0000000000..7f7dcca5d4 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/GraphEdge.tsx @@ -0,0 +1,109 @@ +import { useTranslation } from "react-i18next"; +import AddRoundedIcon from "@mui/icons-material/AddRounded"; +import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded"; +import { ActionIcon } from "@app/ui"; +import type { LaidOutEdge } from "@portal/components/pipelines/graph/pipelineLayout"; +import { useEdgeDrop } from "@portal/components/pipelines/graph/useChainDragDrop"; +import "@portal/components/pipelines/graph/GraphEdge.css"; + +/** + * A note on the wire arriving at a node: why what flows in will not suit it. + * + * `blocking` separates "this cannot run in this order" from "this is probably not what you meant". + * Both are shown on the wire and neither refuses the edit - the order stays the user's to choose - + * but only a blocking one stops the pipeline being saved, so it must not read as mere advice. + */ +export interface ChainWarning { + text: string; + blocking?: boolean; +} + +export interface GraphEdgeProps { + edge: LaidOutEdge; + /** Add a new step in the slot this wire opens. */ + onInsert: (index: number) => void; + stepCount: number; + /** Given the chain's new order as original step indices, and which steps the drag carried. */ + onReorder: (order: number[], moved: readonly number[]) => void; + /** A step is in flight, so open wires advertise themselves as landing spots. */ + dragActive: boolean; + /** + * Why what flows along this wire will not be much use to the node it arrives at (encrypting + * before an OCR, say). Never refuses the edit - the order stays the user's to choose - but a + * blocking one means the chain cannot run at all, and is coloured apart from mere advice. + */ + warning?: ChainWarning; +} + +/** + * One wire between two nodes: a directed line carrying an insert affordance, and the drop target + * that catches a step dragged onto it. Where the pairing does not make sense the wire says so, + * rather than refusing it. + */ +export function GraphEdge({ + edge, + onInsert, + stepCount, + onReorder, + dragActive, + warning, +}: GraphEdgeProps) { + const { t } = useTranslation(); + const { ref, over } = useEdgeDrop({ + insertIndex: edge.insertIndex, + stepCount, + onReorder, + }); + const open = edge.insertIndex !== null; + + // The insert affordance is shown whenever the wire opens a slot - including on a warned wire, so a + // bad pairing can still take a fixing step between its ends rather than losing its only way in. + const insertButton = open ? ( + onInsert(edge.insertIndex as number)} + > + + + ) : null; + + return ( +
    + + {warning ? ( + + + + + {warning.text} + + + {insertButton} + + ) : ( + insertButton + )} +
    + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/GraphNode.css b/frontend/editor/src/portal/components/pipelines/graph/GraphNode.css new file mode 100644 index 0000000000..9bea07fd4b --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/GraphNode.css @@ -0,0 +1,140 @@ +/** + * Graph-only extras layered on the shared NodeCard tile (see @app/ui/NodeCard): the config warning + * line, drag dimming, the remove control and run-state glyphs. The surface, selection ring and + * icon/title/detail layout all live in NodeCard. + */ + +/* Amber carries the tone on the glyph; the words stay body-coloured. --c-warning is amber-600, + which is only 3.18:1 on a light surface at this size (axe) - readable as an icon, not as text. */ +.portal-graph-node__warning { + display: inline-flex; + align-items: center; + gap: 0.25rem; + color: var(--c-text); +} + +.portal-graph-node__warning svg { + color: var(--c-warning); + flex: none; +} + +/* Lifted out of the chain: the origin dims so the drop target reads as the real position. */ +.portal-graph-node.is-dragging { + opacity: 0.4; +} + +/* Steps can be picked up and moved; the input and output are fixed ends. */ +.portal-graph-node--step .sui-node-card__select.sui-btn { + cursor: grab; +} + +.portal-graph-node--step.is-dragging .sui-node-card__select.sui-btn { + cursor: grabbing; +} + +/* Remove: quiet until the node is hovered or focused, so the chain stays calm. */ +.portal-graph-node__remove.sui-ai { + position: absolute; + top: -0.4375rem; + /* Logical, so in RTL the remove sits on the card's trailing (left) corner rather than on top of + the leading icon badge. */ + inset-inline-end: -0.4375rem; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; + border: 1px solid var(--c-border); + background: var(--c-surface); + color: var(--c-text-subtle); + opacity: 0; + transition: + opacity var(--motion-fast), + color var(--motion-fast), + border-color var(--motion-fast); +} + +.portal-graph-node:hover .portal-graph-node__remove.sui-ai, +.portal-graph-node.is-selected .portal-graph-node__remove.sui-ai, +.portal-graph-node__remove.sui-ai:focus-visible { + opacity: 1; +} + +.portal-graph-node__remove.sui-ai:hover { + color: var(--c-danger); + border-color: var(--c-danger); +} + +/* Run state: a status glyph on the card's trailing edge, inside the node. The state is carried by + the icon's shape as well as its colour, with the wording kept for assistive tech. */ +.portal-graph-node__run { + flex: none; + align-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + margin-inline-end: 0.625rem; + color: var(--c-text-subtle); +} + +/* A failed step's glyph is a button (it opens the error), so re-assert the plain glyph look over + the shared ActionIcon base. */ +.portal-graph-node__run--open.sui-ai { + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; + border: none; + background: none; + color: var(--c-danger); +} + +.portal-graph-node__run-label { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.portal-graph-node.is-done .portal-graph-node__run { + color: var(--c-success); +} + +.portal-graph-node.is-failed .portal-graph-node__run { + color: var(--c-danger); +} + +/* Running is carried by the pulsing glyph alone: a primary border here would be the selected + treatment, and "the step I am editing" must stay distinguishable from "the step running now". */ +.portal-graph-node.is-running .portal-graph-node__run { + color: var(--c-primary); +} + +.portal-graph-node__pulse { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: currentColor; + animation: portal-graph-pulse 1.2s ease-in-out infinite; +} + +@keyframes portal-graph-pulse { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .portal-graph-node__pulse { + animation: none; + } +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/GraphNode.tsx b/frontend/editor/src/portal/components/pipelines/graph/GraphNode.tsx new file mode 100644 index 0000000000..c19a7c15f2 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/GraphNode.tsx @@ -0,0 +1,183 @@ +import type { MouseEvent, ReactNode, Ref } from "react"; +import { useTranslation } from "react-i18next"; +import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; +import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; +import ErrorOutlineRoundedIcon from "@mui/icons-material/ErrorOutlineRounded"; +import MoveToInboxRoundedIcon from "@mui/icons-material/MoveToInboxRounded"; +import SendRoundedIcon from "@mui/icons-material/SendRounded"; +import TuneRoundedIcon from "@mui/icons-material/TuneRounded"; +import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded"; +import { ActionIcon, NodeCard } from "@app/ui"; +import { type IconBadgeAccent } from "@app/ui/IconBadge"; +import type { GraphNodeKind } from "@portal/components/pipelines/graph/pipelineLayout"; +import "@portal/components/pipelines/graph/GraphNode.css"; + +/** How a node is faring in the current or last test run. */ +export type NodeRunState = "running" | "done" | "failed"; + +/** The kinds this card renders. The placeholder is its own component, not a node variant. */ +type CardKind = Exclude; + +const KIND_ICON: Record = { + input: , + step: , + output: , +}; + +const KIND_ACCENT: Record = { + input: "green", + step: "blue", + output: "purple", +}; + +export interface GraphNodeProps { + kind: CardKind; + title: string; + /** One-line summary under the title (the source's path, a step's parameters). */ + detail?: string; + /** + * Problem with this node's configuration, shown in place of the detail. Distinct from a run + * failure: this is why the pipeline cannot be saved yet. + */ + warning?: string; + /** The step's own tool glyph; falls back to a per-kind default. */ + icon?: ReactNode; + selected: boolean; + runState?: NodeRunState; + onOpenRunState?: () => void; + onSelect: (event: MouseEvent) => void; + /** Takes the node off the chain. For an end, that returns its row to a placeholder. */ + onRemove?: () => void; + /** True while this node is being dragged to another place in the chain. */ + dragging?: boolean; + /** + * The step's place in the chain, so a multi-step drag preview can find the other selected cards + * in the DOM. Absent for the input and output, which are never dragged. + */ + stepIndex?: number; + /** The card element, for the drag adapter to register against. */ + ref?: Ref; +} + +/** + * One node in the pipeline graph: the shared {@link NodeCard} tile carrying its glyph, title and a + * one-line summary, plus the graph-only extras layered on top - run status, a remove control, drag + * dimming, and the "why this cannot be saved" warning line. Position is applied by the graph, so the + * node itself knows nothing about layout. + */ +export function GraphNode({ + kind, + title, + detail, + warning, + icon, + selected, + runState, + onOpenRunState, + onSelect, + onRemove, + dragging, + stepIndex, + ref, +}: GraphNodeProps) { + const { t } = useTranslation(); + + const runStatus = runState && ( + + ); + const remove = onRemove && ( + + + + ); + + return ( + + + {warning} + + ) : ( + detail + ) + } + trailing={ + <> + {runStatus} + {remove} + + } + /> + ); +} + +interface RunStatusProps { + runState: NodeRunState; + title: string; + onOpenRunState?: () => void; +} + +/** The run glyph on the card's trailing edge; a button when it opens a failure, else a status. */ +function RunStatus({ runState, title, onOpenRunState }: RunStatusProps) { + const { t } = useTranslation(); + if (runState === "failed" && onOpenRunState) { + return ( + + + + ); + } + return ( + + {runState === "running" && ( + + )} + {runState === "done" && ( + + )} + {runState === "failed" && ( + + )} + + {t(`portal.pipelines.graph.run.${runState}`)} + + + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.css b/frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.css new file mode 100644 index 0000000000..dba96072c4 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.css @@ -0,0 +1,44 @@ +/** + * The empty-pipeline stand-in: a dashed node in the row the first step will take. Dashed rather + * than solid so it reads as "not yet a step", and full width so it is an obvious target. + */ + +.portal-graph-placeholder.sui-btn { + width: 100%; + height: auto; + min-height: 0; + padding: 0.625rem 0.75rem; + background: none; + border: 1px dashed var(--c-border-strong); + border-radius: var(--radius-lg); + color: var(--c-text-muted); + font-weight: 400; + text-align: left; + transition: + border-color var(--motion-fast), + color var(--motion-fast), + background var(--motion-fast); +} + +.portal-graph-placeholder.sui-btn:hover { + border-color: var(--c-primary); + border-style: solid; + color: var(--c-accent-fg, var(--c-primary)); + background: var(--c-surface); +} + +/* Mantine lays a button's children out inside its label element, not on the root. */ +.portal-graph-placeholder.sui-btn .mantine-Button-label { + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.625rem; + min-width: 0; + overflow: visible; +} + +.portal-graph-placeholder__title { + font-size: 0.875rem; + font-weight: 500; +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.tsx b/frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.tsx new file mode 100644 index 0000000000..0ecee1bfc7 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/GraphPlaceholderNode.tsx @@ -0,0 +1,30 @@ +import AddRoundedIcon from "@mui/icons-material/AddRounded"; +import { Button } from "@app/ui"; +import "@portal/components/pipelines/graph/GraphPlaceholderNode.css"; + +export interface GraphPlaceholderNodeProps { + label: string; + onAdd: () => void; +} + +/** + * The stand-in for a row the pipeline has not filled yet - the first step, or either end of the + * chain on a new pipeline. It sits in the row that thing will occupy, so the chain reads as + * input -> something -> output straight away, and it is the thing you click to fill it: a full-width + * target, rather than a caption pointing at a small plus on a wire. + */ +export function GraphPlaceholderNode({ + label, + onAdd, +}: GraphPlaceholderNodeProps) { + return ( + + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.css b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.css new file mode 100644 index 0000000000..c9aff67012 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.css @@ -0,0 +1,79 @@ +/** + * The graph surface. The canvas is sized by the derived layout and centred in the scroll area, so + * the chain stays put as steps are added or removed. + */ + +.portal-graph { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1.5rem 1rem; + /* Grows with the chain up to whatever the page gives it, then scrolls rather than pushing the + inspector out of reach. A short chain still hugs its content, so there is no empty canvas. */ + max-height: 100%; + overflow: auto; + /* The canvas must sit below the node cards on the surface ladder so they read as raised off it in + both themes. --c-surface-sunken is the only rung darker than --c-surface in light *and* dark; + the legacy --color-bg-subtle collapsed into the page in dark, and --c-bg-raised is lighter than + the cards in light. */ + background: var(--c-surface-sunken); + border: 1px solid var(--c-border-subtle); + border-radius: var(--radius-lg); +} + +/* Sits beside the wire it is describing. Absolute, so appearing mid-drag moves nothing. */ +.portal-graph__drag-hint { + position: absolute; + margin: 0; + /* `left` is the column centre; clear the wire's hit area before the text starts. */ + transform: translate(1.75rem, -50%); + white-space: nowrap; + font-size: 0.75rem; + font-weight: 500; + color: var(--c-accent-fg, var(--c-primary)); + pointer-events: none; +} + +/* transform has no logical form, so mirror it by hand: in RTL the hint clears the wire on the other + side rather than reaching back across it onto the chain. */ +[dir="rtl"] .portal-graph__drag-hint { + transform: translate(-1.75rem, -50%); +} + +/* What follows the cursor when several steps are dragged at once: a copy of each card, stacked, so + the drag shows what is actually moving rather than only the card that was grabbed. Cloned nodes + keep their own styling; they are inert copies, hence no pointer events. */ +.portal-graph__drag-preview { + display: flex; + flex-direction: column; + gap: 0.375rem; + pointer-events: none; +} + +.portal-graph__drag-preview .portal-graph-node { + opacity: 0.9; +} + +.portal-graph__canvas { + position: relative; + flex: none; +} + +/* Nodes are placed by the layout; the slot carries the position, the card fills it. */ +.portal-graph__slot { + position: absolute; + display: flex; +} + +.portal-graph__slot > * { + flex: 1; + min-width: 0; +} + +.portal-graph__hint { + margin: 0; + font-size: 0.8125rem; + color: var(--c-text-subtle); + text-align: center; +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.stories.tsx b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.stories.tsx new file mode 100644 index 0000000000..c16291bd9c --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.stories.tsx @@ -0,0 +1,207 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + PipelineGraph, + selectedSteps, + type ChainEnd, + type GraphNodeContent, + type GraphSelection, + type GraphStepContent, +} from "@portal/components/pipelines/graph/PipelineGraph"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineGraph", + component: PipelineGraph, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +const INPUT = { + label: "Claims intake", + detail: "/srv/claims/in - every hour", +}; +const OUTPUT = { + label: "Archive bucket", + detail: "s3://claims-archive/done", +}; + +/** + * The builder owns the chain in the app, so the stories own it here - otherwise adding, removing + * and dragging would fire their handlers and visibly do nothing. Everything in these stories is + * live: click a wire's plus to insert, the node's X to remove, and drag a step onto a wire to move + * it there. + */ +function Playground({ + initialSteps, + output = OUTPUT, + /** Start with neither end on the chain, the way a brand new pipeline opens. */ + unplacedEnds = false, +}: { + initialSteps: GraphStepContent[]; + output?: { label: string; detail?: string; warning?: string }; + unplacedEnds?: boolean; +}) { + const [steps, setSteps] = useState(initialSteps); + const [selected, setSelected] = useState(null); + const [added, setAdded] = useState(0); + const [inputEnd, setInputEnd] = useState( + unplacedEnds ? null : INPUT, + ); + const [outputEnd, setOutputEnd] = useState( + unplacedEnds ? null : output, + ); + + // Placing an end leaves it owing a choice, which is the warning state the builder shows until the + // user picks a source or destination. + function addEnd(end: ChainEnd) { + if (end === "input") { + setInputEnd({ label: "Choose a source", warning: "No source chosen" }); + } else { + setOutputEnd({ + label: "Choose a destination", + warning: "No destination chosen", + }); + } + setSelected(end); + } + + function removeEnd(end: ChainEnd) { + if (end === "input") setInputEnd(null); + else setOutputEnd(null); + setSelected((current) => (current === end ? null : current)); + } + + function insert(at: number) { + const label = `New tool ${added + 1}`; + setAdded((n) => n + 1); + setSteps((current) => { + const next = [...current]; + next.splice(at, 0, { label }); + return next; + }); + setSelected({ steps: [at] }); + } + + function remove(indices: number[]) { + const gone = new Set(indices); + setSteps((current) => current.filter((_, i) => !gone.has(i))); + setSelected(null); + } + + function reorder(order: number[]) { + const moving = new Set(selectedSteps(selected)); + setSteps((current) => order.map((i) => current[i])); + const landed = order + .map((original, position) => ({ original, position })) + .filter(({ original }) => moving.has(original)) + .map(({ position }) => position); + setSelected(landed.length > 0 ? { steps: landed } : null); + } + + return ( + + ); +} + +/** A typical chain. Drag a step onto any wire to move it there. */ +export const Default: Story = { + render: () => ( + + ), +}; + +/** A pipeline with its ends settled but no steps yet: the placeholder holds the first step's place. */ +export const Empty: Story = { + render: () => , +}; + +/** + * A brand new pipeline, before anything has been chosen. Every row is an invitation rather than a + * complaint - nothing is wrong yet, because nothing has been asked of the user. Click an end to + * place it (it then owes a choice, and says so), and its X puts it back. + */ +export const NewPipeline: Story = { + render: () => , +}; + +/** + * An order that will not do what the user probably meant: OCR cannot read a file that the previous + * step encrypted. The wire says so and the chain still runs - nothing is refused, and the step can + * still be dragged anywhere. + */ +export const OddOrdering: Story = { + render: () => ( + + ), +}; + +/** Mid test-run: finished steps carry a tick, the current one pulses. */ +export const Running: Story = { + render: () => ( + + ), +}; + +/** A failed run, and a step that cannot be saved: the warning replaces the detail line. */ +export const Problems: Story = { + render: () => ( + + ), +}; + +/** + * Multi-selection: cmd/ctrl-click to add a step, shift-click for a run of them, then drag any one + * onto a line to move the whole set together. + */ +export const MultiSelect: Story = { + render: () => ( + + ), +}; diff --git a/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.test.tsx b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.test.tsx new file mode 100644 index 0000000000..c94bd512f3 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.test.tsx @@ -0,0 +1,455 @@ +import { useState } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { + fireEvent, + render as baseRender, + screen, +} from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import { + PipelineGraph, + type GraphSelection, + type PipelineGraphProps, +} from "@portal/components/pipelines/graph/PipelineGraph"; + +// The nodes and wires are built from the shared Mantine-backed controls, so they need the provider. +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +// Deterministic i18n: keys returned verbatim, interpolation applied so aria-labels stay distinct. +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, vars?: Record) => + vars?.name ? `${key}:${String(vars.name)}` : key, + }), +})); + +function renderGraph(overrides: Partial = {}) { + const handlers = { + onSelect: vi.fn(), + onAddEnd: vi.fn(), + onRemoveEnd: vi.fn(), + onInsertStep: vi.fn(), + onRemoveSteps: vi.fn(), + onReorderSteps: vi.fn(), + }; + render( + , + ); + return handlers; +} + +describe("PipelineGraph", () => { + it("renders the chain: input, each step in order, output", () => { + renderGraph(); + const titles = screen + .getAllByRole("button", { pressed: false }) + .map((node) => node.textContent); + expect(titles[0]).toContain("Claims intake"); + expect(titles[1]).toContain("OCR"); + expect(titles[2]).toContain("Redact"); + expect(titles[3]).toContain("Archive bucket"); + }); + + it("shows each node's one-line detail", () => { + renderGraph(); + expect(screen.getByText("/in - every hour")).toBeInTheDocument(); + expect(screen.getByText("s3://claims/done")).toBeInTheDocument(); + }); + + it("selects the ends by their kind and steps by index", () => { + const handlers = renderGraph(); + fireEvent.click(screen.getByText("Claims intake")); + expect(handlers.onSelect).toHaveBeenCalledWith("input"); + fireEvent.click(screen.getByText("Archive bucket")); + expect(handlers.onSelect).toHaveBeenCalledWith("output"); + fireEvent.click(screen.getByText("Redact")); + expect(handlers.onSelect).toHaveBeenCalledWith({ steps: [1] }); + }); + + it("adds and removes steps from the selection with cmd/ctrl-click", () => { + const handlers = renderGraph({ selected: { steps: [0] } }); + fireEvent.click(screen.getByText("Redact"), { metaKey: true }); + expect(handlers.onSelect).toHaveBeenCalledWith({ steps: [0, 1] }); + }); + + it("cmd/ctrl-clicking the only selected step clears the selection", () => { + const handlers = renderGraph({ selected: { steps: [1] } }); + fireEvent.click(screen.getByText("Redact"), { ctrlKey: true }); + expect(handlers.onSelect).toHaveBeenCalledWith(null); + }); + + it("shift-click takes everything between the anchor and the clicked step", () => { + const handlers = renderGraph({ + selected: { steps: [0] }, + steps: [ + { label: "OCR" }, + { label: "Redact" }, + { label: "Compress" }, + { label: "Stamp" }, + ], + }); + fireEvent.click(screen.getByText("Stamp"), { shiftKey: true }); + expect(handlers.onSelect).toHaveBeenCalledWith({ steps: [0, 1, 2, 3] }); + }); + + it("marks every selected step as pressed, not just one", () => { + renderGraph({ selected: { steps: [0, 1] } }); + for (const label of ["OCR", "Redact"]) { + expect(screen.getByText(label).closest("button")).toHaveAttribute( + "aria-pressed", + "true", + ); + } + }); + + it("keeps the drag hint silent until a drag starts", () => { + // Only the silent half is testable here: starting a real drag needs native HTML5 drag events, + // which jsdom does not implement, so the visible half is checked in a browser. + renderGraph(); + expect( + screen.queryByText("portal.pipelines.graph.dragHint"), + ).not.toBeInTheDocument(); + }); + + it("clears the selection when the canvas itself is clicked", () => { + const handlers = renderGraph({ selected: { steps: [1] } }); + fireEvent.click(document.querySelector(".portal-graph") as HTMLElement); + expect(handlers.onSelect).toHaveBeenCalledWith(null); + }); + + it("does not clear the selection when a node is clicked", () => { + // The node's own handler runs; the background handler must not undo it. + const handlers = renderGraph({ selected: { steps: [1] } }); + fireEvent.click(screen.getByText("OCR")); + expect(handlers.onSelect).toHaveBeenCalledWith({ steps: [0] }); + expect(handlers.onSelect).not.toHaveBeenCalledWith(null); + }); + + it("marks the selected node as pressed", () => { + renderGraph({ selected: { steps: [0] } }); + expect(screen.getByText("OCR").closest("button")).toHaveAttribute( + "aria-pressed", + "true", + ); + }); + + it("puts an insert on every wire, reporting the slot it opens", () => { + const handlers = renderGraph(); + // input->OCR, OCR->Redact, Redact->output + const inserts = screen.getAllByLabelText( + "portal.pipelines.graph.insertHere", + ); + expect(inserts).toHaveLength(3); + fireEvent.click(inserts[1]); + expect(handlers.onInsertStep).toHaveBeenCalledWith(1); + }); + + it("holds the first step's place with a placeholder when the chain is empty", () => { + const handlers = renderGraph({ steps: [] }); + // The placeholder is the affordance, so the wires either side of it carry no plus of their + // own - two ways to fill the same slot would be a choice with no difference. + expect( + screen.queryByLabelText("portal.pipelines.graph.insertHere"), + ).not.toBeInTheDocument(); + fireEvent.click(screen.getByText("portal.pipelines.graph.addFirstTool")); + expect(handlers.onInsertStep).toHaveBeenCalledWith(0); + }); + + it("drops the placeholder once the chain has a step", () => { + renderGraph({ steps: [{ label: "OCR" }] }); + expect( + screen.queryByText("portal.pipelines.graph.addFirstTool"), + ).not.toBeInTheDocument(); + }); + + it("warns on the wire arriving at a step it makes little sense to feed", () => { + renderGraph({ + steps: [ + { label: "Add Password" }, + { + label: "OCR", + inputWarning: { text: "OCR cannot read an encrypted file" }, + }, + ], + }); + expect( + screen.getByText("OCR cannot read an encrypted file"), + ).toBeInTheDocument(); + }); + + it("still allows the odd pairing: warned wires keep taking inserts", () => { + // Advisory, not a block - the order stays the user's to choose. + const handlers = renderGraph({ + steps: [ + { label: "Add Password" }, + { + label: "OCR", + inputWarning: { text: "OCR cannot read an encrypted file" }, + }, + ], + }); + // The warned wire shows its note and keeps its plus, so a fixing step can still go between the + // ends that do not suit each other - every wire takes an insert. + const inserts = screen.getAllByLabelText( + "portal.pipelines.graph.insertHere", + ); + expect(inserts).toHaveLength(3); + // The middle wire is the warned one (Add Password -> OCR); inserting there lands between them. + fireEvent.click(inserts[1]); + expect(handlers.onInsertStep).toHaveBeenCalledWith(1); + }); + + it("marks a blocking pairing apart from a merely odd one", () => { + // Both sit on the wire and neither refuses the edit, but one means the chain cannot run at + // all - so it must not read as the same gentle advice. + renderGraph({ + steps: [ + { label: "Extract images" }, + { + label: "Compress", + inputWarning: { text: "Compress needs a PDF", blocking: true }, + }, + ], + }); + const wire = screen + .getByText("Compress needs a PDF") + .closest(".portal-graph-edge"); + expect(wire).toHaveClass("is-blocking"); + }); + + it("leaves an advisory pairing unblocked", () => { + renderGraph({ + steps: [ + { label: "Add Password" }, + { label: "OCR", inputWarning: { text: "OCR cannot read this" } }, + ], + }); + expect( + screen.getByText("OCR cannot read this").closest(".portal-graph-edge"), + ).not.toHaveClass("is-blocking"); + }); + + it("warns on the wire into the output too", () => { + renderGraph({ + output: { + label: "Archive", + inputWarning: { text: "Nothing writes a folder here" }, + }, + }); + expect( + screen.getByText("Nothing writes a folder here"), + ).toBeInTheDocument(); + }); + + it("removes a step from the node itself", () => { + const handlers = renderGraph(); + fireEvent.click( + screen.getByLabelText("portal.pipelines.graph.removeNode:Redact"), + ); + expect(handlers.onRemoveSteps).toHaveBeenCalledWith([1]); + }); + + it("every node on the chain carries its own remove, ends included", () => { + renderGraph(); + // Two steps plus both ends: an end can be taken back off to its placeholder. + expect(screen.getAllByLabelText(/graph.removeNode/)).toHaveLength(4); + }); + + it("takes an end back off the chain", () => { + const handlers = renderGraph(); + fireEvent.click( + screen.getByLabelText("portal.pipelines.graph.removeNode:Claims intake"), + ); + expect(handlers.onRemoveEnd).toHaveBeenCalledWith("input"); + }); + + it("deletes every selected step with the Delete key", () => { + const handlers = renderGraph({ selected: { steps: [0, 1] } }); + fireEvent.keyDown(screen.getByText("Redact"), { key: "Delete" }); + expect(handlers.onRemoveSteps).toHaveBeenCalledWith([0, 1]); + }); + + it("takes a selected end off the chain with the Delete key, like its X does", () => { + const handlers = renderGraph({ selected: "input" }); + fireEvent.keyDown(screen.getByText("Claims intake"), { key: "Delete" }); + expect(handlers.onRemoveEnd).toHaveBeenCalledWith("input"); + // An end is not a step, so the step remover stays out of it. + expect(handlers.onRemoveSteps).not.toHaveBeenCalled(); + }); + + it("ignores Delete when nothing is selected", () => { + const handlers = renderGraph({ selected: null }); + fireEvent.keyDown(screen.getByText("OCR"), { key: "Delete" }); + expect(handlers.onRemoveSteps).not.toHaveBeenCalled(); + expect(handlers.onRemoveEnd).not.toHaveBeenCalled(); + }); + + it("moves the selected step down the chain with Alt+ArrowDown", () => { + const handlers = renderGraph({ selected: { steps: [0] } }); + fireEvent.keyDown(screen.getByText("OCR"), { + key: "ArrowDown", + altKey: true, + }); + // [OCR, Redact] with OCR moved down -> [Redact, OCR]; the dragged step is the reorder payload. + expect(handlers.onReorderSteps).toHaveBeenCalledWith([1, 0], [0]); + }); + + it("moves the selected step up the chain with Alt+ArrowUp", () => { + const handlers = renderGraph({ selected: { steps: [1] } }); + fireEvent.keyDown(screen.getByText("Redact"), { + key: "ArrowUp", + altKey: true, + }); + // [OCR, Redact] with Redact moved up -> [Redact, OCR]. + expect(handlers.onReorderSteps).toHaveBeenCalledWith([1, 0], [1]); + }); + + it("moves a multi-step selection together, keeping it as the payload", () => { + const handlers = renderGraph({ + selected: { steps: [0, 1] }, + steps: [{ label: "OCR" }, { label: "Redact" }, { label: "Compress" }], + }); + fireEvent.keyDown(screen.getByText("OCR"), { + key: "ArrowDown", + altKey: true, + }); + // [OCR, Redact, Compress] with [OCR, Redact] moved down -> [Compress, OCR, Redact]. + expect(handlers.onReorderSteps).toHaveBeenCalledWith([2, 0, 1], [0, 1]); + }); + + it("does not reorder past the end of the chain", () => { + const handlers = renderGraph({ selected: { steps: [0] } }); + fireEvent.keyDown(screen.getByText("OCR"), { + key: "ArrowUp", + altKey: true, + }); + expect(handlers.onReorderSteps).not.toHaveBeenCalled(); + }); + + it("leaves a bare arrow alone, so only the modifier reorders", () => { + const handlers = renderGraph({ selected: { steps: [0] } }); + fireEvent.keyDown(screen.getByText("OCR"), { key: "ArrowDown" }); + expect(handlers.onReorderSteps).not.toHaveBeenCalled(); + }); + + it("carries focus to the moved step so it can be walked several slots", () => { + // A real reorder renumbers the nodes, so focus has to follow the step or a second key press + // would act on whatever now sits where it started. Drive it through a stateful host. + function Host() { + const [steps, setSteps] = useState([ + { label: "OCR" }, + { label: "Redact" }, + { label: "Compress" }, + ]); + const [selected, setSelected] = useState({ steps: [0] }); + return ( + { + setSteps((current) => order.map((i) => current[i])); + const landed = order + .map((original, position) => ({ original, position })) + .filter(({ original }) => moved.includes(original)) + .map(({ position }) => position); + setSelected({ steps: landed }); + }} + /> + ); + } + render(); + fireEvent.keyDown(screen.getByText("OCR"), { + key: "ArrowDown", + altKey: true, + }); + // OCR now sits at position 1, and its card's select button holds focus. + expect(document.activeElement?.textContent).toContain("OCR"); + expect( + document.activeElement?.closest("[data-step-index]"), + ).toHaveAttribute("data-step-index", "1"); + }); + + describe("an end the pipeline has not asked for yet", () => { + it("offers to add it instead of naming it", () => { + renderGraph({ input: null }); + expect( + screen.getByText("portal.pipelines.graph.add.input"), + ).toBeInTheDocument(); + expect(screen.queryByText("Claims intake")).not.toBeInTheDocument(); + }); + + it("greets a brand new pipeline with no warnings at all", () => { + // The whole point: an end nobody has been offered yet is not a problem to report. + renderGraph({ + input: null, + output: null, + steps: [], + }); + expect(screen.queryByText(/warning|chosen/i)).not.toBeInTheDocument(); + expect(screen.getAllByText(/graph.add\./)).toHaveLength(2); + }); + + it("asks for the end when its placeholder is clicked", () => { + const handlers = renderGraph({ output: null }); + fireEvent.click(screen.getByText("portal.pipelines.graph.add.output")); + expect(handlers.onAddEnd).toHaveBeenCalledWith("output"); + }); + + it("has nothing to remove until it is placed", () => { + renderGraph({ input: null, output: null, steps: [] }); + expect( + screen.queryByLabelText(/graph.removeNode/), + ).not.toBeInTheDocument(); + }); + + it("carries no warning onto the wire that arrives at it", () => { + renderGraph({ output: null }); + expect( + screen.queryByText("Nothing writes a folder here"), + ).not.toBeInTheDocument(); + }); + }); + + it("shows a node's warning in place of its detail", () => { + renderGraph({ + steps: [ + { label: "Watermark", detail: "logo.png", warning: "Needs a file" }, + ], + }); + expect(screen.getByText("Needs a file")).toBeInTheDocument(); + expect(screen.queryByText("logo.png")).not.toBeInTheDocument(); + }); + + it("reports a run's progress on the steps it touched", () => { + renderGraph({ + steps: [ + { label: "OCR", runState: "done" }, + { label: "Redact", runState: "running" }, + ], + }); + expect( + screen.getByText("portal.pipelines.graph.run.done"), + ).toBeInTheDocument(); + expect( + screen.getByText("portal.pipelines.graph.run.running"), + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.tsx b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.tsx new file mode 100644 index 0000000000..7afb3a9f41 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/PipelineGraph.tsx @@ -0,0 +1,379 @@ +import { + useLayoutEffect, + useRef, + useState, + type KeyboardEvent, + type MouseEvent as ReactMouseEvent, + type ReactNode, +} from "react"; +import { useTranslation } from "react-i18next"; +import { + GraphNode, + type NodeRunState, +} from "@portal/components/pipelines/graph/GraphNode"; +import { + GraphEdge, + type ChainWarning, +} from "@portal/components/pipelines/graph/GraphEdge"; +import { GraphPlaceholderNode } from "@portal/components/pipelines/graph/GraphPlaceholderNode"; +import { + NODE_HEIGHT, + NODE_WIDTH, + layoutChain, + reorderMany, + stepIndexOf, +} from "@portal/components/pipelines/graph/pipelineLayout"; +import { useStepDraggable } from "@portal/components/pipelines/graph/useChainDragDrop"; +import "@portal/components/pipelines/graph/PipelineGraph.css"; + +// The wire renders it, but callers build it, so it is re-exported from the graph they talk to. +export type { ChainWarning }; + +/** + * What is selected: an end of the chain, one or more steps, or nothing. Only steps come in sets - + * the input and output are fixed ends, so there is nothing to gather or move. + */ +export type GraphSelection = "input" | "output" | { steps: number[] } | null; + +/** The selected step indices, in chain order. Empty unless steps are what is selected. */ +export function selectedSteps(selection: GraphSelection): number[] { + return selection !== null && typeof selection === "object" + ? selection.steps + : []; +} + +/** A node's display content. The graph never derives copy - the builder owns every label. */ +export interface GraphNodeContent { + label: string; + /** One-line summary: the source's path, a step's parameters, the destination. */ + detail?: string; + /** Why this node blocks saving, shown in place of the detail. */ + warning?: string; + /** Why the input will not be much use. */ + inputWarning?: ChainWarning; +} + +export interface GraphStepContent extends GraphNodeContent { + icon?: ReactNode; + runState?: NodeRunState; +} + +/** Which end of the chain: the two nodes every finished pipeline has, one of each. */ +export type ChainEnd = "input" | "output"; + +export interface PipelineGraphProps { + /** + * The chain's ends, or null while a new pipeline has yet to ask for one. Null renders the row as a + * placeholder to fill rather than a node owing a choice, which is what keeps a brand new pipeline + * from opening on a pair of warnings about decisions its author has not been offered yet. + */ + input: GraphNodeContent | null; + output: GraphNodeContent | null; + steps: GraphStepContent[]; + selected: GraphSelection; + onSelect: (selection: GraphSelection) => void; + /** Put an end on the chain, ready to be configured. */ + onAddEnd: (end: ChainEnd) => void; + /** Take an end back off, returning its row to a placeholder. */ + onRemoveEnd: (end: ChainEnd) => void; + /** Add a step in the slot the clicked wire opens. */ + onInsertStep: (index: number) => void; + /** Remove every step given, in one go. */ + onRemoveSteps: (indices: number[]) => void; + /** Reorder the chain to the given original step indices; `moved` is what the drag carried. */ + onReorderSteps: (order: number[], moved: readonly number[]) => void; + onOpenStepError?: (index: number) => void; +} + +/** + * The pipeline as a graph: one input, the steps in run order, one output. + * + * Layout is derived from the chain (see pipelineLayout), so there is nothing to lock, nothing to + * re-tidy and no stored positions - a node is always where its place in the order says it is. + * Dragging a step onto a wire moves it into that slot; clicking a node opens its settings in the + * inspector; the wires carry the insert affordance. + */ +export function PipelineGraph({ + input, + output, + steps, + selected, + onSelect, + onAddEnd, + onRemoveEnd, + onInsertStep, + onRemoveSteps, + onReorderSteps, + onOpenStepError, +}: PipelineGraphProps) { + const { t } = useTranslation(); + const [draggingIndex, setDraggingIndex] = useState(null); + const { nodes, edges, width, height } = layoutChain({ + stepCount: steps.length, + }); + + const graphRef = useRef(null); + // A keyboard reorder renumbers the nodes, so the focused card is no longer under the cursor's + // hand: without moving focus to where the step landed, a second Alt+Arrow would act on whatever + // now sits at the old position. Set by the handler, applied once the new order has rendered. + const focusStepAfterRender = useRef(null); + useLayoutEffect(() => { + const position = focusStepAfterRender.current; + if (position === null) return; + focusStepAfterRender.current = null; + graphRef.current + ?.querySelector( + `[data-step-index="${position}"] .sui-node-card__select`, + ) + ?.focus(); + }); + + // A wire carries the warning belonging to the node it arrives at. + const arrivalWarning = (nodeId: string): ChainWarning | undefined => { + if (nodeId === "output") return output?.inputWarning; + const index = stepIndexOf(nodeId); + return index === null ? undefined : steps[index]?.inputWarning; + }; + + /** + * Clicking the canvas itself clears the selection. Anything that is part of a node, a wire or the + * placeholder handles its own click, so only bare background gets here. + */ + function onBackgroundClick(event: ReactMouseEvent) { + const target = event.target as HTMLElement; + if ( + target.closest( + "[data-graph-node], .portal-graph-edge, .portal-graph-placeholder", + ) + ) { + return; + } + onSelect(null); + } + + const chosen = selectedSteps(selected); + + /** + * Plain click selects one step. Cmd/Ctrl toggles a step in or out of the selection; Shift takes + * everything between the first selected step and this one. The ends of the chain are single-only. + */ + function selectStep(index: number, event: ReactMouseEvent) { + if (event.metaKey || event.ctrlKey) { + const next = chosen.includes(index) + ? chosen.filter((i) => i !== index) + : [...chosen, index].sort((a, b) => a - b); + onSelect(next.length > 0 ? { steps: next } : null); + return; + } + if (event.shiftKey && chosen.length > 0) { + const anchor = chosen[0]; + const [from, to] = anchor <= index ? [anchor, index] : [index, anchor]; + const span = []; + for (let i = from; i <= to; i++) span.push(i); + onSelect({ steps: span }); + return; + } + onSelect({ steps: [index] }); + } + + /** + * Move the selected step(s) one slot along the chain - the keyboard alternative to dragging, which + * pointer-only users cannot reach. Alt with an arrow, so a plain arrow is still free for anything + * that later wants it. The moved block stays selected and takes focus with it, so it can be walked + * several slots in a row. + */ + function moveSelection(direction: "up" | "down"): boolean { + if (chosen.length === 0) return false; + const min = chosen[0]; + const max = chosen[chosen.length - 1]; + // reorderMany's slot is against the original chain: a step's own neighbouring slots are no-ops, + // so up aims one before the block and down one past it. + const slot = direction === "up" ? min - 1 : max + 2; + const order = reorderMany(steps.length, chosen, slot); + if (order === null) return false; // already at that end of the chain + focusStepAfterRender.current = order.indexOf(min); + onReorderSteps(order, chosen); + return true; + } + + /** + * Delete removes whatever is selected: every selected step, or an end of the chain (which returns + * its row to a placeholder, exactly as that node's X does). Alt + Up/Down reorders the selection. + * Scoped to the graph, so typing in the inspector's fields is never intercepted. + */ + function onKeyDown(event: KeyboardEvent) { + if ( + event.altKey && + (event.key === "ArrowUp" || event.key === "ArrowDown") + ) { + // Ends do not reorder (chosen is empty for them), so this only fires for a step selection. + if (moveSelection(event.key === "ArrowUp" ? "up" : "down")) { + event.preventDefault(); + } + return; + } + if (event.key !== "Delete" && event.key !== "Backspace") return; + if (selected === "input" || selected === "output") { + event.preventDefault(); + onRemoveEnd(selected); + return; + } + if (chosen.length === 0) return; + event.preventDefault(); + onRemoveSteps(chosen); + } + + return ( +
    +
    + {edges.map((edge) => ( + + ))} + + {draggingIndex !== null && edges.length > 0 && ( +

    + {t("portal.pipelines.graph.dragHint")} +

    + )} + + {nodes.map((node) => { + const style = { + left: `${node.x}px`, + top: `${node.y}px`, + width: `${NODE_WIDTH}px`, + minHeight: `${NODE_HEIGHT}px`, + }; + if (node.kind === "placeholder") { + return ( +
    + onInsertStep(0)} + /> +
    + ); + } + if (node.kind === "input" || node.kind === "output") { + const kind = node.kind; + const content = kind === "input" ? input : output; + return ( +
    + {content === null ? ( + onAddEnd(kind)} + /> + ) : ( + onSelect(kind)} + onRemove={() => onRemoveEnd(kind)} + /> + )} +
    + ); + } + const index = node.stepIndex ?? 0; + return ( +
    + selectStep(index, event)} + onRemove={() => onRemoveSteps([index])} + onDragChange={(dragging) => + setDraggingIndex(dragging ? index : null) + } + onOpenRunState={ + steps[index].runState === "failed" && onOpenStepError + ? () => onOpenStepError(index) + : undefined + } + /> +
    + ); + })} +
    +
    + ); +} + +interface ChainStepNodeProps { + index: number; + step: GraphStepContent; + selected: boolean; + dragging: boolean; + /** The steps this node's drag carries: the selection when it is part of it, else just itself. */ + moving: number[]; + onSelect: (event: ReactMouseEvent) => void; + onRemove: () => void; + onDragChange: (dragging: boolean) => void; + onOpenRunState?: () => void; +} + +/** A step node plus its drag wiring, which needs a hook per node and so a component per node. */ +function ChainStepNode({ + index, + step, + selected, + dragging, + moving, + onSelect, + onRemove, + onDragChange, + onOpenRunState, +}: ChainStepNodeProps) { + const { ref, guardClick } = useStepDraggable({ moving, onDragChange }); + return ( + + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.test.ts b/frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.test.ts new file mode 100644 index 0000000000..d03e6b3c42 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, test } from "vitest"; +import { + EDGE_LENGTH, + NODE_HEIGHT, + NODE_WIDTH, + layoutChain, + reorderMany, + stepIndexOf, + stepNodeId, +} from "@portal/components/pipelines/graph/pipelineLayout"; + +describe("layoutChain", () => { + test("an empty chain reserves the first step's row for the placeholder", () => { + const { nodes, edges } = layoutChain({ stepCount: 0 }); + expect(nodes.map((n) => n.kind)).toEqual([ + "input", + "placeholder", + "output", + ]); + // The placeholder is the affordance, so neither wire around it offers a plus as well. + expect(edges.map((e) => e.insertIndex)).toEqual([null, null]); + }); + + test("the empty chain is as tall as a one-step chain", () => { + expect(layoutChain({ stepCount: 0 }).height).toBe( + layoutChain({ stepCount: 1 }).height, + ); + }); + + test("steps sit between input and output, in order", () => { + const { nodes } = layoutChain({ stepCount: 3 }); + expect(nodes.map((n) => n.id)).toEqual([ + "input", + "step:0", + "step:1", + "step:2", + "output", + ]); + expect(nodes.map((n) => n.stepIndex)).toEqual([null, 0, 1, 2, null]); + }); + + test("rows are evenly pitched down one column", () => { + const { nodes, width } = layoutChain({ stepCount: 2 }); + expect(nodes.every((n) => n.x === 0)).toBe(true); + const ys = nodes.map((n) => n.y); + const pitch = NODE_HEIGHT + EDGE_LENGTH; + expect(ys).toEqual([0, pitch, pitch * 2, pitch * 3]); + expect(width).toBe(NODE_WIDTH); + }); + + test("the canvas is tall enough for the last node", () => { + const { nodes, height } = layoutChain({ stepCount: 4 }); + const last = nodes[nodes.length - 1]; + expect(height).toBe(last.y + NODE_HEIGHT); + }); + + test("wires span exactly from one node's bottom border to the next node's top", () => { + const { nodes, edges } = layoutChain({ stepCount: 1 }); + expect(edges).toHaveLength(2); + for (const edge of edges) { + expect(edge.x).toBe(NODE_WIDTH / 2); + expect(edge.y2 - edge.y1).toBe(EDGE_LENGTH); + } + expect(edges[0].y1).toBe(nodes[0].y + NODE_HEIGHT); + expect(edges[0].y2).toBe(nodes[1].y); + }); + + test("each wire opens the slot it sits above", () => { + const { edges } = layoutChain({ stepCount: 3 }); + // input->0, 0->1, 1->2, 2->output + expect(edges.map((e) => e.insertIndex)).toEqual([0, 1, 2, 3]); + }); + + test("every wire between real nodes stays open", () => { + // Ordering is the user's to choose: no pairing is refused, however odd it is. + const { edges } = layoutChain({ stepCount: 4 }); + expect(edges.every((e) => e.insertIndex !== null)).toBe(true); + }); +}); + +describe("node ids", () => { + test("step ids round-trip through their index", () => { + expect(stepIndexOf(stepNodeId(7))).toBe(7); + }); + + test("the input and output nodes have no step index", () => { + expect(stepIndexOf("input")).toBeNull(); + expect(stepIndexOf("output")).toBeNull(); + }); +}); + +describe("reorderMany", () => { + test("moving one step below its own place accounts for it lifting out first", () => { + // [a b c], drag a onto the wire above c (slot 2) -> [b a c]. + expect(reorderMany(3, [0], 2)).toEqual([1, 0, 2]); + }); + + test("moving one step above its own place lands on the slot as given", () => { + // [a b c], drag c onto the wire above b (slot 1) -> [a c b]. + expect(reorderMany(3, [2], 1)).toEqual([0, 2, 1]); + }); + + test("the wires either side of a lone step are no-ops", () => { + expect(reorderMany(3, [1], 1)).toBeNull(); + expect(reorderMany(3, [1], 2)).toBeNull(); + }); + + test("moves to either end", () => { + expect(reorderMany(3, [2], 0)).toEqual([2, 0, 1]); + expect(reorderMany(3, [0], 3)).toEqual([1, 2, 0]); + }); + + test("a set of steps lands together, keeping its own order", () => { + // [a b c d], move a+c to the end -> [b d a c]. + expect(reorderMany(4, [0, 2], 4)).toEqual([1, 3, 0, 2]); + }); + + test("a set gathers from apart into one run", () => { + // [a b c d e], move a+e above c (slot 2) -> [b a e c d]. + expect(reorderMany(5, [0, 4], 2)).toEqual([1, 0, 4, 2, 3]); + }); + + test("a contiguous set dropped back where it already is, is a no-op", () => { + expect(reorderMany(4, [1, 2], 1)).toBeNull(); + expect(reorderMany(4, [1, 2], 3)).toBeNull(); + }); + + test("order of the given indices does not matter", () => { + expect(reorderMany(4, [2, 0], 4)).toEqual(reorderMany(4, [0, 2], 4)); + }); + + test("moving every step is a no-op wherever it lands", () => { + expect(reorderMany(3, [0, 1, 2], 0)).toBeNull(); + expect(reorderMany(3, [2, 1, 0], 3)).toBeNull(); + }); + + test("nothing selected moves nothing", () => { + expect(reorderMany(3, [], 1)).toBeNull(); + }); + + test("ignores out-of-range indices rather than injecting undefined steps", () => { + // A stray index (negative or past the end) must be dropped, not carried into the new order. + expect(reorderMany(3, [0, 9], 2)).toEqual([1, 0, 2]); + expect(reorderMany(3, [-1], 2)).toBeNull(); + expect(reorderMany(3, [5], 0)).toBeNull(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.ts b/frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.ts new file mode 100644 index 0000000000..10fadfb6f8 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/pipelineLayout.ts @@ -0,0 +1,184 @@ +/** + * Geometry for the pipeline graph. + * + * A pipeline is a strict sequence - one input, an ordered run of steps, one output - so a node's + * position carries no information that its place in the chain does not already carry. Layout is + * therefore *derived* here on every render rather than owned by the user and persisted: there are + * no stored coordinates to drift, nothing to lock, and nothing to re-tidy. Dragging a node is free + * to mean "move it in the chain" instead of "move it on screen" (see useChainDragDrop). + * + * Everything is a single centred column, which makes each wire a straight vertical line. When the + * model grows past one input/output and a pipeline can branch, x stops being constant and this is + * the module that changes - callers only ever read the result. + */ + +/** + * What a node represents. The chain always has exactly one input and one output. `placeholder` is + * the stand-in shown when a pipeline has no steps yet: it occupies the row the first step will take + * so the chain's shape is visible, and it is the affordance for adding that step. + */ +export type GraphNodeKind = "input" | "step" | "output" | "placeholder"; + +/** Node id: `"input"`, `"output"`, or `"step:"`. Stable for a given chain position. */ +export type GraphNodeId = string; + +export function stepNodeId(index: number): GraphNodeId { + return `step:${index}`; +} + +/** The step index a node id refers to, or null for the input/output nodes. */ +export function stepIndexOf(id: GraphNodeId): number | null { + const match = /^step:(\d+)$/.exec(id); + return match ? Number(match[1]) : null; +} + +export interface LaidOutNode { + id: GraphNodeId; + kind: GraphNodeKind; + /** Position in the chain's step list; null for input/output. */ + stepIndex: number | null; + /** Top-left corner, in canvas coordinates. */ + x: number; + y: number; +} + +export interface LaidOutEdge { + id: string; + from: GraphNodeId; + to: GraphNodeId; + /** + * Where a step dropped on this wire lands in the step list. Null only for the wires either side + * of the placeholder, which is itself the affordance for adding the first step. + */ + insertIndex: number | null; + /** Straight vertical wire, from the upper node's bottom port to the lower node's top port. */ + x: number; + y1: number; + y2: number; +} + +export interface LaidOutChain { + nodes: LaidOutNode[]; + edges: LaidOutEdge[]; + /** Canvas extent, so the scroll container can size itself without measuring. */ + width: number; + height: number; +} + +/** Node box, and the vertical room a wire plus its insert affordance needs between two of them. */ +export const NODE_WIDTH = 260; +export const NODE_HEIGHT = 64; +export const EDGE_LENGTH = 48; + +const ROW_PITCH = NODE_HEIGHT + EDGE_LENGTH; + +export interface LayoutChainOptions { + stepCount: number; +} + +/** + * Lay the chain out top to bottom: input, each step in order, output. Rows are evenly pitched and + * share one x, so wires are vertical and always aligned. + */ +export function layoutChain({ stepCount }: LayoutChainOptions): LaidOutChain { + const nodes: LaidOutNode[] = []; + const row = (index: number) => index * ROW_PITCH; + // An empty pipeline still shows a step row, filled by the placeholder, so the chain reads as + // input -> something -> output rather than as a bare wire. + const rows = Math.max(stepCount, 1); + + nodes.push({ id: "input", kind: "input", stepIndex: null, x: 0, y: row(0) }); + if (stepCount === 0) { + nodes.push({ + id: "placeholder", + kind: "placeholder", + stepIndex: null, + x: 0, + y: row(1), + }); + } + for (let i = 0; i < stepCount; i++) { + nodes.push({ + id: stepNodeId(i), + kind: "step", + stepIndex: i, + x: 0, + y: row(i + 1), + }); + } + nodes.push({ + id: "output", + kind: "output", + stepIndex: null, + x: 0, + y: row(rows + 1), + }); + + const centreX = NODE_WIDTH / 2; + const edges: LaidOutEdge[] = []; + for (let i = 0; i < nodes.length - 1; i++) { + const upper = nodes[i]; + const lower = nodes[i + 1]; + // A wire's insert index is the step slot it sits above: the wire below the input opens slot 0, + // the wire below step i opens slot i+1. A final-only step closes the wire beneath it. + const above = upper.stepIndex; + // The placeholder is itself the "add the first step" affordance, so the wires either side of it + // stay plain - two pluses for the same slot would be a choice with no difference. + const placeholderRow = + upper.kind === "placeholder" || lower.kind === "placeholder"; + const insertIndex = placeholderRow ? null : (above ?? -1) + 1; + edges.push({ + id: `${upper.id}->${lower.id}`, + from: upper.id, + to: lower.id, + insertIndex, + x: centreX, + // Exactly node-bottom to node-top: the wire meets both borders. The arrowhead is kept inside + // this box (see GraphEdge.css) so the node, drawn after it, cannot paint over the tip. + y1: upper.y + NODE_HEIGHT, + y2: lower.y, + }); + } + + return { + nodes, + edges, + width: NODE_WIDTH, + height: row(rows + 1) + NODE_HEIGHT, + }; +} + +/** + * The chain's new order after dropping `moving` on the wire that opens `insertIndex`. + * + * Returns the original step indices in their new positions, or null when the move changes nothing + * (dropping a step on either of its own wires, say). The moved steps land together in the target + * slot, keeping their order relative to each other; the slot is expressed against the *original* + * chain, so lifting the moved steps out first has to be accounted for - which is done by counting + * how many of the steps that stay put sit above the slot. + */ +export function reorderMany( + stepCount: number, + moving: readonly number[], + insertIndex: number, +): number[] | null { + // Range-check: a stray index would survive into `next` and then read as an undefined step, so + // keep only positions that exist in the chain before lifting anything out. + const lifted = [...new Set(moving)] + .filter((i) => i >= 0 && i < stepCount) + .sort((a, b) => a - b); + if (lifted.length === 0) return null; + + const staying: number[] = []; + for (let i = 0; i < stepCount; i++) { + if (!lifted.includes(i)) staying.push(i); + } + + const landing = staying.filter((i) => i < insertIndex).length; + const next = [ + ...staying.slice(0, landing), + ...lifted, + ...staying.slice(landing), + ]; + return next.every((value, i) => value === i) ? null : next; +} diff --git a/frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.test.ts b/frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.test.ts new file mode 100644 index 0000000000..1816f1a0ca --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.test.ts @@ -0,0 +1,91 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + createDragClickGuard, + fillDragPreview, +} from "@portal/components/pipelines/graph/useChainDragDrop"; + +/** Stands in for the graph's rendered cards, which the preview clones out of the DOM. */ +function renderCards(labels: string[]) { + document.body.innerHTML = labels + .map( + (label, i) => + `
    ${label}
    `, + ) + .join(""); +} + +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("fillDragPreview", () => { + it("stacks a copy of every dragged card, in the order given", () => { + renderCards(["OCR", "Redact", "Compress"]); + const container = document.createElement("div"); + fillDragPreview(container, [0, 2]); + expect([...container.children].map((c) => c.textContent)).toEqual([ + "OCR", + "Compress", + ]); + }); + + it("leaves the originals alone", () => { + renderCards(["OCR", "Redact"]); + fillDragPreview(document.createElement("div"), [0, 1]); + expect(document.querySelectorAll("[data-step-index]")).toHaveLength(2); + }); + + it("copies are solid, not dimmed like the cards they came from", () => { + renderCards(["OCR"]); + const container = document.createElement("div"); + fillDragPreview(container, [0]); + expect(document.querySelector("[data-step-index]")).toHaveClass( + "is-dragging", + ); + expect(container.firstElementChild).not.toHaveClass("is-dragging"); + }); + + it("skips an index with no card rather than throwing", () => { + renderCards(["OCR"]); + const container = document.createElement("div"); + fillDragPreview(container, [0, 7]); + expect(container.children).toHaveLength(1); + }); +}); + +// The drag itself needs native HTML5 drag events, which jsdom does not implement, so the guard's +// state machine is exercised here directly - it is the half that decides whether a click selects. +describe("createDragClickGuard", () => { + it("lets a plain press through", () => { + const guard = createDragClickGuard(); + guard.beginGesture(); + expect(guard.swallowsClick()).toBe(false); + }); + + it("swallows the click that trails a drag", () => { + const guard = createDragClickGuard(); + guard.beginGesture(); + guard.noteDrag(); + expect(guard.swallowsClick()).toBe(true); + }); + + it("still selects on the next press after a drag left no click behind", () => { + // The regression: native drag usually emits no trailing click, so a guard cleared only by + // consuming one stayed raised and ate the user's next real click on that node. + const guard = createDragClickGuard(); + guard.beginGesture(); + guard.noteDrag(); + // ...drop, and no click follows. + guard.beginGesture(); + expect(guard.swallowsClick()).toBe(false); + }); + + it("guards each drag, not just the first", () => { + const guard = createDragClickGuard(); + for (const _ of [1, 2]) { + guard.beginGesture(); + guard.noteDrag(); + expect(guard.swallowsClick()).toBe(true); + } + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.ts b/frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.ts new file mode 100644 index 0000000000..f80949d077 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/graph/useChainDragDrop.ts @@ -0,0 +1,230 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { + draggable, + dropTargetForElements, +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; +import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source"; +import { + NODE_WIDTH, + reorderMany, +} from "@portal/components/pipelines/graph/pipelineLayout"; + +/** + * Drag-to-reorder for the pipeline chain. + * + * The chain is a sequence, so a step's meaningful move is "somewhere else in the order" - which + * makes the *wires* the drop targets, not the nodes. Each wire already knows the slot it opens + * (see layoutChain), so a drop is one call to reorderMany with that slot; there is no midpoint + * arithmetic or above/below bookkeeping, and no free coordinates to store. + */ + +const DRAG_TYPE = "pipeline-step"; + +interface StepDragData extends Record { + type: typeof DRAG_TYPE; + /** Every step this drag carries, in chain order - one, or the whole selection. */ + moving: number[]; +} + +function isStepDrag(data: Record): data is StepDragData { + return data.type === DRAG_TYPE && Array.isArray(data.moving); +} + +export interface UseStepDraggableOptions { + /** + * The steps this node's drag should carry: the current selection when this node is part of it, + * otherwise just itself. Resolved by the graph, which is what knows the selection. + */ + moving: number[]; + /** Told when this step's drag starts and ends, so the graph can light up the wires. */ + onDragChange: (dragging: boolean) => void; +} + +export interface UseStepDraggableResult { + ref: React.RefObject; + /** + * Wraps the node's click so the click that can trail a drag does not also select. Native drag + * usually swallows it, but the page editor carries the same guard - cheap insurance. + */ + guardClick: (action: (event: E) => void) => (event: E) => void; +} + +/** + * Tells a click that trails a drag apart from a genuine one. + * + * A gesture begins on pointerdown and may turn into a drag; only a click belonging to a gesture + * that dragged is swallowed. The clearing has to happen when the *next* gesture begins rather than + * when a click is swallowed - native HTML5 drag usually leaves no trailing click at all, so a flag + * cleared only by consuming one stays raised and eats the user's next real click on that node. + */ +export function createDragClickGuard() { + let dragged = false; + return { + /** A new press has started; nothing has dragged yet. */ + beginGesture: () => { + dragged = false; + }, + /** This gesture became a drag. */ + noteDrag: () => { + dragged = true; + }, + /** True if a click arriving now is the tail of a drag rather than a plain press. */ + swallowsClick: () => dragged, + }; +} + +export type DragClickGuard = ReturnType; + +/** + * Stack a copy of every dragged card into the preview container, so a multi-step drag shows what is + * actually moving rather than only the card that was grabbed. Exported for testing; the cards are + * found in the DOM by their chain position. + */ +export function fillDragPreview( + container: HTMLElement, + moving: readonly number[], +): void { + container.className = "portal-graph__drag-preview"; + container.style.width = `${NODE_WIDTH}px`; + for (const index of moving) { + const card = document.querySelector(`[data-step-index="${index}"]`); + if (!card) continue; + const copy = card.cloneNode(true) as HTMLElement; + // The originals dim once the drag starts; the copies are the drag, so they stay solid. + copy.classList.remove("is-dragging"); + container.appendChild(copy); + } +} + +/** Makes one step node draggable, tagged with the chain position it started from. */ +export function useStepDraggable({ + moving, + onDragChange, +}: UseStepDraggableOptions): UseStepDraggableResult { + const ref = useRef(null); + const guardRef = useRef(null); + guardRef.current ??= createDragClickGuard(); + const guard = guardRef.current; + + // Read through refs so a reorder (which renumbers every later step) never re-registers the + // adapter mid-gesture. + const movingRef = useRef(moving); + movingRef.current = moving; + const onDragChangeRef = useRef(onDragChange); + onDragChangeRef.current = onDragChange; + + useEffect(() => { + const element = ref.current; + if (!element) return; + // Any fresh input on the node starts a new gesture and clears the guard, so the only click it + // ever swallows is one trailing that same gesture's drag. Keyboard counts: activating the card + // with Enter or Space produces a click with no pointerdown before it. + const startGesture = () => guard.beginGesture(); + element.addEventListener("pointerdown", startGesture); + element.addEventListener("keydown", startGesture); + const stopDraggable = draggable({ + element, + getInitialData: (): StepDragData => ({ + type: DRAG_TYPE, + moving: movingRef.current, + }), + onGenerateDragPreview: ({ location, nativeSetDragImage }) => { + const moving = movingRef.current; + // One step drags as itself; the browser's own preview of the grabbed card is right. A set + // needs to show what is actually moving, so the preview stacks a copy of every card. + if (moving.length < 2) return; + setCustomNativeDragPreview({ + nativeSetDragImage, + getOffset: preserveOffsetOnSource({ + element, + input: location.current.input, + }), + render: ({ container }) => fillDragPreview(container, moving), + }); + }, + onDragStart: () => { + guard.noteDrag(); + onDragChangeRef.current(true); + }, + onDrop: () => onDragChangeRef.current(false), + }); + return () => { + element.removeEventListener("pointerdown", startGesture); + element.removeEventListener("keydown", startGesture); + stopDraggable(); + }; + }, [guard]); + + const guardClick = useCallback( + (action: (event: E) => void) => + (event: E) => { + if (guard.swallowsClick()) return; + action(event); + }, + [guard], + ); + + return { ref, guardClick }; +} + +export interface UseEdgeDropOptions { + /** The slot this wire opens; null for the wires either side of the empty-chain placeholder. */ + insertIndex: number | null; + stepCount: number; + /** + * Given the chain's new order as original step indices, and the original indices of the steps the + * drag actually carried - so the caller can keep the dragged steps selected rather than guessing + * from the prior selection. + */ + onReorder: (order: number[], moved: readonly number[]) => void; +} + +export interface UseEdgeDropResult { + ref: React.RefObject; + /** A step is hovering this wire and would land here. */ + over: boolean; +} + +/** Makes one wire a drop target that moves the dropped step into the slot the wire opens. */ +export function useEdgeDrop({ + insertIndex, + stepCount, + onReorder, +}: UseEdgeDropOptions): UseEdgeDropResult { + const ref = useRef(null); + const [over, setOver] = useState(false); + + const insertIndexRef = useRef(insertIndex); + insertIndexRef.current = insertIndex; + const stepCountRef = useRef(stepCount); + stepCountRef.current = stepCount; + const onReorderRef = useRef(onReorder); + onReorderRef.current = onReorder; + + useEffect(() => { + const element = ref.current; + if (!element) return; + return dropTargetForElements({ + element, + // A wire with no slot is not a target at all, so a step dragged over it shows no landing spot. + canDrop: ({ source }) => + insertIndexRef.current !== null && isStepDrag(source.data), + onDragEnter: () => setOver(true), + onDragLeave: () => setOver(false), + onDrop: ({ source }) => { + setOver(false); + const slot = insertIndexRef.current; + if (slot === null || !isStepDrag(source.data)) return; + const order = reorderMany( + stepCountRef.current, + source.data.moving, + slot, + ); + if (order !== null) onReorderRef.current(order, source.data.moving); + }, + }); + }, []); + + return { ref, over }; +} diff --git a/frontend/editor/src/portal/mocks/handlers/pipelines.ts b/frontend/editor/src/portal/mocks/handlers/pipelines.ts index f4c085ade7..0158d1e168 100644 --- a/frontend/editor/src/portal/mocks/handlers/pipelines.ts +++ b/frontend/editor/src/portal/mocks/handlers/pipelines.ts @@ -69,6 +69,29 @@ function seedPipelines(): StoredPolicy[] { output: { type: "inline", options: {} }, outputIds: ["src-contracts"], }, + { + // A chain long enough to overflow the builder's graph column, which is where the graph has to + // start scrolling instead of pushing the inspector off the page. + id: "plc-long", + name: "Full document pipeline", + owner: "ops@acme.com", + enabled: true, + inputs: [{ sourceId: "src-claims", trigger: null }], + steps: [ + { operation: "/api/v1/misc/repair", parameters: {} }, + { operation: "/api/v1/misc/ocr-pdf", parameters: {} }, + { operation: "/api/v1/general/rotate-pdf", parameters: {} }, + { operation: "/api/v1/general/crop", parameters: {} }, + { operation: "/api/v1/general/remove-pages", parameters: {} }, + { operation: "/api/v1/misc/add-page-numbers", parameters: {} }, + { operation: "/api/v1/security/add-watermark", parameters: {} }, + { operation: "/api/v1/security/sanitize-pdf", parameters: {} }, + { operation: "/api/v1/misc/flatten", parameters: {} }, + { operation: "/api/v1/misc/compress-pdf", parameters: {} }, + ], + output: { type: "inline", options: {} }, + outputIds: ["src-archive"], + }, { id: "plc-onboarding", name: "Onboarding OCR (paused)", diff --git a/frontend/editor/src/portal/views/PipelineBuilder.css b/frontend/editor/src/portal/views/PipelineBuilder.css index 128e8d4a1b..464d3fad55 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.css +++ b/frontend/editor/src/portal/views/PipelineBuilder.css @@ -1,3 +1,13 @@ +/** + * The builder claims the shell's view rather than lengthening it, so a long chain scrolls *inside* + * the graph while the header and the inspector stay put. A chain grows 112px per step, so it passes + * a typical viewport at around five steps - and if the page scrolled instead, clicking a node near + * the bottom would put the inspector (and Save) off-screen, which is the one interaction this whole + * layout exists to serve. + * + * The shell already makes .portal-shell__view the scroll container, so height here resolves against + * a definite box; capping the columns is what stops that view scrolling at all. + */ .portal-builder { display: flex; flex-direction: column; @@ -5,6 +15,14 @@ padding: 1.5rem; max-width: 84rem; margin: 0 auto; + height: 100%; + min-height: 0; +} + +/* The header and any banners keep their own size; only the grid absorbs (or gives up) space. Left + to the flex default they would all shrink together and squash on a short viewport. */ +.portal-builder > *:not(.portal-builder__grid) { + flex: none; } .portal-builder__loading { @@ -13,254 +31,38 @@ padding: 4rem 0; } -/* Header */ -.portal-builder__head { - display: flex; - align-items: center; - gap: 0.75rem; - flex-wrap: wrap; - padding-bottom: 1rem; - border-bottom: 1px solid var(--c-border-subtle); -} - -.portal-builder__back { - display: inline-flex; - align-items: center; - gap: 0.25rem; - border: none; - background: none; - padding: 0; - font-size: 0.8125rem; - color: var(--c-text-subtle); - cursor: pointer; - white-space: nowrap; -} - -.portal-builder__back:hover { - color: var(--c-text); -} - -.portal-builder__head-main { - flex: 1; - min-width: 12rem; -} - -.portal-builder__head-actions { - display: flex; - align-items: center; - gap: 0.75rem; -} - /* Two-pane layout */ .portal-builder__grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1.25rem; + /* Takes whatever the header leaves, and pins the row to exactly that. `minmax(0, 1fr)` rather + than the implicit `auto` row is what makes the columns' `max-height: 100%` mean anything: an + auto row is sized BY its tallest item, so a long settings form would size the row to itself and + then resolve its own 100% against it - capping nothing, and clipping the form with no scrollbar. + `align-items: start` still lets a short column hug its content inside the bounded row. */ + grid-template-rows: minmax(0, 1fr); + flex: 1 1 auto; + min-height: 0; align-items: start; } +/* Stacked, the inspector sits below the graph, so there is nothing to hold in view - and capping + here would nest a scroll region inside a scrolling page, which is worse than a long page. Let the + builder grow and hand scrolling back to the shell. */ @media (max-width: 60rem) { + .portal-builder { + height: auto; + } + .portal-builder__grid { grid-template-columns: 1fr; + grid-template-rows: auto; + flex: none; } } -.portal-builder__flow { - display: flex; - flex-direction: column; - gap: 0.625rem; -} - -.portal-builder__section-label { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--c-text-subtle); - font-weight: 600; -} - -.portal-builder__empty { - font-size: 0.8125rem; - color: var(--c-text-subtle); - margin: 0; - padding: 0.5rem 0; -} - -/* Step cards */ -.portal-builder__steps { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.portal-builder__step { - display: flex; - align-items: center; - gap: 0.5rem; - background: var(--c-surface); - border: 1px solid var(--c-border-subtle); - border-radius: var(--radius-lg); - padding: 0.5rem 0.625rem; - transition: - border-color var(--motion-fast), - background var(--motion-fast); -} - -.portal-builder__step--active { - border-color: var(--c-primary); - background: var(--c-primary-tint); -} - -.portal-builder__step-main { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - gap: 0.625rem; - border: none; - background: none; - padding: 0.25rem; - text-align: left; - cursor: pointer; - color: inherit; -} - -.portal-builder__step-index { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.375rem; - height: 1.375rem; - flex-shrink: 0; - border-radius: 50%; - font-size: 0.6875rem; - font-weight: 600; - background: var(--c-primary-tint); - color: var(--c-primary); -} - -.portal-builder__step--active .portal-builder__step-index { - background: var(--c-primary); - color: #fff; -} - -.portal-builder__step-text { - display: flex; - flex-direction: column; - min-width: 0; -} - -.portal-builder__step-name { - font-size: 0.875rem; - font-weight: 500; - color: var(--c-text); -} - -.portal-builder__step-note { - font-size: 0.6875rem; - color: var(--c-text-subtle); -} - -/* A step that cannot run on what the one before it produces. */ -.portal-builder__step-note--danger { - color: var(--c-danger); -} - -/* A picker entry that cannot run on what the chain currently produces. */ -.portal-pipelines__picker-note { - margin-left: auto; - padding-left: 0.5rem; - font-size: 0.6875rem; - color: var(--c-text-subtle); -} - -.portal-builder__step-actions { - display: flex; - gap: 0.25rem; -} - -.portal-builder__step-actions button { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.5rem; - height: 1.5rem; - padding: 0; - border-radius: var(--radius-md); - border: 1px solid var(--c-border); - background: var(--c-surface); - color: var(--c-text-subtle); - cursor: pointer; - transition: - background var(--motion-fast), - color var(--motion-fast); -} - -.portal-builder__step-actions button:hover:not(:disabled) { - background: var(--c-hover); - color: var(--c-text); -} - -.portal-builder__step-actions button:disabled { - opacity: 0.4; - cursor: default; -} - -.portal-builder__add-step { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.375rem; - width: 100%; - padding: 0.625rem; - border: 1px dashed var(--c-border); - border-radius: var(--radius-lg); - background: none; - color: var(--c-text-subtle); - font-size: 0.8125rem; - cursor: pointer; - transition: - border-color var(--motion-fast), - color var(--motion-fast); -} - -.portal-builder__add-step:hover { - border-color: var(--c-primary); - color: var(--c-primary); -} - -/* Pipeline settings (above the operation list) */ -.portal-builder__settings { - display: flex; - flex-direction: column; - gap: 0.75rem; - background: var(--color-bg-subtle); - border: 1px solid var(--c-border-subtle); - border-radius: var(--radius-lg); - padding: 1.125rem; -} - -.portal-builder__settings-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); - gap: 1.25rem; -} - -.portal-builder__settings-col { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -/* Input and destination each span the full settings width so their row has room. */ -.portal-builder__inputs-col { - grid-column: 1 / -1; -} - -/* The input row (source + trigger + optional schedule) and the destination row. */ +/* The input's source dropdown and its edit affordance, in the inspector. */ .portal-builder__input-row { display: flex; flex-wrap: wrap; @@ -273,58 +75,23 @@ min-width: 10rem; } -/* The connect-source button trails to the end of the row. */ -.portal-builder__input-row > button:last-child { - margin-left: auto; -} - -/* Inspector: heading sits outside the card so it aligns with the operations heading. */ -.portal-builder__inspector-col { - position: sticky; - top: 1rem; - display: flex; - flex-direction: column; - gap: 0.625rem; -} - -/* Once the grid stacks (60rem), a sticky inspector would ride over content */ -@media (max-width: 60rem) { - .portal-builder__inspector-col { - position: static; - } -} - -.portal-builder__inspector { - background: var(--c-surface); - border: 1px solid var(--c-border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); - padding: 1.125rem; -} - /* Tool picker */ -.portal-pipelines__picker { - border: 1px solid var(--c-border); - border-radius: var(--radius-lg); - background: var(--c-surface); - overflow: hidden; +/* The picker fills the modal hosting it rather than sitting in a card of its own: same surface, + same radius, so a bordered box in here would frame nothing. Its rows carry the structure, and + they run to the panel's edges - which is what lets the search divider span the full width. */ +.portal-pipelines__picker-modal .sui-modal__body { + padding: 0; } +/* Holds the shared Input, which brings its own border/focus ring; the row just insets it and rules + it off from the list below. */ .portal-pipelines__picker-search { - display: flex; - align-items: center; - padding: 0.5rem 0.75rem; + padding: 0.75rem 0.75rem 0.625rem; border-bottom: 1px solid var(--c-border-subtle); } -.portal-pipelines__picker-search input { - flex: 1; - border: none; - background: none; - padding: 0; - font-size: 0.875rem; - color: var(--c-text); - outline: none; +.portal-pipelines__picker-search .sui-input { + width: 100%; } .portal-pipelines__picker-list { @@ -334,7 +101,7 @@ } .portal-pipelines__picker-group-label { - padding: 0.5rem 0.75rem 0.25rem; + padding: 0.5rem 1.125rem 0.25rem; font-size: 0.6875rem; color: var(--c-text-subtle); } @@ -346,7 +113,7 @@ width: 100%; border: none; background: none; - padding: 0.4375rem 0.75rem; + padding: 0.4375rem 1.125rem; text-align: left; cursor: pointer; color: var(--c-text); @@ -368,63 +135,52 @@ display: block; } +/* Name over its optional note - a two-line item, so the note reads as a sub-line rather than + running straight on from the name. */ +.portal-pipelines__picker-text { + display: flex; + flex-direction: column; + gap: 0.0625rem; + min-width: 0; +} + .portal-pipelines__picker-name { font-size: 0.8125rem; } +/* Why this tool cannot follow the step before it. Advisory: the item is still pickable, just dimmed + and captioned so the reason is clear without shouting. */ +.portal-pipelines__picker-note { + font-size: 0.6875rem; + color: var(--c-text-muted); + white-space: normal; + line-height: 1.3; +} + +/* Muted via a token, not opacity: opacity on text drops the contrast below the floor. The name + still reads as de-emphasised, and the icon (not text) can take the opacity. */ +.portal-pipelines__picker-item.is-incompatible .portal-pipelines__picker-name { + color: var(--c-text-muted); +} + +.portal-pipelines__picker-item.is-incompatible .portal-pipelines__picker-icon { + opacity: 0.55; +} + .portal-pipelines__picker-empty { - padding: 1rem 0.75rem; + padding: 1rem 1.125rem; font-size: 0.8125rem; color: var(--c-text-subtle); margin: 0; } -/* The back link, step row, add-step affordance, tool-picker item and step - actions are the shared Button/ActionIcon carrying bespoke styling. Re-assert - their original look over the design-system button base (which otherwise - imposes a fixed height, its own padding/border and accent text colour). */ -.portal-builder__back.sui-btn { - height: auto; - min-height: 0; - padding: 0; - font-weight: 400; - font-size: 0.8125rem; - color: var(--c-text-subtle); -} - -.portal-builder__back.sui-btn:hover { - color: var(--c-text); -} - -.portal-builder__step-main.sui-btn { - flex: 1; - height: auto; - min-height: 0; - padding: 0.25rem; - font-weight: 400; - color: inherit; -} - -.portal-builder__add-step.sui-btn { - height: auto; - min-height: 0; - padding: 0.625rem; - border: 1px dashed var(--c-border); - background: none; - font-weight: 400; - font-size: 0.8125rem; - color: var(--c-text-subtle); -} - -.portal-builder__add-step.sui-btn:hover { - border-color: var(--c-primary); - color: var(--c-primary); -} - +/* The tool-picker item is the shared Button carrying bespoke styling, so re-assert its look over + the design-system base (which otherwise imposes a fixed height, its own padding and an accent + text colour). */ .portal-pipelines__picker-item.sui-btn { height: auto; min-height: 0; - padding: 0.4375rem 0.75rem; + padding: 0.4375rem 1.125rem; font-weight: 400; color: var(--c-text); } @@ -433,9 +189,39 @@ background: var(--c-hover); } -.portal-builder__step-actions .sui-ai { - width: 1.5rem; - height: 1.5rem; - min-width: 1.5rem; - min-height: 1.5rem; +/* A quiet way into the chosen source's own settings, beside its dropdown. */ +.portal-builder__input-row .portal-builder__source-edit { + color: var(--c-text-subtle); +} + +.portal-builder__input-row .portal-builder__source-edit:hover:not(:disabled) { + color: var(--c-accent-fg, var(--c-primary)); +} + +/* The input's schedule row, its number field, and a builder-owned muted line. These lived in + Pipelines.css and only rendered because the router bundles both views together; a code-split (or + any Storybook story of the builder alone) left them unstyled. Kept here so the builder is + self-contained. */ +.portal-builder__schedule { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.portal-builder__schedule-count { + width: 5rem; +} + +.portal-builder__muted { + font-size: 0.8125rem; + color: var(--c-text-subtle); + margin: 0; +} + +/* The space-between button row shared by the builder's modal footers. */ +.portal-builder__composer-footer { + display: flex; + justify-content: space-between; + gap: 0.5rem; + width: 100%; } diff --git a/frontend/editor/src/portal/views/PipelineBuilder.stories.tsx b/frontend/editor/src/portal/views/PipelineBuilder.stories.tsx index 95d2987e52..c204721c38 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.stories.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.stories.tsx @@ -22,10 +22,21 @@ function withRoute(path: string) { const meta: Meta = { title: "Portal/Views/PipelineBuilder", component: PipelineBuilder, - parameters: { layout: "padded" }, - // The builder reads the tool registry (for step labels + settings UIs), so - // it needs this provider to render at all. + parameters: { layout: "fullscreen" }, decorators: [ + // The builder sizes itself against the shell's view - a fixed-height, non-scrolling box - which + // is what lets it cap its columns instead of lengthening the page. Given an auto-height parent + // its `height: 100%` resolves to nothing and the cap silently stops applying, so the story has + // to honour that contract or it reviews a layout the app never renders. + // Matches .portal-shell__view: a definite height with `auto` overflow, so the capped desktop + // layout has something to size against and the stacked layout can still scroll. + (Story) => ( +
    + +
    + ), + // The builder reads the tool registry (for step labels + settings UIs), so + // it needs this provider to render at all. (Story) => ( @@ -45,3 +56,12 @@ export const Default: Story = { export const Edit: Story = { decorators: [withRoute("/processor/pipelines/plc-redaction")], }; + +/** + * A chain taller than the page. The graph column scrolls on its own so the header and the inspector + * stay where they are - if the page scrolled instead, selecting a step near the end of the chain + * would carry its settings off-screen. + */ +export const LongChain: Story = { + decorators: [withRoute("/processor/pipelines/plc-long")], +}; diff --git a/frontend/editor/src/portal/views/PipelineBuilder.test.tsx b/frontend/editor/src/portal/views/PipelineBuilder.test.tsx index e06025889e..24d215a99f 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.test.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.test.tsx @@ -7,6 +7,8 @@ import { } from "@testing-library/react"; import { PortalTestProviders } from "@portal/test/TestQueryProvider"; import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; +import { qk } from "@portal/queries/keys"; import type { Policy, TriggerOutcome } from "@portal/api/pipelines"; import type { SourceView } from "@portal/api/sources"; import type { ToolRegistryCatalog } from "@app/contexts/ToolRegistryContext"; @@ -61,22 +63,63 @@ vi.mock("@portal/api/integrations", () => ({ createIntegration: (...args: unknown[]) => createIntegration(...args), })); -// The destination picker just selects saved sources; stub it to a button that -// picks a fixed source, keeping this suite focused on the builder. +// The destination picker just selects saved sources; stub its three affordances +// (pick, create, edit) to buttons, keeping this suite focused on the builder. vi.mock("@portal/components/pipelines/DestinationPicker", () => ({ DestinationPicker: ({ value, onChange, + onCreateNew, + onEdit, }: { value: string[]; onChange: (ids: string[]) => void; + onCreateNew: () => void; + onEdit: (sourceId: string) => void; }) => ( - + <> + + + + ), })); +// The source modal has its own suite; stub it to the two things the builder +// depends on - the record it was opened on, and the sources-cache invalidation +// that follows a save (which is how a new source reaches the pickers). +vi.mock("@portal/components/sources/SourceModal", () => ({ + SourceModal: ({ + open, + sourceId, + }: { + open: boolean; + sourceId?: string | null; + }) => { + const queryClient = useQueryClient(); + if (!open) return null; + return ( +
    + source-modal:{sourceId || "new"} + +
    + ); + }, +})); + // One editable tool, Compress, so the picker and step settings have something to render. vi.mock("@app/contexts/ToolRegistryContext", () => { const compress = { @@ -129,9 +172,41 @@ vi.mock("@app/contexts/ToolRegistryContext", () => { fromApiParams: (params: Record) => ({ ...params }), }, } as unknown as ToolRegistryEntry; + // A tool that will not run on its defaults: its validateParams is the same predicate its own Run + // button uses, so a step for it is "unconfigured" until a language is chosen. + const ocr = { + name: "OCR", + icon: null, + component: null, + description: "", + categoryId: "recommendedTools", + subcategoryId: "general", + automationSettings: (props: { + onParameterChange: (key: string, value: unknown) => void; + }) => ( + + ), + operationConfig: { + operationType: "ocr", + toolType: 0, + endpoint: "/api/v1/misc/ocr-pdf", + defaultParameters: { languages: [] }, + validateParams: (params: { languages?: string[] }) => + (params.languages ?? []).length > 0, + buildFormData: () => new FormData(), + toApiParams: (params: Record) => ({ ...params }), + fromApiParams: (params: Record) => ({ ...params }), + }, + } as unknown as ToolRegistryEntry; const allTools = { compress, extractImages, + ocr, } as unknown as ToolRegistryCatalog["allTools"]; const catalog: ToolRegistryCatalog = { regularTools: allTools, @@ -215,8 +290,44 @@ describe("PipelineBuilder", () => { createIntegration.mockReset(); }); - // Choose the given source in the (pre-seeded) input row's dropdown. + // The settings of a node are reached by selecting it in the graph, so every helper below opens + // its node first. Nodes are found by position rather than by label, because a node's title is + // its current value - it changes as the pipeline is filled in. + + /** The graph's selectable nodes, in chain order: input, each step, output. */ + function graphNodes(): HTMLElement[] { + return screen + .getAllByRole("button") + .filter((b) => b.hasAttribute("aria-pressed")); + } + + /** + * The control that opens an end of the chain, once the graph has rendered. A new pipeline has not + * placed its ends yet, so that control is the "add" placeholder and clicking it both puts the node + * on the chain and selects it; a loaded pipeline already has the node, so it is a plain select. + */ + function endOpener(end: "input" | "output"): Promise { + return waitFor(() => { + const placeholder = screen.queryByText( + `portal.pipelines.graph.add.${end}`, + ); + if (placeholder) return placeholder; + const nodes = graphNodes(); + if (nodes.length === 0) throw new Error("the graph has not rendered yet"); + return end === "input" ? nodes[0] : nodes[nodes.length - 1]; + }); + } + + async function openInput() { + fireEvent.click(await endOpener("input")); + } + + async function openOutput() { + fireEvent.click(await endOpener("output")); + } + async function pickInputSource(sourceName: string) { + await openInput(); fireEvent.click( await screen.findByRole("textbox", { name: "portal.pipelines.builder.inputSource", @@ -225,24 +336,147 @@ describe("PipelineBuilder", () => { fireEvent.click(await screen.findByText(sourceName)); } - it("always shows exactly one input row, with no add or remove controls", async () => { + /** + * Add a tool. An empty chain offers the placeholder; once it has steps, the wires carry the + * inserts instead. + */ + async function addTool(toolName: string) { + const placeholder = screen.queryByText( + "portal.pipelines.graph.addFirstTool", + ); + if (placeholder) { + fireEvent.click(placeholder); + } else { + // The LAST wire, so repeated calls append. Taking the first would insert each new tool ahead + // of the ones already there, silently reversing the order a caller asked for. + const inserts = screen.getAllByLabelText( + "portal.pipelines.graph.insertHere", + ); + fireEvent.click(inserts[inserts.length - 1]); + } + fireEvent.click(await screen.findByText(toolName)); + } + + async function pickDestination() { + await openOutput(); + fireEvent.click(await screen.findByText("pick output")); + } + + /** Open the header's overflow tray. */ + async function openTray() { + fireEvent.click( + await screen.findByLabelText("portal.pipelines.builder.moreActions"), + ); + } + + it("greets a new pipeline with places to fill, not problems to fix", async () => { renderBuilder("/processor/pipelines/new"); - // The input row is a fixed part of the form: its source dropdown is present from the - // start, and there is nothing to add or remove. + // Both ends offer to be added rather than complaining about being empty: the user has not been + // asked for a source or a destination yet, so there is nothing yet to warn them about. expect( - await screen.findAllByRole("textbox", { + await screen.findByText("portal.pipelines.graph.add.input"), + ).toBeInTheDocument(); + expect( + screen.getByText("portal.pipelines.graph.add.output"), + ).toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.builder.needsSource"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.builder.needsDestination"), + ).not.toBeInTheDocument(); + // Nothing is on the chain, so there is nothing to remove either. + expect( + screen.queryByLabelText(/portal.pipelines.graph.removeNode/), + ).not.toBeInTheDocument(); + }); + + it("warns on a step whose tool cannot run on its defaults", async () => { + renderBuilder("/processor/pipelines/new"); + await addTool("OCR"); + + // The tool declares its own mandatory parameters, so the node says so without the builder + // knowing anything about OCR. + expect( + await screen.findByText("portal.pipelines.builder.needsConfiguring"), + ).toBeInTheDocument(); + expect( + screen.getByText("portal.pipelines.composer.create").closest("button"), + ).toBeDisabled(); + }); + + it("clears the warning once the step is configured", async () => { + renderBuilder("/processor/pipelines/new"); + await addTool("OCR"); + await screen.findByText("portal.pipelines.builder.needsConfiguring"); + + // Adding a step selects it, so its settings are already open. + fireEvent.click(screen.getByText("pick language")); + + await waitFor(() => + expect( + screen.queryByText("portal.pipelines.builder.needsConfiguring"), + ).not.toBeInTheDocument(), + ); + }); + + it("leaves a tool that runs happily on its defaults unwarned", async () => { + renderBuilder("/processor/pipelines/new"); + await addTool("Compress"); + + expect( + await screen.findByText("portal.pipelines.graph.add.input"), + ).toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.builder.needsConfiguring"), + ).not.toBeInTheDocument(); + }); + + it("only asks for a source once the user has asked for the node", async () => { + renderBuilder("/processor/pipelines/new"); + await openInput(); + + // Placing the node is what turns it into an outstanding choice. + expect( + await screen.findByText("portal.pipelines.builder.needsSource"), + ).toBeInTheDocument(); + // Still nothing chosen, so the pipeline cannot be saved. + expect( + screen.getByText("portal.pipelines.composer.create").closest("button"), + ).toBeDisabled(); + }); + + it("puts an end back to a placeholder when it is removed", async () => { + renderBuilder("/processor/pipelines/new"); + await openInput(); + await screen.findByText("portal.pipelines.builder.needsSource"); + + fireEvent.click(screen.getByLabelText("portal.pipelines.graph.removeNode")); + + expect( + await screen.findByText("portal.pipelines.graph.add.input"), + ).toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.builder.needsSource"), + ).not.toBeInTheDocument(); + }); + + it("edits a node's settings only once it is selected", async () => { + renderBuilder("/processor/pipelines/new"); + await screen.findByText("portal.pipelines.graph.add.input"); + + // Nothing selected: the inspector says so rather than showing a form. + expect( + screen.getByText("portal.pipelines.inspector.noSelectionTitle"), + ).toBeInTheDocument(); + + await openInput(); + expect( + screen.getByRole("textbox", { name: "portal.pipelines.builder.inputSource", }), - ).toHaveLength(1); - expect( - screen.queryByText("portal.pipelines.builder.addInput"), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole("button", { - name: "portal.pipelines.builder.removeInput", - }), - ).not.toBeInTheDocument(); + ).toBeInTheDocument(); }); it("builds a new pipeline: name it, add a tool, an input, a destination, and save", async () => { @@ -257,12 +491,11 @@ describe("PipelineBuilder", () => { }, ); - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click(await screen.findByText("Compress")); + await addTool("Compress"); // A pipeline must have at least one input source and one output destination. await pickInputSource("Claims intake"); - fireEvent.click(screen.getByText("pick output")); + await pickDestination(); fireEvent.click(screen.getByText("portal.pipelines.composer.create")); @@ -291,13 +524,11 @@ describe("PipelineBuilder", () => { { target: { value: "Broken chain" } }, ); await pickInputSource("Claims intake"); - fireEvent.click(screen.getByText("pick output")); + await pickDestination(); // Extract images emits images; compress only takes a PDF, so it can never run. - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click(await screen.findByText("Extract images")); - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click(await screen.findByText("Compress")); + await addTool("Extract images"); + await addTool("Compress"); expect( await screen.findByText("portal.pipelines.builder.stepsIncompatible"), @@ -307,6 +538,21 @@ describe("PipelineBuilder", () => { ).toBeDisabled(); }); + it("says why on the wire arriving at the step that cannot run", async () => { + renderBuilder("/processor/pipelines/new"); + await pickInputSource("Claims intake"); + await pickDestination(); + + await addTool("Extract images"); + await addTool("Compress"); + + // The banner names which steps are at fault; the wire explains what is wrong where it happens. + const note = await screen.findByText( + /portal\.pipelines\.builder\.diagnostic\./, + ); + expect(note.closest(".portal-graph-edge")).toHaveClass("is-blocking"); + }); + it("allows a chain whose steps line up", async () => { renderBuilder("/processor/pipelines/new"); @@ -317,10 +563,9 @@ describe("PipelineBuilder", () => { { target: { value: "Fine chain" } }, ); await pickInputSource("Claims intake"); - fireEvent.click(screen.getByText("pick output")); + await pickDestination(); - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click(await screen.findByText("Compress")); + await addTool("Compress"); expect( screen.queryByText("portal.pipelines.builder.stepsIncompatible"), @@ -352,7 +597,7 @@ describe("PipelineBuilder", () => { expect(saveButton()).toBeDisabled(); // Both chosen: allowed, and both are sent. - fireEvent.click(screen.getByText("pick output")); + await pickDestination(); fireEvent.click(screen.getByText("portal.pipelines.composer.create")); await waitFor(() => expect(savePipeline).toHaveBeenCalledTimes(1)); expect(savePipeline).toHaveBeenCalledWith( @@ -363,6 +608,130 @@ describe("PipelineBuilder", () => { ); }); + it("creates and edits sources in place through the modal", async () => { + renderBuilder("/processor/pipelines/new"); + await pickInputSource("Claims intake"); + + // Connect source opens the modal in create mode, without leaving the + // builder (and its unsaved edits) for the Sources page. + fireEvent.click(screen.getByText("portal.sources.actions.connectSource")); + expect(screen.getByText("source-modal:new")).toBeInTheDocument(); + expect(screen.queryByText("pipelines list")).not.toBeInTheDocument(); + + // The pencil beside the input opens the same modal on the chosen source. + fireEvent.click( + screen.getByLabelText("portal.pipelines.composer.editSource"), + ); + expect(screen.getByText("source-modal:src-in")).toBeInTheDocument(); + }); + + it("cannot edit an input source before one is chosen", async () => { + renderBuilder("/processor/pipelines/new"); + await openInput(); + + expect( + screen.getByLabelText("portal.pipelines.composer.editSource"), + ).toBeDisabled(); + await pickInputSource("Claims intake"); + expect( + screen.getByLabelText("portal.pipelines.composer.editSource"), + ).not.toBeDisabled(); + }); + + it("makes a source created from the input row the pipeline's input", async () => { + fetchSources + .mockResolvedValueOnce({ kpis: [], sources: [SOURCE] }) + .mockResolvedValue({ + kpis: [], + sources: [SOURCE, { ...SOURCE, id: "src-new", name: "Scanner drop" }], + }); + renderBuilder("/processor/pipelines/new"); + await openInput(); + + fireEvent.click(screen.getByText("portal.sources.actions.connectSource")); + fireEvent.click(screen.getByText("source saved")); + + // The new source is the one the pipeline was missing, so it becomes the input. + await waitFor(() => + expect( + screen.getByRole("textbox", { + name: "portal.pipelines.builder.inputSource", + }), + ).toHaveValue("Scanner drop"), + ); + }); + + it("makes a destination created from the picker the pipeline's output", async () => { + fetchSources + .mockResolvedValueOnce({ kpis: [], sources: [SOURCE] }) + .mockResolvedValue({ + kpis: [], + sources: [ + SOURCE, + { ...SOURCE, id: "src-new", name: "Archive bucket", type: "s3" }, + ], + }); + renderBuilder("/processor/pipelines/new"); + await openInput(); + openOutput(); + await screen.findByText("pick output"); + + fireEvent.click(screen.getByText("new destination")); + fireEvent.click(screen.getByText("source saved")); + + // Created from the destination picker, so it lands in the output rather + // than the input. + await waitFor(() => + expect(screen.getByText("output:src-new")).toBeInTheDocument(), + ); + // The input was left alone: its node still shows the prompt. + expect( + screen.getByText("portal.pipelines.builder.chooseSource"), + ).toBeInTheDocument(); + }); + + it("leaves a new source that cannot be written to out of the destination", async () => { + // A webhook can be read from but not written to, so it must not be picked + // as a destination the dropdown has no option for. + fetchSources + .mockResolvedValueOnce({ kpis: [], sources: [SOURCE] }) + .mockResolvedValue({ + kpis: [], + sources: [ + SOURCE, + { ...SOURCE, id: "src-hook", name: "Partner hook", type: "webhook" }, + ], + }); + renderBuilder("/processor/pipelines/new"); + await openInput(); + openOutput(); + await screen.findByText("pick output"); + + fireEvent.click(screen.getByText("new destination")); + fireEvent.click(screen.getByText("source saved")); + + // It was not made the destination... + await waitFor(() => expect(fetchSources).toHaveBeenCalledTimes(2)); + expect(screen.getByText("pick output")).toBeInTheDocument(); + + // ...but it did arrive, and is offered as an input, where a webhook makes sense. + await openInput(); + fireEvent.click( + screen.getByRole("textbox", { + name: "portal.pipelines.builder.inputSource", + }), + ); + expect(await screen.findByText("Partner hook")).toBeInTheDocument(); + }); + + it("edits the chosen destination through the same modal", async () => { + renderBuilder("/processor/pipelines/new"); + await pickDestination(); + + fireEvent.click(screen.getByText("edit destination")); + expect(screen.getByText("source-modal:src-1")).toBeInTheDocument(); + }); + it("runs an existing pipeline and reports success", async () => { renderBuilder("/processor/pipelines/plc-1"); @@ -401,9 +770,8 @@ describe("PipelineBuilder", () => { it("clears processed history from the header and confirms", async () => { renderBuilder("/processor/pipelines/plc-1"); - fireEvent.click( - await screen.findByText("portal.pipelines.detail.clearHistory"), - ); + await openTray(); + fireEvent.click(screen.getByText("portal.pipelines.detail.clearHistory")); await waitFor(() => expect(clearProcessedHistory).toHaveBeenCalledWith("plc-1"), @@ -424,8 +792,7 @@ describe("PipelineBuilder", () => { target: { value: "Watermarked" }, }, ); - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click(await screen.findByText("Compress")); + await addTool("Compress"); // The tool's settings upload a file, which a stored pipeline can't persist yet. fireEvent.click(await screen.findByText("upload logo")); @@ -450,10 +817,7 @@ describe("PipelineBuilder", () => { target: { value: "Notify only" }, }, ); - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click( - await screen.findByText("portal.policies.operations.discordNotify.label"), - ); + await addTool("portal.policies.operations.discordNotify.label"); // Operation chosen, account not: still not saveable. expect( @@ -516,10 +880,7 @@ describe("PipelineBuilder", () => { }, ); - fireEvent.click(screen.getByRole("button", { name: /addTool/ })); - fireEvent.click( - await screen.findByText("portal.policies.operations.discordNotify.label"), - ); + await addTool("portal.policies.operations.discordNotify.label"); fireEvent.click( await screen.findByPlaceholderText( @@ -530,7 +891,7 @@ describe("PipelineBuilder", () => { // Saving needs the input's source and a destination. await pickInputSource("Claims intake"); - fireEvent.click(screen.getByText("pick output")); + await pickDestination(); fireEvent.click(screen.getByText("portal.pipelines.composer.create")); diff --git a/frontend/editor/src/portal/views/PipelineBuilder.tsx b/frontend/editor/src/portal/views/PipelineBuilder.tsx index d788fc4f35..ecc249d88e 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.tsx @@ -1,19 +1,15 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import KeyboardArrowUpRoundedIcon from "@mui/icons-material/KeyboardArrowUpRounded"; -import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded"; -import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; -import HistoryRoundedIcon from "@mui/icons-material/HistoryRounded"; +import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import AddRoundedIcon from "@mui/icons-material/AddRounded"; -import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded"; +import MoveToInboxRoundedIcon from "@mui/icons-material/MoveToInboxRounded"; +import SendRoundedIcon from "@mui/icons-material/SendRounded"; import { ActionIcon, Banner, Button, - Checkbox, - EmptyState, + FormField, Input, Modal, Select, @@ -47,11 +43,14 @@ import { deletePipeline, fetchPipeline, fetchRun, + fetchRunOutput, fetchTriggers, + runPipelineTest, savePipeline, triggerPipeline, type Policy, type PolicyRunView, + type RunOutputFile, type TriggerConfig, type TriggerInfo, type TriggerOutcome, @@ -61,14 +60,27 @@ import { DestinationPicker } from "@portal/components/pipelines/DestinationPicke import { availableOutputModes } from "@portal/components/pipelines/outputModes"; import { type SourceView } from "@portal/api/sources"; import { useSources } from "@portal/queries/sources"; +import { SourceModal } from "@portal/components/sources/SourceModal"; import { EDITOR_SOURCE_TYPE } from "@portal/components/sources/sourceTypes"; import { useAsync } from "@portal/hooks/useAsync"; import { useQueryClient } from "@tanstack/react-query"; import { qk } from "@portal/queries/keys"; import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; import { humanizeOperation } from "@portal/components/pipelines/pipelineOperations"; +import { PipelineHeader } from "@portal/components/pipelines/PipelineHeader"; +import { PipelineInspector } from "@portal/components/pipelines/PipelineInspector"; +import { PipelineDefinitionModal } from "@portal/components/pipelines/PipelineDefinitionModal"; +import { + PipelineGraph, + selectedSteps, + type ChainEnd, + type ChainWarning, + type GraphSelection, + type GraphStepContent, +} from "@portal/components/pipelines/graph/PipelineGraph"; import { PipelineStepSettings } from "@portal/components/pipelines/PipelineStepSettings"; import { ToolPicker } from "@portal/components/pipelines/ToolPicker"; +import { BrandMark } from "@portal/components/BrandMarks"; import { STEP_OPERATIONS } from "@portal/components/policies/stepOperations"; import { integrationStepConfigured, @@ -158,6 +170,11 @@ function buildTriggerFor(input: WorkingInput): TriggerConfig | null { return { type: input.triggerType, options: {} }; } +/** Whether a source can be written to, i.e. offered as a pipeline destination. */ +function isWritableSource(source: SourceView): boolean { + return (availableOutputModes() as string[]).includes(source.type); +} + /** * Full-page pipeline builder (route: /pipelines/new and /pipelines/:id). Pipeline-level settings * (sources, trigger, output) sit above the operation list; the operation list and the selected @@ -206,10 +223,7 @@ export function PipelineBuilder() { // A destination is a source used as a write target: only writable types (folder/S3, filtered per // deployment) can be picked, and the virtual editor is already excluded from availableSources. const writableSources = useMemo( - () => - availableSources.filter((source) => - (availableOutputModes() as string[]).includes(source.type), - ), + () => availableSources.filter(isWritableSource), [availableSources], ); const triggers = useMemo( @@ -223,9 +237,23 @@ export function PipelineBuilder() { // wire shape stays a list (see save()). const [input, setInput] = useState(blankInput); const [steps, setSteps] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(null); - const [pickerOpen, setPickerOpen] = useState(false); + /** Which node the inspector is editing: an end of the chain, a step, or nothing. */ + const [selected, setSelected] = useState(null); + /** Slot the tool picker will insert into, or null when it is closed. */ + const [pickerAt, setPickerAt] = useState(null); + const [definitionOpen, setDefinitionOpen] = useState(false); + /** The last test run in this session: one file through the steps as they stand. */ + const [testRun, setTestRun] = useState(null); + const [testing, setTesting] = useState(false); const [outputIds, setOutputIds] = useState([]); + /** + * Whether the user has asked for each end of the chain yet, distinguishing "not offered" from + * "offered and still owed a choice" - the two states an empty sourceId cannot tell apart. Only a + * brand new pipeline starts with either false; anything loaded arrives with both ends set, and + * choosing one places it, so these are just the "clicked add, chosen nothing" window. + */ + const [inputAsked, setInputAsked] = useState(false); + const [outputAsked, setOutputAsked] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [seeded, setSeeded] = useState(false); @@ -236,6 +264,39 @@ export function PipelineBuilder() { const [deleting, setDeleting] = useState(false); const [pendingNav, setPendingNav] = useState(null); + // Create or edit a source in place, instead of leaving the builder (and its + // unsaved edits) for the Sources page. + const [sourceModal, setSourceModal] = useState<{ + open: boolean; + sourceId: string | null; + }>({ open: false, sourceId: null }); + // A source created from here is the one the pipeline was missing, so select it + // on arrival - as the input or the destination, whichever asked for it. + const autoSelectRef = useRef<"input" | "output" | null>(null); + const knownSourceIdsRef = useRef>(new Set()); + useEffect(() => { + const target = autoSelectRef.current; + const known = knownSourceIdsRef.current; + knownSourceIdsRef.current = new Set(availableSources.map((s) => s.id)); + if (!target) return; + const fresh = availableSources.find((s) => !known.has(s.id)); + if (!fresh) return; + // One arrival answers the request, whatever type it turned out to be. + autoSelectRef.current = null; + if (target === "input") { + changeInputSource(fresh.id); + } else if (isWritableSource(fresh)) { + // A source of an unwritable type is left alone rather than becoming a + // destination the picker has no option for. + setOutputIds([fresh.id]); + } + }, [availableSources]); + + function createSourceFor(target: "input" | "output") { + autoSelectRef.current = target; + setSourceModal({ open: true, sourceId: null }); + } + const mounted = useRef(true); useEffect(() => { mounted.current = true; @@ -273,14 +334,6 @@ export function PipelineBuilder() { setSeeded(true); }, [isEdit, policyState.data, allTools, seeded]); - // Keep one tool's settings open: auto-select the first step whenever a pipeline has steps but - // nothing is selected (initial load, or after the selected step is removed). - useEffect(() => { - if (seeded && selectedIndex === null && steps.length > 0) { - setSelectedIndex(0); - } - }, [seeded, selectedIndex, steps.length]); - const sourceType = (sourceId: string) => availableSources.find((s) => s.id === sourceId)?.type; @@ -339,38 +392,65 @@ export function PipelineBuilder() { }); } - function addOperationStep(op: (typeof STEP_OPERATIONS)[number]) { + /** Put an end on the chain and open it, so the click that asks for it also offers the choice. */ + function addEnd(end: ChainEnd) { + if (end === "input") setInputAsked(true); + else setOutputAsked(true); + setSelected(end); + } + + /** Take an end back off, discarding whatever it held so its row reads as unfilled again. */ + function removeEnd(end: ChainEnd) { + if (end === "input") { + setInputAsked(false); + setInput(blankInput()); + } else { + setOutputAsked(false); + setOutputIds([]); + } + setSelected((current) => (current === end ? null : current)); + } + + /** Drop a new step into the slot the picker was opened on, and select it to be configured. */ + function insertStep(step: WorkingToolStep) { + const at = pickerAt ?? steps.length; setSteps((current) => { - const next = [...current, newIntegrationStep(op)]; - setSelectedIndex(next.length - 1); + const next = [...current]; + next.splice(at, 0, step); return next; }); - setPickerOpen(false); + setSelected({ steps: [at] }); + setPickerAt(null); + } + + function addOperationStep(op: (typeof STEP_OPERATIONS)[number]) { + insertStep(newIntegrationStep(op)); } function addStep(tool: ExecutableTool) { - setSteps((current) => { - const next = [...current, newWorkingToolStep(tool, allTools)]; - setSelectedIndex(next.length - 1); - return next; - }); - setPickerOpen(false); + insertStep(newWorkingToolStep(tool, allTools)); } - function removeStep(index: number) { - setSelectedIndex(null); - setSteps((current) => current.filter((_, i) => i !== index)); + function removeSteps(indices: number[]) { + const gone = new Set(indices); + setSelected(null); + setSteps((current) => current.filter((_, i) => !gone.has(i))); } - function moveStep(index: number, delta: number) { - setSteps((current) => { - const target = index + delta; - if (target < 0 || target >= current.length) return current; - const next = [...current]; - [next[index], next[target]] = [next[target], next[index]]; - return next; - }); - setSelectedIndex((cur) => (cur === index ? index + delta : cur)); + /** + * Apply a reordered chain, given as the original step indices in their new positions. The steps + * the drag carried stay selected where they land, so a set can be dragged again without re-picking + * it - and dragging an unselected step selects it, rather than leaving the inspector on whatever + * was selected before. + */ + function reorderSteps(order: number[], moved: readonly number[]) { + const moving = new Set(moved); + setSteps((current) => order.map((i) => current[i])); + const landed = order + .map((original, position) => ({ original, position })) + .filter(({ original }) => moving.has(original)) + .map(({ position }) => position); + setSelected(landed.length > 0 ? { steps: landed } : null); } function updateStepParams(index: number, params: ErasedToolParams) { @@ -396,6 +476,21 @@ export function PipelineBuilder() { return entry?.name ?? humanizeOperation(step.operation); } + /** + * A step's glyph, matching how the tool picker draws it: an integration step carries its vendor's + * mark, a tool step its own icon. Without this every node falls back to the generic slider glyph, + * so a chain reads as a stack of identical cards. + */ + function stepIcon(step: WorkingToolStep): ReactNode { + const op = stepOperation(step); + if (op) + return ( + + ); + if (isIntegrationStep(step)) return ; + return step.toolId ? allTools[step.toolId]?.icon : undefined; + } + // Steps whose params carry an uploaded file can't be saved: the bytes aren't persisted with the // policy, so a later run would send null for that field (see stepRequiresUpload). const uploadStepLabels = steps.filter(stepRequiresUpload).map(stepLabel); @@ -431,16 +526,22 @@ export function PipelineBuilder() { .map((d) => stepLabel(steps[d.stepIndex])); const hasIncompatibleSteps = hasBlockingDiagnostics(chainDiagnostics); - // What a newly added step would be handed, so the picker can flag tools that cannot take it. - const chainOutput = useMemo( + /** + * What a step added at the open slot would be handed, so the picker can flag tools that cannot + * take it. Scoped to the steps *before* that slot rather than the whole chain: the graph inserts + * anywhere, so what precedes the new step is not necessarily the chain's final output. + */ + const precedingOutput = useMemo( () => - chainOutputFormat( - steps.map((step) => ({ - operation: step.operation, - parameters: step.params, - })), - ), - [steps], + pickerAt === null + ? undefined + : chainOutputFormat( + steps.slice(0, pickerAt).map((step) => ({ + operation: step.operation, + parameters: step.params, + })), + ), + [steps, pickerAt], ); function diagnosticNote(diagnostic: ToolDiagnostic): string { @@ -451,26 +552,21 @@ export function PipelineBuilder() { }); } - /** The most severe diagnostic for a step, rendered as its note. */ - function renderStepDiagnostic(index: number) { + /** + * The step's most severe diagnostic, for the wire arriving at it - which is where a note about + * what the step is being handed belongs, rather than on the step itself. + */ + function stepInputWarning(index: number): ChainWarning | undefined { const forStep = diagnosticsForStep(chainDiagnostics, index); const diagnostic = forStep.find((d) => d.severity === "ERROR") ?? forStep.find((d) => d.severity === "WARN") ?? forStep[0]; - if (!diagnostic) return null; - return ( - - {diagnosticNote(diagnostic)} - - ); + if (!diagnostic) return undefined; + return { + text: diagnosticNote(diagnostic), + blocking: diagnostic.severity === "ERROR", + }; } // Track unsaved edits: snapshot the form and compare against the state captured just after @@ -505,7 +601,6 @@ export function PipelineBuilder() { !submitting; const listPath = toPortalPath(VIEW_PATHS.pipelines); - const sourcesPath = `${toPortalPath(VIEW_PATHS.sources)}/new`; function close() { navigate(listPath); @@ -517,12 +612,6 @@ export function PipelineBuilder() { else navigate(destination); } - // Jump to the source builder, for when the source you want to read from or write to doesn't - // exist yet. Inputs and the output destination are both saved sources, so both create one here. - function goToSources() { - attemptLeave(sourcesPath); - } - async function save(destination: string) { if (!canSave) return; setSubmitting(true); @@ -550,16 +639,69 @@ export function PipelineBuilder() { } // Poll a run until it reaches a terminal state (or we give up), so a failure surfaces. - async function awaitRun(runId: string): Promise { + async function awaitRun( + runId: string, + onProgress?: (view: PolicyRunView) => void, + ): Promise { for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) { if (!mounted.current) return null; const view = await fetchRun(runId); + onProgress?.(view); if (TERMINAL_STATUSES.has(view.status)) return view; await sleep(POLL_INTERVAL_MS); } return null; } + /** + * Run the steps as they stand against one uploaded file. Output is forced inline so nothing + * reaches the pipeline's real destination, and the pipeline need not be saved first - this is + * how the chain gets checked while it is still being built. + */ + async function handleTest(file: File) { + if (testing) return; + setTesting(true); + setTestRun(null); + setRunResult(null); + try { + const { runId } = await runPipelineTest( + { + name: name.trim() || t("portal.pipelines.builder.testRun"), + steps: steps.map((step) => serializeToolStep(step, allTools)), + output: { type: "inline", options: {} }, + }, + file, + ); + const final = await awaitRun(runId, (view) => { + if (mounted.current) setTestRun(view); + }); + if (mounted.current && final) setTestRun(final); + } catch (e) { + if (mounted.current) + setRunResult({ tone: "danger", text: errorMessage(e) }); + } finally { + if (mounted.current) setTesting(false); + } + } + + /** Save one of a test run's outputs to disk. */ + async function downloadOutput(output: RunOutputFile) { + try { + const blob = await fetchRunOutput(output.fileId); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = output.fileName ?? output.fileId; + link.click(); + // Revoke on the next tick: some browsers have not yet begun reading the + // blob when click() returns, and revoking now would cancel the download. + setTimeout(() => URL.revokeObjectURL(url), 0); + } catch (e) { + if (mounted.current) + setRunResult({ tone: "danger", text: errorMessage(e) }); + } + } + /** Explain an empty trigger: parked files outrank blander reasons. */ function emptySweepResult(outcome: TriggerOutcome): RunResult { if (outcome.parked > 0) { @@ -669,95 +811,257 @@ export function PipelineBuilder() { ); } + const chosenSteps = selectedSteps(selected); + // One step selected means its settings; several means there is no single thing to configure. const selectedStep = - selectedIndex !== null ? (steps[selectedIndex] ?? null) : null; + chosenSteps.length === 1 ? (steps[chosenSteps[0]] ?? null) : null; - return ( -
    -
    - -
    - setName(e.target.value)} - /> -
    -
    - setEnabled(e.target.checked)} - label={t("portal.pipelines.builder.enabled")} - /> - {isEdit && ( + const chosenSource = availableSources.find((s) => s.id === input.sourceId); + const chosenDestination = writableSources.find((s) => s.id === outputIds[0]); + + /** How this input fires, in a few words, for the input node's summary line. */ + function triggerSummary(): string { + if (input.triggerType === MANUAL) + return t("portal.pipelines.composer.triggerManual"); + if (input.triggerType === "schedule") + // One counted phrase per unit, so it reads "Runs every hour" / "Runs every 3 hours" rather + // than the ungrammatical, untranslatable "Run every 1 hours". + return t( + `portal.pipelines.composer.runsEvery.${input.scheduleUnit.toLowerCase()}`, + { count: Number(input.scheduleCount) || 1 }, + ); + return t(`portal.pipelines.trigger.${input.triggerType}`, { + defaultValue: input.triggerType, + }); + } + + /** Why a step cannot be saved yet, if anything. */ + function stepWarning(step: WorkingToolStep): string | undefined { + if (isIntegrationStep(step)) { + if (!stepOperation(step)) + return t("portal.pipelines.builder.chooseOperation"); + if (!integrationStepConfigured(step)) + return t("portal.pipelines.builder.chooseAccount"); + return undefined; + } + if (stepRequiresUpload(step)) + return t("portal.pipelines.builder.needsUpload"); + if (stepNeedsConfiguring(step, allTools)) + return t("portal.pipelines.builder.needsConfiguring"); + return undefined; + } + + /** A step's one-line summary: what it will do beyond its name. */ + function stepDetail(step: WorkingToolStep): string | undefined { + if (step.support === "unsupported") + return t("portal.pipelines.builder.usesDefaults"); + if (step.support === "unknown") + return t("portal.pipelines.builder.unknownStep"); + return undefined; + } + + // A run reports one step cursor, so progress reads off it: everything before the cursor is done, + // the cursor itself is whatever the run currently is. + function stepRunState(index: number): GraphStepContent["runState"] { + if (!testRun) return undefined; + if (index < testRun.currentStep) return "done"; + if (index > testRun.currentStep) return undefined; + if (testRun.status === "FAILED") return "failed"; + if (testRun.status === "COMPLETED") return "done"; + return "running"; + } + + const graphSteps: GraphStepContent[] = steps.map((step, i) => ({ + label: stepLabel(step), + detail: stepDetail(step), + icon: stepIcon(step), + warning: stepWarning(step), + inputWarning: stepInputWarning(i), + runState: stepRunState(i), + })); + + const definitionJson = JSON.stringify( + { + name: name.trim(), + enabled, + inputs: [{ sourceId: input.sourceId, trigger: buildTriggerFor(input) }], + steps: steps.map((step) => serializeToolStep(step, allTools)), + outputIds, + }, + null, + 2, + ); + + const testSummary = + testRun === null + ? null + : { + status: + testRun.status === "FAILED" + ? ("failed" as const) + : testRun.status === "COMPLETED" + ? ("completed" as const) + : ("running" as const), + completedSteps: testRun.currentStep, + stepCount: testRun.stepCount, + error: testRun.error, + outputs: testRun.outputs ?? [], + }; + + /** The editor for whatever node is selected. Undefined when nothing is. */ + function inspectorBody() { + if (selected === "input") { + // Nothing to pick from yet: a dropdown of nothing helps no one, so offer only the way to make + // the first source. The trigger has no meaning without a source either, so it waits too. + const hasSources = availableSources.length > 0; + return ( + <> + {hasSources && ( <> - - - + +
    +
    + + updateInput({ + triggerType: + value && value !== MANUAL_OPTION ? value : MANUAL, + }) + } + options={triggerOptionsFor(input.sourceId)} + /> + + + {input.triggerType === "schedule" && ( +
    + + {t("portal.pipelines.composer.scheduleEvery")} + + + updateInput({ scheduleCount: e.target.value }) + } + className="portal-builder__schedule-count" + /> + changeInputSource(value ?? "")} - options={sourceOptions} - /> -
    -
    - - updateInput({ scheduleCount: e.target.value }) - } - className="portal-pipelines__schedule-count" - /> -
    } is no longer a row to a screen reader. That row control is then the + * keyboard path to the same action, so nothing is lost by leaving the row itself inert. + */ + rowsContainControls?: boolean; /** Rendered in place of the body when there are no rows. */ empty?: ReactNode; className?: string; @@ -42,6 +55,7 @@ export function Table({ rowKey, onRowClick, isRowInteractive, + rowsContainControls = false, empty, className, }: TableProps) { @@ -60,7 +74,11 @@ export function Table({ className={`sui-table__th sui-table__th--${c.align ?? "left"}`} style={c.width ? { width: c.width } : undefined} > - {c.header} + {c.headerHidden ? ( + {c.header} + ) : ( + c.header + )} ))} @@ -76,6 +94,9 @@ export function Table({ rows.map((row) => { const rowInteractive = interactive && (isRowInteractive?.(row) ?? true); + // Only a row that owns the whole interaction takes the button role and the keyboard + // handling that goes with it; see rowsContainControls. + const rowIsControl = rowInteractive && !rowsContainControls; return ( ({ : "sui-table__row" } onClick={rowInteractive ? () => onRowClick?.(row) : undefined} - tabIndex={rowInteractive ? 0 : undefined} - role={rowInteractive ? "button" : undefined} + tabIndex={rowIsControl ? 0 : undefined} + role={rowIsControl ? "button" : undefined} onKeyDown={ - rowInteractive + rowIsControl ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); diff --git a/frontend/editor/src/portal/api/fileRunEvents.test.ts b/frontend/editor/src/portal/api/fileRunEvents.test.ts new file mode 100644 index 0000000000..0e1729a891 --- /dev/null +++ b/frontend/editor/src/portal/api/fileRunEvents.test.ts @@ -0,0 +1,104 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * Contract tests for the api module itself. The component tests mock it wholesale, + * so they cannot catch mistakes in how it talks to the http client — a + * double-encoded body slipped through that gap once. These pin the call shape. + */ + +const json = vi.fn(); + +vi.mock("@portal/api/http", () => ({ + apiClient: { local: { json: (...args: unknown[]) => json(...args) } }, +})); + +const { applyFileRunEventAction, fetchFileRunEvents } = + await import("@portal/api/fileRunEvents"); + +describe("fileRunEvents api", () => { + beforeEach(() => json.mockReset()); + + describe("fetchFileRunEvents", () => { + it("requests the collection with no query when unfiltered", async () => { + json.mockResolvedValue({ events: [] }); + + await fetchFileRunEvents(); + + expect(json).toHaveBeenCalledWith("/api/v1/file-run-events"); + }); + + it("serialises only the filters that were supplied", async () => { + json.mockResolvedValue({ events: [] }); + + await fetchFileRunEvents({ status: "NEW", limit: 10 }); + + const [path] = json.mock.calls[0] as [string]; + const query = new URL(path, "http://localhost").searchParams; + expect(query.get("status")).toBe("NEW"); + expect(query.get("limit")).toBe("10"); + expect(query.has("kindId")).toBe(false); + }); + + it("never sends a team parameter, because the server derives it", async () => { + // The server derives the team, so asserted here to keep a param from creeping in. + json.mockResolvedValue({ events: [] }); + + await fetchFileRunEvents({ status: "NEW", kindId: "UNKNOWN", limit: 5 }); + + const [path] = json.mock.calls[0] as [string]; + expect(path).not.toMatch(/team/i); + }); + + it("unwraps the response envelope", async () => { + json.mockResolvedValue({ events: [{ id: "a" }] }); + + await expect(fetchFileRunEvents()).resolves.toEqual([{ id: "a" }]); + }); + + it("tolerates a response with no events array", async () => { + json.mockResolvedValue(undefined); + + await expect(fetchFileRunEvents()).resolves.toEqual([]); + }); + }); + + describe("applyFileRunEventAction", () => { + it("passes body as an object, not a pre-serialised string", async () => { + // The regression this file exists for: apiClient stringifies `body` + // itself, so a string here would arrive double-encoded. + json.mockResolvedValue({ id: "fre-1" }); + + await applyFileRunEventAction("fre-1", "ACKNOWLEDGE"); + + const [, options] = json.mock.calls[0] as [ + string, + { method: string; body: unknown }, + ]; + expect(typeof options.body).toBe("object"); + expect(options.body).toEqual({ inputs: {} }); + expect(options.method).toBe("POST"); + }); + + it("forwards collected inputs", async () => { + json.mockResolvedValue({ id: "fre-1" }); + + await applyFileRunEventAction("fre-1", "ACKNOWLEDGE", { + password: "hunter2", + }); + + const [, options] = json.mock.calls[0] as [string, { body: unknown }]; + expect(options.body).toEqual({ inputs: { password: "hunter2" } }); + }); + + it("encodes ids into the path so an odd id cannot break the URL", async () => { + json.mockResolvedValue({ id: "x" }); + + await applyFileRunEventAction("a/b?c", "ACKNOWLEDGE"); + + const [path] = json.mock.calls[0] as [string]; + expect(path).toBe( + "/api/v1/file-run-events/a%2Fb%3Fc/actions/ACKNOWLEDGE", + ); + }); + }); +}); diff --git a/frontend/editor/src/portal/api/fileRunEvents.ts b/frontend/editor/src/portal/api/fileRunEvents.ts new file mode 100644 index 0000000000..8b52c7d3fc --- /dev/null +++ b/frontend/editor/src/portal/api/fileRunEvents.ts @@ -0,0 +1,120 @@ +import { apiClient } from "@portal/api/http"; + +/** + * Recorded policy and pipeline failures, mirroring the backend `FileRunEventView`. + * + * Two properties of the contract: the server sends i18n keys plus an English + * `defaultTitle` rather than rendered copy, so a client can display a kind it was + * not built with; and each row's `actions` arrive already resolved, so rendering + * buttons needs no knowledge of the rules. + */ + +/** Where the fault lay. Widened server-side ahead of the kinds that need it. */ +export type FailureStage = + | "INPUT" + | "INTERNAL" + | "OUTPUT" + | "BLOCKED" + | "NEVER_RAN"; + +export type FailureSeverity = "ERROR" | "WARNING" | "INFO"; + +export type FailureRemedy = + | "TRANSIENT" + | "NEEDS_USER_INPUT" + | "NEEDS_FILE_FIX" + | "NEEDS_CONFIG_FIX" + | "NEEDS_SERVER_FIX" + | "PERMANENT"; + +export type FailureScope = "FILE" | "RUN" | "POLICY" | "SOURCE" | "SERVER"; + +export type FailureOrigin = "TOOL" | "POLICY" | "PIPELINE"; + +export type FileRunEventStatus = + | "NEW" + | "ACKNOWLEDGED" + | "DISMISSED" + | "RESOLVED"; + +/** + * One button as offered for one row. `id` is a plain string rather than a union + * because the server may know actions this build does not; the renderer skips them. + */ +export interface FailureActionOffer { + id: string; + labelKey: string; + enabled: boolean; + disabledReasonKey: string | null; +} + +export interface FileRunEvent { + id: string; + kindId: string; + stage: FailureStage; + severity: FailureSeverity; + scope: FailureScope; + origin: FailureOrigin; + remedy: FailureRemedy; + titleKey: string; + descriptionKey: string; + /** English fallback, used when this build has no translation for `titleKey`. */ + defaultTitle: string; + /** Raw failure message. For an unclassified failure this is the only detail. */ + detail: string | null; + policyId: string | null; + runId: string | null; + /** + * Opaque reference, never a name. Only the owner's own client can resolve it to + * something readable, from its local file store. + */ + fileId: string | null; + actor: string | null; + /** How many times this same failure has been seen; repeats fold into one row. */ + occurrences: number; + status: FileRunEventStatus; + statusActor: string | null; + actions: FailureActionOffer[]; + createdAt: number; + lastSeenAt: number; +} + +export interface FileRunEventsResponse { + events: FileRunEvent[]; +} + +export interface ListFileRunEventsParams { + status?: FileRunEventStatus; + kindId?: string; + limit?: number; +} + +/** GET /api/v1/file-run-events — the caller's team only; there is no team param. */ +export async function fetchFileRunEvents( + params: ListFileRunEventsParams = {}, +): Promise { + const query = new URLSearchParams(); + if (params.status) query.set("status", params.status); + if (params.kindId) query.set("kindId", params.kindId); + if (params.limit != null) query.set("limit", String(params.limit)); + const suffix = query.toString() ? `?${query}` : ""; + + const response = await apiClient.local.json( + `/api/v1/file-run-events${suffix}`, + ); + return response?.events ?? []; +} + +/** POST /api/v1/file-run-events/{id}/actions/{actionId} — returns the updated row. */ +export async function applyFileRunEventAction( + eventId: string, + actionId: string, + inputs: Record = {}, +): Promise { + return apiClient.local.json( + `/api/v1/file-run-events/${encodeURIComponent(eventId)}/actions/${encodeURIComponent(actionId)}`, + // A plain object, not a JSON string: apiClient serialises `body` itself, so + // pre-stringifying would double-encode it. + { method: "POST", body: { inputs } }, + ); +} diff --git a/frontend/editor/src/portal/components/documents/ReviewQueueTable.tsx b/frontend/editor/src/portal/components/documents/ReviewQueueTable.tsx index bff13d0c4f..e63ea925c4 100644 --- a/frontend/editor/src/portal/components/documents/ReviewQueueTable.tsx +++ b/frontend/editor/src/portal/components/documents/ReviewQueueTable.tsx @@ -71,8 +71,11 @@ export function ReviewQueueTable({ )} {d.sensitive && ( + // role="img" so the label is allowed and the icon reads as one thing: aria-label + // is ignored on a bare span, leaving the padlock silent. @@ -136,7 +139,8 @@ export function ReviewQueueTable({ }, { key: "actions", - header: "", + header: t("portal.documents.table.columns.actions"), + headerHidden: true, width: "3rem", render: (d) => ( + ); + })} + + ); +} diff --git a/frontend/editor/src/portal/components/failures/FileRunEventList.test.tsx b/frontend/editor/src/portal/components/failures/FileRunEventList.test.tsx new file mode 100644 index 0000000000..1458a1d64d --- /dev/null +++ b/frontend/editor/src/portal/components/failures/FileRunEventList.test.tsx @@ -0,0 +1,193 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { render as baseRender, screen, waitFor } from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import type { FileRunEvent } from "@portal/api/fileRunEvents"; + +/** + * Tests for the list: the states it survives (loading, empty, no registry, refused), + * plus replacing a row in place after acting and re-reading when the server refuses. + */ + +const fetchFileRunEvents = vi.fn(); +const applyFileRunEventAction = vi.fn(); + +vi.mock("@portal/api/fileRunEvents", () => ({ + fetchFileRunEvents: (...args: unknown[]) => fetchFileRunEvents(...args), + applyFileRunEventAction: (...args: unknown[]) => + applyFileRunEventAction(...args), +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + // Faithful to i18next: a known key resolves, an unknown key falls back to + // defaultValue. That is what exercises the server-key-then-generic chain. + t: (key: string, options?: { defaultValue?: string } | string) => { + const known: Record = { + "portal.failures.kind.inputPasswordProtected.title": + "Password-protected document", + "portal.failures.action.acknowledge": "Acknowledge", + "portal.failures.empty.title": "No failures recorded", + "portal.failures.occurrences": "occurrences", + "portal.failures.runReference": "Run r1", + "portal.failures.stage.input": "Input", + }; + if (known[key]) return known[key]; + if (typeof options === "string") return options; + if (options?.defaultValue) return options.defaultValue; + return key; + }, + }), +})); + +// The list reads through the shared query hooks, and @app/ui needs Mantine. +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +const { FileRunEventList } = + await import("@portal/components/failures/FileRunEventList"); + +function event(overrides: Partial = {}): FileRunEvent { + return { + id: "fre-1", + kindId: "INPUT_PASSWORD_PROTECTED", + stage: "INPUT", + severity: "ERROR", + scope: "FILE", + origin: "POLICY", + remedy: "NEEDS_USER_INPUT", + titleKey: "portal.failures.kind.inputPasswordProtected.title", + descriptionKey: "portal.failures.kind.inputPasswordProtected.description", + defaultTitle: "Password-protected document", + detail: "The PDF Document is passworded", + policyId: "p1", + runId: "r1", + fileId: "f-1", + actor: "dana@example.com", + occurrences: 1, + status: "NEW", + statusActor: null, + actions: [ + { + id: "ACKNOWLEDGE", + labelKey: "portal.failures.action.acknowledge", + enabled: true, + disabledReasonKey: null, + }, + ], + createdAt: 0, + lastSeenAt: 0, + ...overrides, + }; +} + +describe("FileRunEventList", () => { + beforeEach(() => { + fetchFileRunEvents.mockReset(); + applyFileRunEventAction.mockReset(); + // The dev-panel test stubs import.meta.env.DEV, which would otherwise persist + // into every test after it. + vi.unstubAllEnvs(); + }); + + it("renders a row's title, run reference and raw detail, but no document name", async () => { + fetchFileRunEvents.mockResolvedValue([event()]); + + render(); + + expect(await screen.findByText("Password-protected document")).toBeTruthy(); + // A run reference, not a document name: the record holds no file identity. + expect(screen.getByText("Run r1")).toBeTruthy(); + // The raw message is shown, not swallowed: for an unclassified failure it is + // the only diagnostic available. + expect(screen.getByText("The PDF Document is passworded")).toBeTruthy(); + }); + + it("shows the occurrence count only once a failure has repeated", async () => { + fetchFileRunEvents.mockResolvedValue([event({ occurrences: 1 })]); + const { unmount } = render(); + await screen.findByText("Run r1"); + expect(screen.queryByText(/occurrences/)).toBeNull(); + unmount(); + + fetchFileRunEvents.mockResolvedValue([event({ occurrences: 14 })]); + render(); + expect(await screen.findByText(/occurrences/)).toBeTruthy(); + }); + + it("shows an empty state when there is nothing to triage", async () => { + fetchFileRunEvents.mockResolvedValue([]); + + render(); + + expect(await screen.findByText("No failures recorded")).toBeTruthy(); + }); + + it("renders nothing when the server has no failure registry", async () => { + // A core-only build has no such route, which is not worth showing a reviewer as + // an error, so the section stays silent. + fetchFileRunEvents.mockRejectedValue(new Error("404")); + + const { container } = render(); + + await waitFor(() => { + expect(container.querySelector(".portal-failures__list")).toBeNull(); + }); + expect(screen.queryByText("No failures recorded")).toBeNull(); + }); + + it("renders no heading at all for a caller the server refuses", async () => { + // Reviewing is leader-only, so a member's read returns 403. With no dev panel to + // frame, the whole section goes rather than leaving a bare heading. + vi.stubEnv("DEV", false); + fetchFileRunEvents.mockRejectedValue(new Error("403")); + + const { container } = render(); + + await waitFor(() => { + expect(container.querySelector(".portal-failures")).toBeNull(); + }); + // No section, and specifically no heading: Mantine puts its + + +
    +
    + +
    +
    Stirling PDF
    +
    Draw Signature
    +
    +
    + +
    +
    Connecting…
    + +
    +
    + +
    + +
    +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + + + +

    Draw your signature above, then send it. It appears in the Sign tool on your computer automatically.

    +
    + + + +
    Stirling PDF · signatures transfer directly to your desktop
    +
    + + + + diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java index bfacb4d503..dc9c9538cd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java @@ -49,6 +49,33 @@ class MobileScannerControllerTest { when(systemProps.isEnableMobileScanner()).thenReturn(false); } + // --- shared-endpoint gating: scanner and mobile signature share this API --- + + @Test + void createSession_whenOnlyMobileSignatureEnabled_returnsOk() { + // The signature feature must work with the scanner switched off. + when(applicationProperties.getSystem()).thenReturn(systemProps); + when(systemProps.isEnableMobileScanner()).thenReturn(false); + when(systemProps.isEnableMobileSignature()).thenReturn(true); + SessionInfo sessionInfo = new SessionInfo("test-session", 1000L, 601000L, 600000L); + when(mobileScannerService.createSession("test-session")).thenReturn(sessionInfo); + + ResponseEntity> response = controller.createSession("test-session"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + } + + @Test + void createSession_whenBothFeaturesDisabled_returnsForbidden() { + when(applicationProperties.getSystem()).thenReturn(systemProps); + when(systemProps.isEnableMobileScanner()).thenReturn(false); + when(systemProps.isEnableMobileSignature()).thenReturn(false); + + ResponseEntity> response = controller.createSession("test-session"); + + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + } + // --- createSession tests --- @Test diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index ed1b61e098..8ce96c2552 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -4993,6 +4993,34 @@ uploadSuccess = "Upload Successful!" uploadSuccessMessage = "Your images have been transferred." validating = "Validating session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scan to upload photos. Images auto-convert to PDF." descriptionNoConvert = "Scan to upload photos from your mobile device." @@ -10254,6 +10282,7 @@ backgroundRemovalFailedTitle = "Background removal failed" hint = "Upload a PNG or JPG image of your signature" label = "Upload signature image" placeholder = "Select image file" +previewAlt = "Current image signature" processing = "Processing image..." removeBackground = "Remove white background (make transparent)" @@ -10267,6 +10296,17 @@ saved = "Select a saved signature above, then click anywhere on the PDF to place text = "After entering your name above, click anywhere on the PDF to place your signature." title = "How to add signature" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Move Signature" pause = "Pause placement" diff --git a/frontend/editor/src/core/App.tsx b/frontend/editor/src/core/App.tsx index 6fd23e5510..81db0564b8 100644 --- a/frontend/editor/src/core/App.tsx +++ b/frontend/editor/src/core/App.tsx @@ -9,6 +9,7 @@ import HomePage from "@app/pages/HomePage"; import Onboarding from "@app/components/onboarding/Onboarding"; const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage")); +const MobileSignPage = lazy(() => import("@app/pages/MobileSignPage")); // Import global styles import "@app/styles/tailwind.css"; @@ -42,6 +43,16 @@ export default function App() { } /> + {/* Mobile signature drawing - reached from the Sign tool QR code */} + + + + } + /> + {/* All other routes need AppProviders for backend integration */} string | null; + undo: () => void; + clear: () => void; +} + +interface Stroke { + color: string; + size: number; + points: Array<{ x: number; y: number }>; +} + +interface MobileDrawCanvasProps { + penColor: string; + penSize: number; + /** Fired when the canvas goes between empty and inked (gates Send/Undo). */ + onHasInkChange: (hasInk: boolean) => void; +} + +/** Padding kept around the ink when cropping the export, in CSS pixels. */ +const EXPORT_PADDING = 12; + +function drawStroke(ctx: CanvasRenderingContext2D, stroke: Stroke) { + const { points } = stroke; + if (points.length === 0) return; + + ctx.strokeStyle = stroke.color; + ctx.fillStyle = stroke.color; + ctx.lineWidth = stroke.size; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + if (points.length === 1) { + // A tap: render a dot, which a zero-length stroke would not show. + ctx.beginPath(); + ctx.arc(points[0].x, points[0].y, stroke.size / 2, 0, Math.PI * 2); + ctx.fill(); + return; + } + + // Quadratic midpoint smoothing: each point becomes the control point of a + // curve to the midpoint of the next segment, turning jagged pointer samples + // into a pen-like line. + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length - 1; i++) { + const midX = (points[i].x + points[i + 1].x) / 2; + const midY = (points[i].y + points[i + 1].y) / 2; + ctx.quadraticCurveTo(points[i].x, points[i].y, midX, midY); + } + const last = points[points.length - 1]; + ctx.lineTo(last.x, last.y); + ctx.stroke(); +} + +export const MobileDrawCanvas = forwardRef< + MobileDrawCanvasHandle, + MobileDrawCanvasProps +>(function MobileDrawCanvas({ penColor, penSize, onHasInkChange }, ref) { + const canvasRef = useRef(null); + const strokesRef = useRef([]); + const activeStrokeRef = useRef(null); + // Live styling for the stroke currently being drawn, without re-rendering + const penRef = useRef({ color: penColor, size: penSize }); + penRef.current = { color: penColor, size: penSize }; + + const redraw = useCallback(() => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext("2d"); + if (!canvas || !ctx) return; + const dpr = window.devicePixelRatio || 1; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr); + for (const stroke of strokesRef.current) drawStroke(ctx, stroke); + if (activeStrokeRef.current) drawStroke(ctx, activeStrokeRef.current); + }, []); + + // Match the backing store to the element's CSS size × devicePixelRatio, and + // re-match on resize/rotation. Strokes are CSS-space, so a redraw restores + // them at the new size. + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const resize = () => { + const rect = canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + canvas.width = Math.max(1, Math.round(rect.width * dpr)); + canvas.height = Math.max(1, Math.round(rect.height * dpr)); + redraw(); + }; + + resize(); + const observer = new ResizeObserver(resize); + observer.observe(canvas); + return () => observer.disconnect(); + }, [redraw]); + + const pointFromEvent = (e: React.PointerEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + return { x: e.clientX - rect.left, y: e.clientY - rect.top }; + }; + + const handlePointerDown = (e: React.PointerEvent) => { + // One stroke at a time: a second touch while drawing would scribble. + if (activeStrokeRef.current) return; + e.currentTarget.setPointerCapture(e.pointerId); + activeStrokeRef.current = { + color: penRef.current.color, + size: penRef.current.size, + points: [pointFromEvent(e)], + }; + redraw(); + }; + + const handlePointerMove = (e: React.PointerEvent) => { + const stroke = activeStrokeRef.current; + if (!stroke) return; + // Coalesced events give the full sample train on high-rate digitizers, + // where the per-frame synthetic event alone would drop curvature. + const events = + "getCoalescedEvents" in e.nativeEvent + ? (e.nativeEvent as PointerEvent).getCoalescedEvents() + : [e.nativeEvent as PointerEvent]; + const rect = (e.currentTarget as HTMLCanvasElement).getBoundingClientRect(); + for (const ev of events) { + stroke.points.push({ + x: ev.clientX - rect.left, + y: ev.clientY - rect.top, + }); + } + redraw(); + }; + + const endStroke = () => { + const stroke = activeStrokeRef.current; + if (!stroke) return; + activeStrokeRef.current = null; + strokesRef.current = [...strokesRef.current, stroke]; + redraw(); + onHasInkChange(true); + }; + + useImperativeHandle(ref, () => ({ + exportPng: () => { + const strokes = strokesRef.current; + const canvas = canvasRef.current; + if (strokes.length === 0 || !canvas) return null; + + // Crop to the inked region so the signature stamps tightly, clamped to + // what was actually visible. + const rect = canvas.getBoundingClientRect(); + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const stroke of strokes) { + const reach = stroke.size / 2 + EXPORT_PADDING; + for (const p of stroke.points) { + minX = Math.min(minX, p.x - reach); + minY = Math.min(minY, p.y - reach); + maxX = Math.max(maxX, p.x + reach); + maxY = Math.max(maxY, p.y + reach); + } + } + minX = Math.max(0, minX); + minY = Math.max(0, minY); + maxX = Math.min(rect.width, maxX); + maxY = Math.min(rect.height, maxY); + const width = Math.max(1, maxX - minX); + const height = Math.max(1, maxY - minY); + + const dpr = window.devicePixelRatio || 1; + const exportCanvas = document.createElement("canvas"); + exportCanvas.width = Math.round(width * dpr); + exportCanvas.height = Math.round(height * dpr); + const ctx = exportCanvas.getContext("2d"); + if (!ctx) return null; + // Translate args are device pixels; scale args map stroke space to them. + ctx.setTransform(dpr, 0, 0, dpr, -minX * dpr, -minY * dpr); + for (const stroke of strokes) drawStroke(ctx, stroke); + return exportCanvas.toDataURL("image/png"); + }, + undo: () => { + strokesRef.current = strokesRef.current.slice(0, -1); + redraw(); + onHasInkChange(strokesRef.current.length > 0); + }, + clear: () => { + strokesRef.current = []; + activeStrokeRef.current = null; + redraw(); + onHasInkChange(false); + }, + })); + + return ( + + ); +}); diff --git a/frontend/editor/src/core/components/shared/MobileTransferModal.tsx b/frontend/editor/src/core/components/shared/MobileTransferModal.tsx new file mode 100644 index 0000000000..7a153c59fc --- /dev/null +++ b/frontend/editor/src/core/components/shared/MobileTransferModal.tsx @@ -0,0 +1,161 @@ +import { ReactNode } from "react"; +import { Modal, Stack, Text, Box, Alert } from "@mantine/core"; +import { QRCodeSVG } from "qrcode.react"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; +import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; +import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { useMobileTransferSession } from "@app/hooks/useMobileTransferSession"; + +/** + * The QR modal shell every phone-to-desktop transfer feature shares: session + * lifecycle, QR code, expiry/error alerts, and the fallback URL line. A + * feature supplies its copy, its public route, and what a received file + * means — the scanner converts to PDF, the sign tool routes it to a + * signature source. + */ +interface MobileTransferModalProps { + opened: boolean; + onClose: () => void; + /** SPA route the phone opens, without slashes: "mobile-scanner", "mobile-sign". */ + routePath: string; + /** Called once per uploaded file; arrivals are untrusted, validate inside. */ + onFileReceived: (file: File) => void | Promise; + title: string; + description: string; + instructions: string; + expiryWarningTitle: string; + /** Interpolates the seconds remaining into the feature's warning copy. */ + formatExpiryWarning: (seconds: number) => string; + errorTitle: string; + sessionCreateErrorMessage: string; + pollingErrorMessage: string; + /** Rendered under the QR once files have arrived (e.g. a received-count badge). */ + renderReceived?: (count: number) => ReactNode; + qrSize?: number; +} + +export default function MobileTransferModal({ + opened, + onClose, + routePath, + onFileReceived, + title, + description, + instructions, + expiryWarningTitle, + formatExpiryWarning, + errorTitle, + sessionCreateErrorMessage, + pollingErrorMessage, + renderReceived, + qrSize = 240, +}: MobileTransferModalProps) { + const { config } = useAppConfig(); + + const { mobileUrl, filesReceived, error, timeRemaining, showExpiryWarning } = + useMobileTransferSession({ + active: opened, + routePath, + onFileReceived, + sessionCreateErrorMessage, + pollingErrorMessage, + // In dev the backend-advertised frontendUrl is the backend origin, which + // serves no SPA — the phone must open the Vite origin this page runs on, + // so let the URL builder fall back to it. An explicit server_url still + // wins, as an escape hatch. + configuredUrl: + localStorage.getItem("server_url") || + (import.meta.env.DEV ? "" : config?.frontendUrl || ""), + }); + + return ( + + + } + color="blue" + variant="light" + > + {description} + + + {showExpiryWarning && timeRemaining !== null && ( + } + title={expiryWarningTitle} + color="orange" + > + + {formatExpiryWarning(Math.ceil(timeRemaining / 1000))} + + + )} + + {error && ( + } + title={errorTitle} + color="red" + > + {error} + + )} + + + + + + + {filesReceived > 0 && renderReceived?.(filesReceived)} + + + {instructions} + + + + {mobileUrl} + + + + + ); +} diff --git a/frontend/editor/src/core/components/shared/MobileUploadModal.tsx b/frontend/editor/src/core/components/shared/MobileUploadModal.tsx index 76557d2b77..64ff82cb9f 100644 --- a/frontend/editor/src/core/components/shared/MobileUploadModal.tsx +++ b/frontend/editor/src/core/components/shared/MobileUploadModal.tsx @@ -1,17 +1,10 @@ -import { useEffect, useCallback, useState, useRef } from "react"; -import { Modal, Stack, Text, Badge, Box, Alert } from "@mantine/core"; -import { QRCodeSVG } from "qrcode.react"; +import { useCallback } from "react"; +import { Badge } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { useAppConfig } from "@app/contexts/AppConfigContext"; -import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; -import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; -import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; -import { BASE_PATH } from "@app/constants/app"; -import { buildMobileScannerUrl } from "@app/utils/mobileScannerUrl"; +import MobileTransferModal from "@app/components/shared/MobileTransferModal"; import { convertImageToPdf, isImageFile } from "@app/utils/imageToPdfUtils"; -import apiClient from "@app/services/apiClient"; interface MobileUploadModalProps { opened: boolean; @@ -19,47 +12,6 @@ interface MobileUploadModalProps { onFilesReceived: (files: File[]) => void; } -// Generate a cryptographically secure UUID v4-like session ID -function generateSessionId(): string { - // Use Web Crypto API for cryptographically secure random values - const cryptoObj = - typeof crypto !== "undefined" ? crypto : (window as any).crypto; - - if (cryptoObj && typeof cryptoObj.getRandomValues === "function") { - const bytes = new Uint8Array(16); - cryptoObj.getRandomValues(bytes); - - // Set version (4) and variant bits per RFC 4122 - bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 - bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 - - // Convert bytes to hex string in UUID format - const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")); - return [ - hex.slice(0, 4).join(""), - hex.slice(4, 6).join(""), - hex.slice(6, 8).join(""), - hex.slice(8, 10).join(""), - hex.slice(10, 16).join(""), - ].join("-"); - } - - // If Web Crypto is not available, fail fast rather than using insecure randomness - console.error( - "Web Crypto API not available. Cannot generate secure session ID.", - ); - throw new Error( - "Web Crypto API not available. Cannot generate secure session ID.", - ); -} - -interface SessionInfo { - sessionId: string; - createdAt: number; - expiresAt: number; - timeoutMs: number; -} - /** * MobileUploadModal * @@ -73,371 +25,103 @@ export default function MobileUploadModal({ }: MobileUploadModalProps) { const { t } = useTranslation(); const { config } = useAppConfig(); + const convertToPdf = config?.mobileScannerConvertToPdf !== false; - const [sessionId, setSessionId] = useState(() => generateSessionId()); - const [sessionInfo, setSessionInfo] = useState(null); - const [filesReceived, setFilesReceived] = useState(0); - const [error, setError] = useState(null); - const [timeRemaining, setTimeRemaining] = useState(null); - const [showExpiryWarning, setShowExpiryWarning] = useState(false); - const pollIntervalRef = useRef(null); - const timerIntervalRef = useRef(null); - const processedFiles = useRef>(new Set()); + const handleFileReceived = useCallback( + async (received: File) => { + let file = received; - // Build the QR-code URL the phone opens. It must land on the public - // /mobile-scanner route under the app's base path, otherwise the phone hits - // the auth-gated catch-all route and is bounced to the login page. - const mobileUrl = buildMobileScannerUrl({ - configuredUrl: - localStorage.getItem("server_url") || config?.frontendUrl || "", - sessionId, - origin: window.location.origin, - basePath: BASE_PATH, - }); - - // Create session on backend - const createSession = useCallback( - async (newSessionId: string) => { - try { - const response = await apiClient.post( - `/api/v1/mobile-scanner/create-session/${newSessionId}`, - undefined, - { - responseType: "json", - }, - ); - - if (!response.status || response.status !== 200) { - throw new Error("Failed to create session"); - } - - const data = response.data; - setSessionInfo(data); - setError(null); - console.log("[MobileUploadModal] Session created:", data); - } catch (err) { - console.error("[MobileUploadModal] Failed to create session:", err); - setError( - t("mobileUpload.sessionCreateError", "Failed to create session"), - ); - } - }, - [t], - ); - - // Regenerate session (when expired or warned) - const regenerateSession = useCallback(() => { - const newSessionId = generateSessionId(); - setSessionId(newSessionId); - setShowExpiryWarning(false); - setFilesReceived(0); - processedFiles.current.clear(); - createSession(newSessionId); - }, [createSession]); - - const pollForFiles = useCallback(async () => { - if (!opened) return; - - try { - const response = await apiClient.get( - `/api/v1/mobile-scanner/files/${sessionId}`, - ); - if (!response.status || response.status !== 200) { - throw new Error("Failed to check for files"); - } - - const data = response.data; - const files = data.files || []; - - // Download only files we haven't processed yet - const newFiles = files.filter( - (f: any) => !processedFiles.current.has(f.filename), - ); - - if (newFiles.length > 0) { - for (const fileMetadata of newFiles) { - try { - const downloadResponse = await apiClient.get( - `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, - { - responseType: "blob", - }, - ); - - if (downloadResponse.status === 200) { - const blob = downloadResponse.data; - let file = new File([blob], fileMetadata.filename, { - type: fileMetadata.contentType || "image/jpeg", - }); - - // Convert images to PDF if enabled - if ( - isImageFile(file) && - config?.mobileScannerConvertToPdf !== false - ) { - try { - file = await convertImageToPdf(file, { - imageResolution: config?.mobileScannerImageResolution as - | "full" - | "reduced" - | undefined, - pageFormat: config?.mobileScannerPageFormat as - | "keep" - | "A4" - | "letter" - | undefined, - stretchToFit: config?.mobileScannerStretchToFit, - }); - console.log( - "[MobileUploadModal] Converted image to PDF:", - file.name, - ); - } catch (convertError) { - console.warn( - "[MobileUploadModal] Failed to convert image to PDF, using original file:", - convertError, - ); - // Continue with original image file if conversion fails - } - } - - processedFiles.current.add(fileMetadata.filename); - setFilesReceived((prev) => prev + 1); - onFilesReceived([file]); - } - } catch (err) { - console.error( - "[MobileUploadModal] Failed to download file:", - fileMetadata.filename, - err, - ); - } - } - - // Delete the entire session immediately after downloading all files - // This ensures files are only on server for ~1 second + // Convert images to PDF if enabled + if (isImageFile(file) && convertToPdf) { try { - await apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`); - console.log( - "[MobileUploadModal] Session cleaned up after file download", - ); - } catch (cleanupErr) { + file = await convertImageToPdf(file, { + imageResolution: config?.mobileScannerImageResolution as + | "full" + | "reduced" + | undefined, + pageFormat: config?.mobileScannerPageFormat as + | "keep" + | "A4" + | "letter" + | undefined, + stretchToFit: config?.mobileScannerStretchToFit, + }); + } catch (convertError) { console.warn( - "[MobileUploadModal] Failed to cleanup session after download:", - cleanupErr, + "[MobileUploadModal] Failed to convert image to PDF, using original file:", + convertError, ); + // Continue with original image file if conversion fails } } - } catch (err) { - console.error("[MobileUploadModal] Error polling for files:", err); - setError(t("mobileUpload.pollingError", "Error checking for files")); - } - }, [opened, sessionId, onFilesReceived, t]); - // Create session when modal opens - useEffect(() => { - if (opened) { - createSession(sessionId); - setFilesReceived(0); - setError(null); - setShowExpiryWarning(false); - processedFiles.current.clear(); - } - }, [opened, sessionId]); // Only run when opened changes - - useEffect(() => { - if (!opened) return; - - createSession(sessionId); - setFilesReceived(0); - setError(null); - setShowExpiryWarning(false); - processedFiles.current.clear(); - - return () => { - console.log("Cleaning up session on unmount/close:", sessionId); - apiClient - .delete(`/api/v1/mobile-scanner/session/${sessionId}`) - .catch((err) => - console.warn("[MobileUploadModal] Cleanup failed:", err), - ); - }; - }, [opened, sessionId, createSession]); - - // Start polling for files when modal opens - useEffect(() => { - if (opened && sessionInfo) { - // Poll every 2 seconds - pollIntervalRef.current = window.setInterval(pollForFiles, 2000); - - // Initial poll - pollForFiles(); - } else { - // Stop polling when modal closes - if (pollIntervalRef.current) { - clearInterval(pollIntervalRef.current); - pollIntervalRef.current = null; - } - } - - return () => { - if (pollIntervalRef.current) { - clearInterval(pollIntervalRef.current); - } - }; - }, [opened, sessionInfo, pollForFiles]); - - // Session timeout timer - useEffect(() => { - if (!opened || !sessionInfo) return; - - const updateTimer = () => { - const now = Date.now(); - const remaining = sessionInfo.expiresAt - now; - - if (remaining <= 0) { - // Session expired - regenerate - setShowExpiryWarning(false); - regenerateSession(); - } else if (remaining <= 60000 && !showExpiryWarning) { - // Less than 1 minute remaining - show warning - setShowExpiryWarning(true); - } - - setTimeRemaining(Math.max(0, remaining)); - }; - - // Update immediately - updateTimer(); - - // Update every second - timerIntervalRef.current = window.setInterval(updateTimer, 1000); - - return () => { - if (timerIntervalRef.current) { - clearInterval(timerIntervalRef.current); - } - }; - }, [opened, sessionInfo, showExpiryWarning, regenerateSession]); + onFilesReceived([file]); + }, + [config, convertToPdf, onFilesReceived], + ); return ( - - - } - color="blue" - variant="light" + description={ + convertToPdf + ? t( + "mobileUpload.description", + "Scan this QR code with your mobile device to upload photos. Images will be automatically converted to PDF.", + ) + : t( + "mobileUpload.descriptionNoConvert", + "Scan this QR code with your mobile device to upload photos.", + ) + } + instructions={ + convertToPdf + ? t( + "mobileUpload.instructions", + "Open the camera app on your phone and scan this code. Images will be automatically converted to PDF.", + ) + : t( + "mobileUpload.instructionsNoConvert", + "Open the camera app on your phone and scan this code. Files will be uploaded through the server.", + ) + } + expiryWarningTitle={t( + "mobileUpload.expiryWarning", + "Session Expiring Soon", + )} + formatExpiryWarning={(seconds) => + t( + "mobileUpload.expiryWarningMessage", + "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.", + { seconds }, + ) + } + errorTitle={t("mobileUpload.error", "Connection Error")} + sessionCreateErrorMessage={t( + "mobileUpload.sessionCreateError", + "Failed to create session", + )} + pollingErrorMessage={t( + "mobileUpload.pollingError", + "Error checking for files", + )} + renderReceived={(count) => ( + } > - - {config?.mobileScannerConvertToPdf !== false - ? t( - "mobileUpload.description", - "Scan this QR code with your mobile device to upload photos. Images will be automatically converted to PDF.", - ) - : t( - "mobileUpload.descriptionNoConvert", - "Scan this QR code with your mobile device to upload photos.", - )} - - - - {showExpiryWarning && timeRemaining !== null && ( - } - title={t("mobileUpload.expiryWarning", "Session Expiring Soon")} - color="orange" - > - - {t( - "mobileUpload.expiryWarningMessage", - "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.", - { seconds: Math.ceil(timeRemaining / 1000) }, - )} - - - )} - - {error && ( - } - title={t("mobileUpload.error", "Connection Error")} - color="red" - > - {error} - - )} - - - - - - - {filesReceived > 0 && ( - } - > - {t("mobileUpload.filesReceived", "{{count}} file(s) received", { - count: filesReceived, - })} - - )} - - - {config?.mobileScannerConvertToPdf !== false - ? t( - "mobileUpload.instructions", - "Open the camera app on your phone and scan this code. Images will be automatically converted to PDF.", - ) - : t( - "mobileUpload.instructionsNoConvert", - "Open the camera app on your phone and scan this code. Files will be uploaded through the server.", - )} - - - - {mobileUrl} - - - - + {t("mobileUpload.filesReceived", "{{count}} file(s) received", { + count, + })} + + )} + /> ); } diff --git a/frontend/editor/src/core/components/tools/sign/MobileSignatureModal.test.tsx b/frontend/editor/src/core/components/tools/sign/MobileSignatureModal.test.tsx new file mode 100644 index 0000000000..c8607d55c4 --- /dev/null +++ b/frontend/editor/src/core/components/tools/sign/MobileSignatureModal.test.tsx @@ -0,0 +1,176 @@ +/** + * Receive-flow contract for the phone-signature QR modal. + * + * The transfer session's upload endpoint accepts any file from anyone holding + * the QR URL, so the modal must treat arrivals as untrusted. Images become + * draw/photo payloads by filename prefix, a signature-text JSON payload is + * parsed and clamped field by field, and anything else is ignored. + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import MobileSignatureModal, { + type MobileSignaturePayload, +} from "@app/components/tools/sign/MobileSignatureModal"; +import apiClient from "@app/services/apiClient"; +import { expectConsole } from "@app/tests/failOnConsole"; + +// Render the English fallbacks (the test i18n instance has no loaded locale). +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: unknown) => + typeof fallback === "string" ? fallback : key, + }), +})); + +vi.mock("@app/services/apiClient", () => ({ + default: { + defaults: { baseURL: "http://localhost:8080" }, + post: vi.fn(), + get: vi.fn(), + delete: vi.fn(), + }, +})); + +vi.mock("@app/contexts/AppConfigContext", () => ({ + useAppConfig: () => ({ config: { enableMobileSignature: true } }), +})); + +const mockedApi = vi.mocked(apiClient, true); + +const SESSION_INFO = { + sessionId: "s", + createdAt: Date.now(), + expiresAt: Date.now() + 600_000, + timeoutMs: 600_000, +}; + +function primeSession( + files: Array<{ filename: string; contentType: string; body?: string }>, +) { + mockedApi.post.mockResolvedValue({ + status: 200, + data: SESSION_INFO, + } as never); + mockedApi.delete.mockResolvedValue({ status: 200 } as never); + mockedApi.get.mockImplementation(((url: string, config?: unknown) => { + if (url.includes("/files/")) { + return Promise.resolve({ status: 200, data: { files } } as never); + } + if (url.includes("/download/")) { + const filename = url.split("/").pop() ?? ""; + const meta = files.find((f) => f.filename === filename); + return Promise.resolve({ + status: 200, + data: new Blob([meta?.body ?? "fake-bytes"], { + type: meta?.contentType, + }), + config, + } as never); + } + return Promise.reject(new Error(`unexpected GET ${url}`)); + }) as never); +} + +function renderModal( + onSignatureReceived: (payload: MobileSignaturePayload) => void, + onClose: () => void, +) { + return render( + + + , + ); +} + +describe("MobileSignatureModal", () => { + // clearAllMocks (not restoreAllMocks): the hook's unmount cleanup still + // calls apiClient.delete during test teardown, so implementations must + // survive until React Testing Library's auto-cleanup has unmounted. + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("hands a drawn signature to the caller as a draw payload and closes", async () => { + primeSession([ + { filename: "signature-draw-1.png", contentType: "image/png" }, + ]); + const onSignatureReceived = vi.fn(); + const onClose = vi.fn(); + + renderModal(onSignatureReceived, onClose); + + await waitFor(() => expect(onSignatureReceived).toHaveBeenCalledTimes(1)); + const payload = onSignatureReceived.mock.calls[0][0]; + expect(payload.kind).toBe("draw"); + expect(payload.dataUrl).toMatch(/^data:image\/png/); + expect(onClose).toHaveBeenCalled(); + }); + + it("routes a photographed signature as a photo payload", async () => { + primeSession([ + { filename: "signature-photo-1.jpg", contentType: "image/jpeg" }, + ]); + const onSignatureReceived = vi.fn(); + + renderModal(onSignatureReceived, vi.fn()); + + await waitFor(() => expect(onSignatureReceived).toHaveBeenCalledTimes(1)); + expect(onSignatureReceived.mock.calls[0][0].kind).toBe("photo"); + }); + + it("parses a typed signature as text, clamping unknown font and colour", async () => { + primeSession([ + { + filename: "signature-text-1.json", + contentType: "application/json", + body: JSON.stringify({ + text: " Reece ", + fontFamily: "Wingdings", + color: "javascript:alert(1)", + }), + }, + ]); + const onSignatureReceived = vi.fn(); + const onClose = vi.fn(); + + renderModal(onSignatureReceived, onClose); + + await waitFor(() => expect(onSignatureReceived).toHaveBeenCalledTimes(1)); + expect(onSignatureReceived.mock.calls[0][0]).toEqual({ + kind: "text", + text: "Reece", + fontFamily: "Helvetica", + color: "#000000", + }); + expect(onClose).toHaveBeenCalled(); + }); + + it("ignores a non-image upload instead of setting it as the signature", async () => { + // Rejecting the upload logs a warning - that's the contract under test. + expectConsole.warn(/Ignoring non-image upload/); + primeSession([{ filename: "evil.html", contentType: "text/html" }]); + const onSignatureReceived = vi.fn(); + const onClose = vi.fn(); + + renderModal(onSignatureReceived, onClose); + + // The poll + download cycle must have run before we assert the negative. + await waitFor(() => + expect( + mockedApi.get.mock.calls.some(([url]) => + String(url).includes("/download/"), + ), + ).toBe(true), + ); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(onSignatureReceived).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/editor/src/core/components/tools/sign/MobileSignatureModal.tsx b/frontend/editor/src/core/components/tools/sign/MobileSignatureModal.tsx new file mode 100644 index 0000000000..82deae8b5f --- /dev/null +++ b/frontend/editor/src/core/components/tools/sign/MobileSignatureModal.tsx @@ -0,0 +1,155 @@ +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import MobileTransferModal from "@app/components/shared/MobileTransferModal"; + +/** + * What the phone sent, routed to the matching signature source: ink and + * photos as pixels, typed signatures as data so they stay editable text. + */ +export type MobileSignaturePayload = + | { kind: "draw"; dataUrl: string } + | { kind: "photo"; dataUrl: string } + | { kind: "text"; text: string; fontFamily: string; color: string }; + +/** Fonts the sign tool's text mode offers; anything else falls back. */ +const TEXT_FONTS = new Set([ + "Helvetica", + "Times-Roman", + "Courier", + "Arial", + "Georgia", +]); +const HEX_COLOR = /^#[0-9a-fA-F]{6}$/; +const MAX_TEXT_LENGTH = 200; + +interface MobileSignatureModalProps { + opened: boolean; + onClose: () => void; + onSignatureReceived: (payload: MobileSignaturePayload) => void; +} + +/** FileReader-based (rather than File.text/arrayBuffer, absent in jsdom). */ +function readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result ?? "")); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); +} + +/** + * QR modal for creating a signature on a phone or tablet. The phone opens the + * public `/mobile-sign` page; the first valid arrival becomes the signature + * and the modal closes. + */ +export default function MobileSignatureModal({ + opened, + onClose, + onSignatureReceived, +}: MobileSignatureModalProps) { + const { t } = useTranslation(); + + // The session endpoints accept any upload from anyone holding the QR URL, + // so nothing here is trusted: images pass as pixels, a typed signature is + // parsed and clamped field by field, everything else is ignored. + const handleFileReceived = useCallback( + async (file: File) => { + if ( + file.type === "application/json" && + file.name.startsWith("signature-text") + ) { + try { + const parsed: unknown = JSON.parse(await readFileAsText(file)); + const record = parsed as Record; + const text = + typeof record?.text === "string" + ? record.text.trim().slice(0, MAX_TEXT_LENGTH) + : ""; + if (!text) return; + onSignatureReceived({ + kind: "text", + text, + fontFamily: TEXT_FONTS.has(record.fontFamily as string) + ? (record.fontFamily as string) + : "Helvetica", + color: HEX_COLOR.test(record.color as string) + ? (record.color as string) + : "#000000", + }); + onClose(); + } catch { + console.warn( + "[MobileSignatureModal] Ignoring malformed text payload", + ); + } + return; + } + + if (!file.type.startsWith("image/")) { + console.warn( + "[MobileSignatureModal] Ignoring non-image upload:", + file.type, + ); + return; + } + + await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (event) => { + const dataUrl = event.target?.result; + if (typeof dataUrl === "string") { + onSignatureReceived({ + kind: file.name.startsWith("signature-photo") ? "photo" : "draw", + dataUrl, + }); + onClose(); + } + resolve(); + }; + reader.onerror = () => resolve(); + reader.readAsDataURL(file); + }); + }, + [onSignatureReceived, onClose], + ); + + return ( + + t( + "sign.mobile.expiryWarningMessage", + "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.", + { seconds }, + ) + } + errorTitle={t("sign.mobile.error", "Connection Error")} + sessionCreateErrorMessage={t( + "sign.mobile.sessionCreateError", + "Failed to create session", + )} + pollingErrorMessage={t( + "sign.mobile.pollingError", + "Error checking for the signature", + )} + /> + ); +} diff --git a/frontend/editor/src/core/components/tools/sign/SignSettings.tsx b/frontend/editor/src/core/components/tools/sign/SignSettings.tsx index 648ada1c47..4aa9f8ffd3 100644 --- a/frontend/editor/src/core/components/tools/sign/SignSettings.tsx +++ b/frontend/editor/src/core/components/tools/sign/SignSettings.tsx @@ -34,6 +34,11 @@ import { AddSignatureResult, } from "@app/hooks/tools/sign/useSavedSignatures"; import { SavedSignaturesSection } from "@app/components/tools/sign/SavedSignaturesSection"; +import MobileSignatureModal, { + type MobileSignaturePayload, +} from "@app/components/tools/sign/MobileSignatureModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useIsMobile } from "@app/hooks/useIsMobile"; import { buildSignaturePreview } from "@app/utils/signaturePreview"; type SignatureDrafts = { @@ -116,6 +121,12 @@ const SignSettings = ({ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [isPlacementManuallyPaused, setPlacementManuallyPaused] = useState(false); + const [isMobileSignModalOpen, setIsMobileSignModalOpen] = useState(false); + const { config } = useAppConfig(); + const isMobileViewport = useIsMobile(); + // Drawing on a phone needs a second device, so the QR entry is desktop-only. + const canDrawOnPhone = + Boolean(config?.enableMobileSignature) && !isMobileViewport; // State for different signature types const [canvasSignatureData, setCanvasSignatureData] = useState< @@ -625,6 +636,71 @@ const SignSettings = ({ [onActivateSignaturePlacement], ); + // Route a signature made on a phone to the matching source, mirroring + // handleUseSavedSignature: ink is a canvas signature, a photo is an image + // signature, and typed text stays editable text rather than baked pixels. + const handleMobileSignatureReceived = useCallback( + (payload: MobileSignaturePayload) => { + // Receiving a signature is as clear an intent to place it as drawing + // one, so placement goes live even if it was paused beforehand. + setPlacementManuallyPaused(false); + lastAppliedPlacementKey.current = null; + if (payload.kind === "draw") { + if (parameters.signatureType !== "canvas") { + onParameterChange("signatureType", "canvas"); + } + handleCanvasSignatureChange(payload.dataUrl); + } else if (payload.kind === "photo") { + if (parameters.signatureType !== "image") { + onParameterChange("signatureType", "image"); + } + setImageSignatureData(payload.dataUrl); + } else { + if (parameters.signatureType !== "text") { + onParameterChange("signatureType", "text"); + } + onParameterChange("signerName", payload.text); + onParameterChange("fontFamily", payload.fontFamily); + onParameterChange("textColor", payload.color); + // Move the draft mirror in the same commit as the parameters. The + // record/restore effect pair otherwise sees them one commit apart and + // ping-pongs old draft against new params, wiping the received text + // and looping until React aborts the update depth. + const nextDraft = { + signerName: payload.text, + fontSize: parameters.fontSize ?? 16, + fontFamily: payload.fontFamily, + textColor: payload.color, + }; + lastSyncedTextDraft.current = nextDraft; + setSignatureDrafts((prev) => ({ ...prev, text: nextDraft })); + } + // Activate directly for every kind: the canvas-change handler only + // activates when the data actually changed, and the auto-activate + // effect only reacts to state transitions - neither fires for a + // repeat of the same signature. Fired twice because the first shot can + // land between the receive commit and the ready-state settling, where + // the placement effect immediately deactivates it; the second shot is + // after everything has settled, and re-activating is idempotent. + if (typeof window !== "undefined") { + window.setTimeout( + () => onActivateSignaturePlacement?.(), + PLACEMENT_ACTIVATION_DELAY, + ); + window.setTimeout(() => onActivateSignaturePlacement?.(), 500); + } else { + onActivateSignaturePlacement?.(); + } + }, + [ + parameters.signatureType, + parameters.fontSize, + onParameterChange, + handleCanvasSignatureChange, + onActivateSignaturePlacement, + ], + ); + const hasCanvasSignature = useMemo( () => Boolean(canvasSignatureData), [canvasSignatureData], @@ -980,6 +1056,29 @@ const SignSettings = ({ if (signatureSource === "image") { return ( + {imageSignatureData && ( + + {translate("image.previewAlt", + + )} + {canDrawOnPhone && ( + <> + + setIsMobileSignModalOpen(false)} + onSignatureReceived={handleMobileSignatureReceived} + /> + + )} {sourceOptions.length > 1 && ( b.toString(16).padStart(2, "0")); + return [ + hex.slice(0, 4).join(""), + hex.slice(4, 6).join(""), + hex.slice(6, 8).join(""), + hex.slice(8, 10).join(""), + hex.slice(10, 16).join(""), + ].join("-"); + } + + // If Web Crypto is not available, fail fast rather than using insecure randomness + throw new Error( + "Web Crypto API not available. Cannot generate secure session ID.", + ); +} + +export interface MobileTransferSessionInfo { + sessionId: string; + createdAt: number; + expiresAt: number; + timeoutMs: number; +} + +interface UseMobileTransferSessionParams { + /** Session exists and polling runs only while true (modal open). */ + active: boolean; + /** SPA route the phone opens, without slashes: "mobile-scanner", "mobile-sign". */ + routePath: string; + /** Called once per newly uploaded file, in upload order. */ + onFileReceived: (file: File) => void | Promise; + /** Message shown when the backend refuses to create a session. */ + sessionCreateErrorMessage: string; + /** Message shown when polling for uploads fails. */ + pollingErrorMessage: string; + /** Host the phone should reach, when configured (server_url / frontendUrl). */ + configuredUrl?: string; +} + +export function useMobileTransferSession({ + active, + routePath, + onFileReceived, + sessionCreateErrorMessage, + pollingErrorMessage, + configuredUrl, +}: UseMobileTransferSessionParams) { + const [sessionId, setSessionId] = useState(() => generateSessionId()); + const [sessionInfo, setSessionInfo] = + useState(null); + const [filesReceived, setFilesReceived] = useState(0); + const [error, setError] = useState(null); + const [timeRemaining, setTimeRemaining] = useState(null); + const [showExpiryWarning, setShowExpiryWarning] = useState(false); + const pollIntervalRef = useRef(null); + const timerIntervalRef = useRef(null); + const processedFiles = useRef>(new Set()); + + // The QR-code URL the phone opens. It must land on the public route under + // the app's base path, otherwise the phone hits the auth-gated catch-all + // route and is bounced to the login page. + const mobileUrl = buildMobileRouteUrl({ + configuredUrl: configuredUrl ?? "", + sessionId, + origin: window.location.origin, + basePath: BASE_PATH, + routePath, + }); + + const createSession = useCallback( + async (newSessionId: string) => { + try { + const response = await apiClient.post( + `/api/v1/mobile-scanner/create-session/${newSessionId}`, + undefined, + { responseType: "json" }, + ); + + if (!response.status || response.status !== 200) { + throw new Error("Failed to create session"); + } + + setSessionInfo(response.data); + setError(null); + } catch (err) { + console.error("[useMobileTransferSession] create failed:", err); + setError(sessionCreateErrorMessage); + } + }, + [sessionCreateErrorMessage], + ); + + // Regenerate session (when expired or warned) + const regenerateSession = useCallback(() => { + const newSessionId = generateSessionId(); + setSessionId(newSessionId); + setShowExpiryWarning(false); + setFilesReceived(0); + processedFiles.current.clear(); + createSession(newSessionId); + }, [createSession]); + + const pollForFiles = useCallback(async () => { + if (!active) return; + + try { + const response = await apiClient.get( + `/api/v1/mobile-scanner/files/${sessionId}`, + ); + if (!response.status || response.status !== 200) { + throw new Error("Failed to check for files"); + } + + const files = response.data.files || []; + + // Download only files we haven't processed yet + const newFiles = files.filter( + (f: any) => !processedFiles.current.has(f.filename), + ); + if (newFiles.length === 0) return; + + for (const fileMetadata of newFiles) { + try { + const downloadResponse = await apiClient.get( + `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, + { responseType: "blob" }, + ); + + if (downloadResponse.status === 200) { + const file = new File( + [downloadResponse.data], + fileMetadata.filename, + { type: fileMetadata.contentType || "image/jpeg" }, + ); + processedFiles.current.add(fileMetadata.filename); + setFilesReceived((prev) => prev + 1); + await onFileReceived(file); + } + } catch (err) { + console.error( + "[useMobileTransferSession] download failed:", + fileMetadata.filename, + err, + ); + } + } + + // Delete the entire session immediately after downloading, so uploads + // sit on the server only for the seconds between polls. + try { + await apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`); + } catch (cleanupErr) { + console.warn( + "[useMobileTransferSession] post-download cleanup failed:", + cleanupErr, + ); + } + } catch (err) { + console.error("[useMobileTransferSession] polling failed:", err); + setError(pollingErrorMessage); + } + }, [active, sessionId, onFileReceived, pollingErrorMessage]); + + // Create the session while active; delete it when deactivated/unmounted. + useEffect(() => { + if (!active) return; + + createSession(sessionId); + setFilesReceived(0); + setError(null); + setShowExpiryWarning(false); + processedFiles.current.clear(); + + return () => { + apiClient + .delete(`/api/v1/mobile-scanner/session/${sessionId}`) + .catch((err) => + console.warn("[useMobileTransferSession] cleanup failed:", err), + ); + }; + }, [active, sessionId, createSession]); + + // Poll for uploads while the session is live + useEffect(() => { + if (active && sessionInfo) { + pollIntervalRef.current = window.setInterval(pollForFiles, 2000); + pollForFiles(); + } else if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + + return () => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + }; + }, [active, sessionInfo, pollForFiles]); + + // Session timeout timer: warn under a minute, regenerate on expiry + useEffect(() => { + if (!active || !sessionInfo) return; + + const updateTimer = () => { + const now = Date.now(); + const remaining = sessionInfo.expiresAt - now; + + if (remaining <= 0) { + setShowExpiryWarning(false); + regenerateSession(); + } else if (remaining <= 60000 && !showExpiryWarning) { + setShowExpiryWarning(true); + } + + setTimeRemaining(Math.max(0, remaining)); + }; + + updateTimer(); + timerIntervalRef.current = window.setInterval(updateTimer, 1000); + + return () => { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + } + }; + }, [active, sessionInfo, showExpiryWarning, regenerateSession]); + + return { + /** URL to encode in the QR code. */ + mobileUrl, + sessionInfo, + /** Count of files received this session (resets on regenerate). */ + filesReceived, + error, + /** Milliseconds until the session expires, once known. */ + timeRemaining, + /** True inside the final minute before expiry. */ + showExpiryWarning, + regenerateSession, + }; +} diff --git a/frontend/editor/src/core/pages/MobileSignPage.test.tsx b/frontend/editor/src/core/pages/MobileSignPage.test.tsx new file mode 100644 index 0000000000..84bcf1b736 --- /dev/null +++ b/frontend/editor/src/core/pages/MobileSignPage.test.tsx @@ -0,0 +1,100 @@ +/** + * Session-state contract for the phone-side signature page: a missing or + * expired session shows one clear error instead of a canvas whose Send would + * fail; a valid session shows the draw/type/photo tabs. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { MantineProvider } from "@mantine/core"; +import MobileSignPage from "@app/pages/MobileSignPage"; + +vi.mock("@app/services/apiClient", () => ({ + default: { defaults: { baseURL: "http://localhost:8080" } }, +})); + +// Render the English fallbacks the assertions read (the test i18n instance +// has no loaded locale, so bare t() would render raw keys). +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: unknown) => + typeof fallback === "string" ? fallback : key, + }), +})); + +// Branding components pull theme preferences from providers this page doesn't +// need for its session-state contract. +vi.mock("@app/components/shared/LogoIcon", () => ({ + LogoIcon: () => , +})); +vi.mock("@app/components/shared/Wordmark", () => ({ + Wordmark: () => , +})); + +function renderAt(path: string) { + return render( + + + + + , + ); +} + +describe("MobileSignPage", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + // jsdom has no ResizeObserver; the draw canvas sizes itself with one. + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); + // jsdom's canvas has no real 2d context (and logs an error when asked); + // the draw canvas tolerates a null context. + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("shows the expired-session error when the URL has no session", async () => { + renderAt("/mobile-sign"); + + await waitFor(() => + expect(screen.getByText(/Session expired/i)).toBeInTheDocument(), + ); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("shows the expired-session error when the backend rejects the session", async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: false, + json: async () => ({ valid: false }), + } as Response); + + renderAt("/mobile-sign?session=stale-session"); + + await waitFor(() => + expect(screen.getByText(/Session expired/i)).toBeInTheDocument(), + ); + }); + + it("shows the signature tabs once the session validates", async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ valid: true }), + } as Response); + + renderAt("/mobile-sign?session=good-session"); + + await waitFor(() => expect(screen.getByText("Draw")).toBeInTheDocument()); + expect(screen.getByText("Type")).toBeInTheDocument(); + expect(screen.getByText("Photo")).toBeInTheDocument(); + expect(screen.getByText(/Send to computer/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/core/pages/MobileSignPage.tsx b/frontend/editor/src/core/pages/MobileSignPage.tsx new file mode 100644 index 0000000000..fd4034e0ba --- /dev/null +++ b/frontend/editor/src/core/pages/MobileSignPage.tsx @@ -0,0 +1,554 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { + Alert, + Box, + Card, + Group, + Image, + Select, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useMediaQuery } from "@mantine/hooks"; +import { Button as DSButton } from "@app/ui/Button"; +import { SegmentedControl } from "@app/ui/SegmentedControl"; +import { useTranslation } from "react-i18next"; +import { LogoIcon } from "@app/components/shared/LogoIcon"; +import { Wordmark } from "@app/components/shared/Wordmark"; +import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded"; +import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; +import UndoRoundedIcon from "@mui/icons-material/UndoRounded"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded"; +import PhotoCameraRoundedIcon from "@mui/icons-material/PhotoCameraRounded"; +import SendRoundedIcon from "@mui/icons-material/SendRounded"; +import { + MobileDrawCanvas, + type MobileDrawCanvasHandle, +} from "@app/components/mobileSign/MobileDrawCanvas"; +import apiClient from "@app/services/apiClient"; + +// Use the configured API base (e.g. api.stirling.com), not the page origin. +const API_BASE = (apiClient.defaults.baseURL ?? "").replace(/\/+$/, ""); + +type SignatureTab = "draw" | "type" | "photo"; + +// Ink pigments, not UI theme colours: they are baked into the exported PNG +// and transferred to the desktop, so they must be fixed literals. +const INK_COLORS = [ + { value: "#101010", label: "black" }, // theme-allow-color ink pigment, serialized into the signature + { value: "#1d4ed8", label: "blue" }, // theme-allow-color ink pigment, serialized into the signature +]; + +const PEN_SIZES = [ + { value: 2, label: "S" }, + { value: 3.5, label: "M" }, + { value: 6, label: "L" }, +]; + +/** + * The sign tool's own text-mode fonts, so a typed signature transfers as + * data and stays editable there. `css` approximates each for the on-phone + * preview; `value` is what the desktop's font parameter understands. + */ +const TYPE_FONTS = [ + { + value: "Helvetica", + css: "Helvetica, Arial, sans-serif", + label: "Helvetica", + }, + { + value: "Times-Roman", + css: "'Times New Roman', Times, serif", + label: "Times", + }, + { + value: "Courier", + css: "'Courier New', Courier, monospace", + label: "Courier", + }, + { value: "Arial", css: "Arial, sans-serif", label: "Arial" }, + { value: "Georgia", css: "Georgia, serif", label: "Georgia" }, +]; + +async function dataUrlToBlob(dataUrl: string): Promise { + const response = await fetch(dataUrl); + return response.blob(); +} + +/** + * MobileSignPage + * + * Phone-side page for sending a signature to the desktop: draw one (the main + * path), type one, or photograph one. Reached by scanning the QR code shown in + * the editor's Sign tool; the session comes from the QR URL and rides the same + * transfer backend as the mobile scanner. + */ +export default function MobileSignPage() { + const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const sessionId = searchParams.get("session"); + // Landscape phones (not tablets — hence the height cap) get a compact + // layout: branding hidden, tighter padding, shorter pad, so the canvas and + // the Send button fit on screen together. + const compactLandscape = + useMediaQuery("(orientation: landscape) and (max-height: 32rem)") ?? false; + + const [sessionValid, setSessionValid] = useState(null); + const [tab, setTab] = useState("draw"); + const [hasInk, setHasInk] = useState(false); + const [typedText, setTypedText] = useState(""); + const [typeFont, setTypeFont] = useState(TYPE_FONTS[0].value); + const [inkColor, setInkColor] = useState(INK_COLORS[0].value); + const [penSize, setPenSize] = useState(PEN_SIZES[1].value); + const [photoDataUrl, setPhotoDataUrl] = useState(null); + const [photoError, setPhotoError] = useState(null); + const [isSending, setIsSending] = useState(false); + const [sendError, setSendError] = useState(null); + const [sentCount, setSentCount] = useState(0); + const [justSent, setJustSent] = useState(false); + + const canvasHandle = useRef(null); + const photoInputRef = useRef(null); + const cameraInputRef = useRef(null); + + // Validate the session up front, so a stale QR shows one clear error rather + // than a canvas whose Send fails. + useEffect(() => { + if (!sessionId) { + setSessionValid(false); + return; + } + (async () => { + try { + const response = await fetch( + `${API_BASE}/api/v1/mobile-scanner/validate-session/${sessionId}`, + ); + const data = response.ok ? await response.json() : null; + setSessionValid(Boolean(data?.valid)); + } catch { + setSessionValid(false); + } + })(); + }, [sessionId]); + + const handlePhotoSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + if (!file.type.startsWith("image/")) { + setPhotoError( + t("mobileSign.photo.invalidType", "Please choose an image file."), + ); + return; + } + setPhotoError(null); + const reader = new FileReader(); + reader.onload = (event) => + setPhotoDataUrl((event.target?.result as string) ?? null); + reader.readAsDataURL(file); + }; + + /** + * What this tab sends: ink and photos as image files, typed signatures as a + * JSON payload (text + font + colour) so the desktop keeps them editable in + * the sign tool's text mode. The filename prefix tells the desktop which + * source the signature belongs to. + */ + const buildUpload = useCallback(async (): Promise<{ + blob: Blob; + filename: string; + } | null> => { + if (tab === "draw") { + const dataUrl = canvasHandle.current?.exportPng(); + if (!dataUrl) return null; + return { + blob: await dataUrlToBlob(dataUrl), + filename: `signature-draw-${Date.now()}.png`, + }; + } + if (tab === "type") { + const text = typedText.trim(); + if (!text) return null; + const payload = JSON.stringify({ + text, + fontFamily: typeFont, + color: inkColor, + }); + return { + blob: new Blob([payload], { type: "application/json" }), + filename: `signature-text-${Date.now()}.json`, + }; + } + if (!photoDataUrl) return null; + const blob = await dataUrlToBlob(photoDataUrl); + const extension = blob.type === "image/jpeg" ? "jpg" : "png"; + return { + blob, + filename: `signature-photo-${Date.now()}.${extension}`, + }; + }, [tab, typedText, typeFont, inkColor, photoDataUrl]); + + const canSend = + (tab === "draw" && hasInk) || + (tab === "type" && typedText.trim().length > 0) || + (tab === "photo" && photoDataUrl !== null); + + const handleSend = async () => { + if (!sessionId) return; + + setIsSending(true); + setSendError(null); + try { + const upload = await buildUpload(); + if (!upload) return; + const formData = new FormData(); + formData.append("files", upload.blob, upload.filename); + + const response = await fetch( + `${API_BASE}/api/v1/mobile-scanner/upload/${sessionId}`, + { method: "POST", body: formData }, + ); + if (!response.ok) { + throw new Error(`Upload failed with status ${response.status}`); + } + + setSentCount((count) => count + 1); + setJustSent(true); + // Reset the inputs so "send another" starts clean + canvasHandle.current?.clear(); + setTypedText(""); + setPhotoDataUrl(null); + if (photoInputRef.current) photoInputRef.current.value = ""; + if (cameraInputRef.current) cameraInputRef.current.value = ""; + } catch (err) { + console.error("[MobileSignPage] upload failed:", err); + setSendError( + t( + "mobileSign.sendError", + "Could not send the signature. Check the connection and try again.", + ), + ); + } finally { + setIsSending(false); + } + }; + + const header = ( + + + + + ); + + if (sessionValid === null) { + return ( + + {header} + + {t("mobileSign.validating", "Checking session…")} + + + ); + } + + if (!sessionValid) { + return ( + + {header} + } + color="red" + title={t("mobileSign.invalidSession", "Session expired")} + > + {t( + "mobileSign.invalidSessionMessage", + "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code.", + )} + + + ); + } + + return ( + + {!compactLandscape && header} + + {justSent && ( + } + color="green" + mb="sm" + withCloseButton + onClose={() => setJustSent(false)} + > + {t( + "mobileSign.sentMessage", + "Signature sent to your computer. You can send another or close this page.", + )} + + )} + {sendError && ( + } + color="red" + mb="sm" + > + {sendError} + + )} + + + + fullWidth + value={tab} + onChange={setTab} + ariaLabel={t("mobileSign.tabsLabel", "Signature source")} + options={[ + // Same order as the sign tool's sources: canvas, image, text + { value: "draw", label: t("mobileSign.tab.draw", "Draw") }, + { value: "photo", label: t("mobileSign.tab.photo", "Photo") }, + { value: "type", label: t("mobileSign.tab.type", "Type") }, + ]} + /> + + + {tab === "draw" && ( + + + + + + + + {INK_COLORS.map((color) => ( + setInkColor(color.value)} + aria-label={color.label} + style={{ + width: 32, + height: 32, + borderRadius: "50%", + background: color.value, + cursor: "pointer", + border: + inkColor === color.value + ? "3px solid var(--mantine-color-blue-4)" + : "3px solid transparent", + }} + /> + ))} + setPenSize(Number(value))} + ariaLabel={t("mobileSign.penSizeLabel", "Pen size")} + options={PEN_SIZES.map((size) => ({ + value: String(size.value), + label: size.label, + }))} + /> + + + canvasHandle.current?.undo()} + leftSection={} + > + {t("mobileSign.undo", "Undo")} + + canvasHandle.current?.clear()} + leftSection={ + + } + > + {t("mobileSign.clear", "Clear")} + + + + + )} + + {tab === "type" && ( + + setTypedText(e.target.value)} + placeholder={t("mobileSign.type.placeholder", "Your name")} + autoComplete="name" + /> + + + + )} + + + } + > + {sentCount > 0 + ? t("mobileSign.sendAnother", "Send another signature") + : t("mobileSign.send", "Send to computer")} + + + + ); +} diff --git a/frontend/editor/src/core/types/appConfig.ts b/frontend/editor/src/core/types/appConfig.ts index 72c7d7c5b6..2dafb3d07a 100644 --- a/frontend/editor/src/core/types/appConfig.ts +++ b/frontend/editor/src/core/types/appConfig.ts @@ -34,6 +34,7 @@ export interface AppConfig { serverCertificateEnabled?: boolean; hardwareSigningAvailable?: boolean; enableMobileScanner?: boolean; + enableMobileSignature?: boolean; mobileScannerConvertToPdf?: boolean; mobileScannerImageResolution?: string; mobileScannerPageFormat?: string; diff --git a/frontend/editor/src/core/utils/mobileScannerUrl.test.ts b/frontend/editor/src/core/utils/mobileScannerUrl.test.ts index 8890399b7b..024eeaf54a 100644 --- a/frontend/editor/src/core/utils/mobileScannerUrl.test.ts +++ b/frontend/editor/src/core/utils/mobileScannerUrl.test.ts @@ -9,10 +9,51 @@ */ import { describe, test, expect } from "vitest"; -import { buildMobileScannerUrl } from "@app/utils/mobileScannerUrl"; +import { + buildMobileRouteUrl, + buildMobileScannerUrl, +} from "@app/utils/mobileScannerUrl"; const sessionId = "abc-123"; +describe("buildMobileRouteUrl", () => { + test("routes other mobile pages (mobile-sign) with the base path", () => { + expect( + buildMobileRouteUrl({ + configuredUrl: "https://app.stirlingpdf.com", + sessionId, + origin: "https://app.stirlingpdf.com", + basePath: "/app", + routePath: "mobile-sign", + }), + ).toBe("https://app.stirlingpdf.com/app/mobile-sign?session=abc-123"); + }); + + test("configured URL with subpath keeps the route un-doubled", () => { + expect( + buildMobileRouteUrl({ + configuredUrl: "https://host.example/app/", + sessionId, + origin: "https://elsewhere.example", + basePath: "/app", + routePath: "mobile-sign", + }), + ).toBe("https://host.example/app/mobile-sign?session=abc-123"); + }); + + test("no configured URL falls back to origin + base path", () => { + expect( + buildMobileRouteUrl({ + configuredUrl: "", + sessionId, + origin: "http://192.168.1.20:8080", + basePath: "", + routePath: "mobile-sign", + }), + ).toBe("http://192.168.1.20:8080/mobile-sign?session=abc-123"); + }); +}); + describe("buildMobileScannerUrl", () => { test("origin-only frontendUrl keeps the app base path (SaaS web regression)", () => { expect( diff --git a/frontend/editor/src/core/utils/mobileScannerUrl.ts b/frontend/editor/src/core/utils/mobileScannerUrl.ts index c705069986..b44dcc358e 100644 --- a/frontend/editor/src/core/utils/mobileScannerUrl.ts +++ b/frontend/editor/src/core/utils/mobileScannerUrl.ts @@ -1,10 +1,10 @@ /** - * Build the URL a phone opens (via the QR code) to reach the SPA's - * `/mobile-scanner` route. + * Build the URL a phone opens (via a QR code) to reach one of the SPA's + * public mobile routes (`/mobile-scanner`, `/mobile-sign`). * - * That route is a public, top-level route. It lives under the app's base path, - * which is the router's `basename`. If the generated URL omits the base path, - * the phone loads a path the router can't match, falls through to the + * These routes are public, top-level routes. They live under the app's base + * path, which is the router's `basename`. If the generated URL omits the base + * path, the phone loads a path the router can't match, falls through to the * auth-gated catch-all route, and gets bounced to the login page. So the base * path must always be present. * @@ -18,15 +18,17 @@ * * With no usable configured URL, fall back to the current origin + base path. */ -export function buildMobileScannerUrl(params: { +export function buildMobileRouteUrl(params: { configuredUrl: string; sessionId: string; origin: string; basePath: string; + /** Route under the SPA base, without slashes: "mobile-scanner", "mobile-sign". */ + routePath: string; }): string { - const { configuredUrl, sessionId, origin, basePath } = params; + const { configuredUrl, sessionId, origin, basePath, routePath } = params; const query = `?session=${sessionId}`; - const route = `${basePath}/mobile-scanner`; + const route = `${basePath}/${routePath}`; const trimmed = configuredUrl.trim(); if (trimmed) { @@ -35,7 +37,7 @@ export function buildMobileScannerUrl(params: { if (parsed.protocol === "http:" || parsed.protocol === "https:") { const subpath = parsed.pathname.replace(/\/+$/, ""); return subpath - ? `${parsed.origin}${subpath}/mobile-scanner${query}` + ? `${parsed.origin}${subpath}/${routePath}${query}` : `${parsed.origin}${route}${query}`; } } catch { @@ -45,3 +47,13 @@ export function buildMobileScannerUrl(params: { return `${origin}${route}${query}`; } + +/** The `/mobile-scanner` QR URL. See {@link buildMobileRouteUrl}. */ +export function buildMobileScannerUrl(params: { + configuredUrl: string; + sessionId: string; + origin: string; + basePath: string; +}): string { + return buildMobileRouteUrl({ ...params, routePath: "mobile-scanner" }); +} diff --git a/frontend/editor/src/proprietary/App.tsx b/frontend/editor/src/proprietary/App.tsx index ed36815bfd..de828c9f2d 100644 --- a/frontend/editor/src/proprietary/App.tsx +++ b/frontend/editor/src/proprietary/App.tsx @@ -15,6 +15,7 @@ import Onboarding from "@app/components/onboarding/Onboarding"; import WatchedFoldersRegistration from "@app/components/watchedFolders/WatchedFoldersRegistration"; const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage")); +const MobileSignPage = lazy(() => import("@app/pages/MobileSignPage")); import { WATCHED_FOLDERS_ENABLED } from "@app/constants/featureFlags"; import { getAdminRouteExtensions } from "@app/routes/adminRouteExtensions"; import { LoginLandingRedirect } from "@app/components/LoginLandingRedirect"; @@ -59,6 +60,16 @@ export default function App() { } /> + {/* Mobile signature drawing - reached from the Sign tool QR code */} + + + + } + /> + {/* Participant signing — public, token-gated, no auth required */} import("@app/pages/MobileScannerPage")); +const MobileSignPage = lazy(() => import("@app/pages/MobileSignPage")); // Import global styles import "@app/styles/tailwind.css"; @@ -83,6 +84,16 @@ export default function App() { } /> + {/* Mobile signature drawing - reached from the Sign tool QR code */} + + + + } + /> + {/* Admin-only route-set (the portal): its own top-level shell, mounted before the catch-all. */} {getAdminRouteExtensions()} diff --git a/frontend/editor/src/saas/components/tools/sign/SignSettings.tsx b/frontend/editor/src/saas/components/tools/sign/SignSettings.tsx index b8691df30d..503087cbe4 100644 --- a/frontend/editor/src/saas/components/tools/sign/SignSettings.tsx +++ b/frontend/editor/src/saas/components/tools/sign/SignSettings.tsx @@ -34,6 +34,11 @@ import { AddSignatureResult, } from "@app/hooks/tools/sign/useSavedSignatures"; import { SavedSignaturesSection } from "@app/components/tools/sign/SavedSignaturesSection"; +import MobileSignatureModal, { + type MobileSignaturePayload, +} from "@app/components/tools/sign/MobileSignatureModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useIsMobile } from "@app/hooks/useIsMobile"; import { buildSignaturePreview } from "@app/utils/signaturePreview"; type SignatureDrafts = { @@ -116,6 +121,12 @@ const SignSettings = ({ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [isPlacementManuallyPaused, setPlacementManuallyPaused] = useState(false); + const [isMobileSignModalOpen, setIsMobileSignModalOpen] = useState(false); + const { config } = useAppConfig(); + const isMobileViewport = useIsMobile(); + // Drawing on a phone needs a second device, so the QR entry is desktop-only. + const canDrawOnPhone = + Boolean(config?.enableMobileSignature) && !isMobileViewport; // State for different signature types const [canvasSignatureData, setCanvasSignatureData] = useState< @@ -665,6 +676,71 @@ const SignSettings = ({ [onActivateSignaturePlacement], ); + // Route a signature made on a phone to the matching source, mirroring + // handleUseSavedSignature: ink is a canvas signature, a photo is an image + // signature, and typed text stays editable text rather than baked pixels. + const handleMobileSignatureReceived = useCallback( + (payload: MobileSignaturePayload) => { + // Receiving a signature is as clear an intent to place it as drawing + // one, so placement goes live even if it was paused beforehand. + setPlacementManuallyPaused(false); + lastAppliedPlacementKey.current = null; + if (payload.kind === "draw") { + if (parameters.signatureType !== "canvas") { + onParameterChange("signatureType", "canvas"); + } + handleCanvasSignatureChange(payload.dataUrl); + } else if (payload.kind === "photo") { + if (parameters.signatureType !== "image") { + onParameterChange("signatureType", "image"); + } + setImageSignatureData(payload.dataUrl); + } else { + if (parameters.signatureType !== "text") { + onParameterChange("signatureType", "text"); + } + onParameterChange("signerName", payload.text); + onParameterChange("fontFamily", payload.fontFamily); + onParameterChange("textColor", payload.color); + // Move the draft mirror in the same commit as the parameters. The + // record/restore effect pair otherwise sees them one commit apart and + // ping-pongs old draft against new params, wiping the received text + // and looping until React aborts the update depth. + const nextDraft = { + signerName: payload.text, + fontSize: parameters.fontSize ?? 16, + fontFamily: payload.fontFamily, + textColor: payload.color, + }; + lastSyncedTextDraft.current = nextDraft; + setSignatureDrafts((prev) => ({ ...prev, text: nextDraft })); + } + // Activate directly for every kind: the canvas-change handler only + // activates when the data actually changed, and the auto-activate + // effect only reacts to state transitions - neither fires for a + // repeat of the same signature. Fired twice because the first shot can + // land between the receive commit and the ready-state settling, where + // the placement effect immediately deactivates it; the second shot is + // after everything has settled, and re-activating is idempotent. + if (typeof window !== "undefined") { + window.setTimeout( + () => onActivateSignaturePlacement?.(), + PLACEMENT_ACTIVATION_DELAY, + ); + window.setTimeout(() => onActivateSignaturePlacement?.(), 500); + } else { + onActivateSignaturePlacement?.(); + } + }, + [ + parameters.signatureType, + parameters.fontSize, + onParameterChange, + handleCanvasSignatureChange, + onActivateSignaturePlacement, + ], + ); + const hasCanvasSignature = useMemo( () => Boolean(canvasSignatureData), [canvasSignatureData], @@ -1019,6 +1095,29 @@ const SignSettings = ({ if (signatureSource === "image") { return ( + {imageSignatureData && ( + + {translate("image.previewAlt", + + )} + {canDrawOnPhone && ( + <> + + setIsMobileSignModalOpen(false)} + onSignatureReceived={handleMobileSignatureReceived} + /> + + )} {sourceOptions.length > 1 && ( Date: Wed, 12 Aug 2026 09:06:05 +0000 Subject: [PATCH 082/154] =?UTF-8?q?a11y:=20empty=20the=20grandfathered=20S?= =?UTF-8?q?torybook=20baseline=20(1,058=20=E2=86=92=200)=20(#7309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Empties the light Storybook accessibility baseline — **1,058 grandfathered violations across 846 stories → 0** — so a new violation fails the gate instead of being silently absorbed. Also burns the dark baseline **812 → 56**; every entry left is one `main` already grandfathers. ## The defect, repeated everywhere A colour picked as a **fill**, chosen to carry a white label at 3:1, reused as **text**, where the floor is 4.5:1. It recurred through status accents, filled buttons, form labels, Mantine's light and outline variants, CSS declarations, inline styles and the generated accent ramp. Three systemic causes account for most of it: - **Mantine's semantic slots were never bound.** `-text`, `-outline`, `-light-color`, `-filled` and `-dimmed` all default to the hue's solid fill. Both resolvers now pin them to the accessible ink for the active scheme. - **The tint ladder was compressed.** `--color--50/100/200` pointed at saturated 400-level primitives, so every "tint" background rendered as a fill. - **Text was faded with `opacity`**, pushing already-muted copy below the floor. Each site now recedes via ink or surface, which is what conveyed the state anyway. ## Dark mode The colour resolver's dark half was empty, so dark fell through to Mantine's stock palette — and fixing the naming violations unmasked the contrast sitting underneath them. Both schemes now share one slot map, since most slots are written in tokens that already flip. The dark-only fixes: `--c-text-subtle` (3.0:1, used in 478 places), the error and section-label inks, and the accent ramp's text step — which light reaches by mixing toward black and dark has to reach by mixing toward white. ## Also - New `--c-*-solid` tokens for fills that must carry a white label, distinct from the `--c-` values used for surfaces, borders and icons. - A `data-user-content-preview` opt-out for nodes rendering a facsimile of the user's own document — WCAG governs the interface, not content authored through it. ## Verification - `task frontend:check:all` — green. - Changed-set gate, both schemes, after the final rebase: **366 stories, 0 regressions**. - Full sweep at the prior base — light **1,447 stories / 0 violations**, dark **1,448 / 0 regressions**. The dark re-record was confirmed key-by-key to be a strict subset of `main`'s, so nothing new is grandfathered. Roughly 28% of what this clears is naming and structure (`button-name`, `label`, `aria-*`) and has no visual signature; the rest is contrast. --- frontend/.gitignore | 1 + frontend/.storybook/a11y-baseline.dark.json | 2483 +--------------- frontend/.storybook/a11y-baseline.json | 2641 +---------------- frontend/.storybook/a11y-changed.mjs | 25 +- frontend/.storybook/preview.tsx | 59 +- .../public/locales/en-GB/translation.toml | 19 + .../public/locales/en-US/translation.toml | 16 + .../shared/config/configSections/Payg.css | 12 +- .../shared/config/configSections/PaygFree.css | 2 +- .../config/configSections/SpendCapControl.css | 4 +- .../config/configSections/usageMeters.tsx | 3 + .../src/core/components/StorageStatsCard.tsx | 1 + .../annotation/shared/ColorPicker.tsx | 8 + .../components/fileEditor/AddFileCard.tsx | 2 +- .../fileManager/CompactFileDetails.tsx | 2 +- .../components/fileManager/DragOverlay.tsx | 14 +- .../fileManager/EmptyFilesState.tsx | 2 +- .../components/fileManager/FileInfoCard.tsx | 16 +- .../core/components/filesPage/FileGrid.tsx | 384 +-- .../components/filesPage/FileOriginBadge.tsx | 4 +- .../core/components/filesPage/FilesPage.css | 35 +- .../components/filesPage/FolderThumbnail.tsx | 5 +- .../InitialOnboardingModal.module.css | 2 +- .../onboarding/OnboardingSlideShell.tsx | 214 +- .../slides/AnalyticsChoiceSlide.tsx | 2 +- .../onboarding/slides/FirstLoginSlide.tsx | 2 +- .../onboarding/slides/SecurityCheckSlide.tsx | 2 +- .../BulkSelectionPanel.module.css | 5 +- .../shared/DropdownListWithFooter.tsx | 7 + .../components/shared/EditableSecretField.tsx | 14 +- .../shared/EncryptedPdfUnlockModal.tsx | 2 +- .../core/components/shared/ErrorBoundary.tsx | 2 +- .../components/shared/FileDropdownMenu.tsx | 5 + .../src/core/components/shared/FileGrid.tsx | 1 + .../components/shared/FilePickerModal.tsx | 9 +- .../shared/FileSelectorPicker.module.css | 2 +- .../components/shared/FileSelectorPicker.tsx | 1 + .../core/components/shared/FileSidebar.css | 6 +- .../components/shared/FileSidebarFileItem.css | 8 +- .../src/core/components/shared/InfoBanner.tsx | 2 +- .../components/shared/MobileTransferModal.tsx | 5 + .../ObscuredOverlay.module.css | 2 +- .../shared/PageEditorFileDropdown.tsx | 5 + .../core/components/shared/ThemeProvider.tsx | 6 +- .../src/core/components/shared/ToolChain.tsx | 4 +- .../components/shared/ToolPanelHeader.css | 8 +- .../core/components/shared/UpdateModal.tsx | 1282 ++++---- .../core/components/shared/WorkbenchBar.css | 4 +- .../components/shared/ZipWarningModal.tsx | 2 +- .../config/configSections/GeneralSection.tsx | 36 +- .../shared/config/configSections/Overview.tsx | 2 +- .../config/configSections/ProviderCard.tsx | 2 +- .../shared/filePreview/DocumentThumbnail.tsx | 2 +- .../shared/signing/CreateSessionFlow.tsx | 5 +- .../sliderWithInput/SliderWithInput.tsx | 8 +- .../wetSignature/DrawSignatureCanvas.tsx | 13 + .../shared/wetSignature/TypeSignatureText.tsx | 19 +- .../wetSignature/UploadSignatureImage.tsx | 2 +- .../PageNumberPreview.module.css | 2 +- .../tools/addStamp/StampPreview.module.css | 2 +- .../tools/addStamp/StampPreview.tsx | 1 + .../addWatermark/WatermarkStyleSettings.tsx | 17 +- .../tools/addWatermark/WatermarkTextStyle.tsx | 9 +- .../tools/automate/AutomationImportModal.tsx | 15 +- .../tools/automate/AutomationRun.tsx | 2 +- .../BookletImpositionSettings.tsx | 4 +- .../modals/CertificateConfigModal.tsx | 4 +- .../certSign/panels/SignControlsPanel.tsx | 4 +- .../certSign/panels/SignRequestPanel.tsx | 2 +- .../certSign/steps/AddSignaturesStep.tsx | 2 +- .../compare/ComparePixelWorkbenchView.tsx | 2 +- .../tools/compress/CompressSettings.tsx | 7 +- .../convert/ConvertFromEmailSettings.tsx | 5 +- .../tools/convert/ConvertFromWebSettings.tsx | 10 +- .../tools/convert/ConvertToPdfaSettings.tsx | 5 +- .../tools/ocr/LanguagePicker.module.css | 2 +- .../components/tools/ocr/LanguagePicker.tsx | 2 +- .../tools/pdfTextEditor/FontStatusPanel.tsx | 4 +- .../tools/pdfTextEditor/PdfTextEditorView.tsx | 2 +- .../removeBlanks/RemoveBlanksSettings.tsx | 13 +- .../replaceColor/ReplaceColorSettings.tsx | 17 +- .../tools/shared/NumberInputWithUnit.tsx | 5 +- .../core/components/tools/shared/ToolStep.tsx | 3 +- .../components/tools/showJS/ShowJSView.css | 2 +- .../tools/toolPicker/FavoriteStar.tsx | 4 + .../reportView/SignatureSection.tsx | 2 +- .../reportView/SignatureStatusBadge.tsx | 43 +- .../components/viewer/AttachmentSidebar.css | 2 +- .../components/viewer/AttachmentSidebar.tsx | 2 +- .../components/viewer/BookmarkSidebar.tsx | 6 +- .../components/viewer/CommentsSidebar.tsx | 7 +- .../core/components/viewer/EmbedPdfViewer.tsx | 2 +- .../core/components/viewer/LayerSidebar.tsx | 2 +- .../core/components/viewer/LocalEmbedPDF.tsx | 6 +- .../core/components/viewer/SidebarBase.css | 2 +- .../components/viewer/nonpdf/JsonViewer.tsx | 2 +- .../components/viewer/nonpdf/TextViewer.tsx | 6 +- .../src/core/pages/MobileScannerPage.tsx | 2 +- frontend/editor/src/core/styles/index.css | 2 +- frontend/editor/src/core/styles/theme.css | 48 +- frontend/editor/src/core/theme/colors.css | 51 +- .../editor/src/core/theme/mantineTheme.ts | 129 + frontend/editor/src/core/theme/primitives.css | 30 +- frontend/editor/src/core/tokens/base.css | 2 +- frontend/editor/src/core/tokens/tokens.css | 43 +- .../core/tools/formFill/FormFill.module.css | 2 +- frontend/editor/src/core/ui/Banner.css | 6 +- frontend/editor/src/core/ui/Button.tsx | 8 + frontend/editor/src/core/ui/ChatFABButton.tsx | 7 +- .../src/core/ui/ChatFABWindow.stories.tsx | 4 +- frontend/editor/src/core/ui/ChatFABWindow.tsx | 4 + frontend/editor/src/core/ui/Chip.tsx | 20 +- frontend/editor/src/core/ui/CodeBlock.tsx | 10 +- frontend/editor/src/core/ui/Drawer.tsx | 6 +- frontend/editor/src/core/ui/Dropdown.css | 2 +- frontend/editor/src/core/ui/EmptyState.css | 2 +- frontend/editor/src/core/ui/FormField.css | 6 +- frontend/editor/src/core/ui/Forms.stories.tsx | 3 + frontend/editor/src/core/ui/ListRow.css | 10 +- frontend/editor/src/core/ui/MantineForms.css | 2 +- .../src/core/ui/MantineForms.stories.tsx | 3 + frontend/editor/src/core/ui/MethodBadge.css | 2 +- frontend/editor/src/core/ui/MetricCard.css | 4 +- frontend/editor/src/core/ui/NavItem.css | 12 +- frontend/editor/src/core/ui/PanelHeader.css | 8 +- .../src/core/ui/ProgressBar.stories.tsx | 9 +- frontend/editor/src/core/ui/ProgressBar.tsx | 5 +- frontend/editor/src/core/ui/Select.tsx | 4 + frontend/editor/src/core/ui/SettingsRow.tsx | 20 +- frontend/editor/src/core/ui/StatTile.css | 2 +- frontend/editor/src/core/ui/StatusBadge.css | 2 +- frontend/editor/src/core/ui/StepIndicator.tsx | 6 + frontend/editor/src/core/ui/Tabs.css | 4 +- frontend/editor/src/core/ui/ToggleSwitch.tsx | 4 + frontend/editor/src/core/ui/accents.css | 30 +- .../src/portal/components/AssistantPanel.tsx | 6 +- .../components/ChatFABWidget.stories.tsx | 6 +- .../portal/components/DownloadEditorModal.css | 4 +- .../components/NotificationsDropdown.css | 4 +- .../src/portal/components/ProcessorFlow.css | 4 +- .../src/portal/components/ProcessorFlow.tsx | 6 +- .../account-link/LinkedInstancesTable.tsx | 6 +- .../billing/PrepaidCapacityCard.tsx | 1 + .../components/billing/SpendLimitCard.tsx | 1 + .../portal/components/billing/WalletMeter.tsx | 1 + .../src/portal/components/billing/billing.css | 32 +- .../components/docs/GettingStartedSection.tsx | 6 +- .../components/docs/PlaybooksSection.tsx | 2 +- .../portal/components/docs/SdksSection.tsx | 2 +- .../portal/components/docs/SkillsSection.tsx | 2 +- .../infrastructure/DeploymentsTab.tsx | 9 +- .../components/infrastructure/ModelsTab.tsx | 7 +- .../procurement/ProcurementAgreement.tsx | 5 + .../components/users/ResetPasswordModal.tsx | 8 +- .../editor/src/portal/data/Ops.stories.tsx | 4 +- frontend/editor/src/portal/theme/base.css | 2 +- .../editor/src/portal/theme/mantineTheme.ts | 46 + .../editor/src/portal/views/DeveloperDocs.css | 18 +- .../editor/src/portal/views/DeveloperDocs.tsx | 11 +- .../editor/src/portal/views/EditorAdmin.css | 12 +- frontend/editor/src/portal/views/Home.css | 2 +- .../src/portal/views/Infrastructure.css | 6 +- .../editor/src/portal/views/Integrations.css | 4 +- .../editor/src/portal/views/Pipelines.css | 4 +- frontend/editor/src/portal/views/Policies.css | 17 +- .../editor/src/portal/views/Procurement.css | 14 +- frontend/editor/src/portal/views/Sources.css | 6 +- frontend/editor/src/portal/views/Users.css | 8 +- .../src/proprietary/auth/ui/OAuthButtons.tsx | 10 +- .../src/proprietary/billing/MeterBar.tsx | 4 + .../proprietary/components/chat/ChatPanel.css | 12 +- .../shared/ChangeUserPasswordModal.tsx | 435 +-- .../config/configSections/AccountSection.tsx | 2 +- .../AdminConnectionsSection.tsx | 6 +- .../configSections/AdminMailSection.tsx | 4 +- .../AdminStorageSharingSection.tsx | 8 +- .../configSections/LoginAgreementEditor.tsx | 2 +- .../configSections/TeamDetailsSection.tsx | 14 +- .../config/configSections/TeamsSection.tsx | 8 +- .../configSections/apiKeys/RefreshModal.tsx | 2 +- .../audit/AuditSystemStatus.tsx | 2 - .../plan/AvailablePlansSection.tsx | 1 + .../plan/FeatureComparisonTable.tsx | 2 +- .../config/configSections/plan/PlanCard.tsx | 2 +- .../dividerWithText/DividerWithText.css | 9 +- .../DeleteFolderConfirmModal.tsx | 2 +- .../watchedFolders/WatchedFolderHomePage.tsx | 4 +- .../WatchedFolderManagementModal.tsx | 2 +- .../WatchedFolderWorkbenchView.tsx | 10 +- .../watchedFolders/WatchedFolders.css | 6 +- .../components/workflow/ParticipantView.tsx | 4 +- .../editor/src/proprietary/routes/Login.tsx | 2 +- .../routes/login/LoggedInState.tsx | 2 +- .../proprietary/routes/signup/SignupForm.tsx | 4 + .../onboarding/OnboardingChecklist.module.css | 4 +- frontend/editor/src/saas/routes/Login.tsx | 4 +- frontend/editor/src/saas/routes/Signup.tsx | 2 +- 197 files changed, 2368 insertions(+), 6641 deletions(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index c0c467073d..9ab1c65091 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -49,3 +49,4 @@ test-results /scripts/dev-update-test/screenshots/ /editor/src-tauri/tauri.conf.dev-update.json .a11y-scan/ +.a11y-acc/ diff --git a/frontend/.storybook/a11y-baseline.dark.json b/frontend/.storybook/a11y-baseline.dark.json index fb461fe6d2..0967ef424b 100644 --- a/frontend/.storybook/a11y-baseline.dark.json +++ b/frontend/.storybook/a11y-baseline.dark.json @@ -1,2482 +1 @@ -{ - "editor/src/core/assets/Brand.stories.tsx :: Logos": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/StorageStatsCard.stories.tsx :: Default": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/StorageStatsCard.stories.tsx :: Nearing Quota": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/StorageStatsCard.stories.tsx :: No Quota": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ColorPicker.stories.tsx :: Default": [ - "aria-input-field-name", - "button-name", - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ColorPicker.stories.tsx :: With Opacity": [ - "aria-input-field-name", - "button-name", - "color-contrast" - ], - "editor/src/core/components/annotation/shared/DrawingControls.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ImageUploader.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ImageUploader.stories.tsx :: With Background Removal": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ImageUploader.stories.tsx :: With Label And Hint": [ - "color-contrast" - ], - "editor/src/core/components/annotation/tools/ImageTool.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/fileManager/FileDetails.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/fileManager/FileDetails.stories.tsx :: Empty": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx :: Cloud Only": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx :: Local And Cloud Choice": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx :: With Files": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: In Folder": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: Local Only With Save To Server": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: Multi Select": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileGrid.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileGrid.stories.tsx :: List Mode": [ - "aria-required-children" - ], - "editor/src/core/components/filesPage/FileOriginBadge.stories.tsx :: Cloud": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FolderAppearancePicker.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/filesPage/FolderAppearancePicker.stories.tsx :: Disabled": [ - "scrollable-region-focusable" - ], - "editor/src/core/components/filesPage/FolderAppearancePicker.stories.tsx :: No Appearance Set": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/filesPage/FolderNameDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/FolderNameDialog.stories.tsx :: Rename": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/FolderThumbnail.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: Empty": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: With Create Folder": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: With Disabled Descendant": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionHistoryModal.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/filesPage/VersionHistoryModal.stories.tsx :: No File Selected": [ - "button-name" - ], - "editor/src/core/components/filesPage/VersionTimeline.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionTimeline.stories.tsx :: Long Chain Collapsed": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionTimeline.stories.tsx :: No Header": [ - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Admin Overview Login Disabled": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Admin Overview Login Enabled": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Analytics Choice": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Analytics Choice Error": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Desktop Install": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: First Login": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: First Login Default Credentials": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Mfa Setup": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Security Check": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Server License": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Server License Over Limit": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Stepped Flow Example": [ - "aria-dialog-name", - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Tour Overview": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Welcome": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingSlideShell.stories.tsx :: Default": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingSlideShell.stories.tsx :: Not Dismissible": [ - "aria-dialog-name", - "aria-progressbar-name" - ], - "editor/src/core/components/onboarding/OnboardingSlideShell.stories.tsx :: Stepped With Back": [ - "aria-dialog-name", - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/slides/WelcomeSlide.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.stories.tsx :: With Expression": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/OperatorsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/OperatorsSection.stories.tsx :: Empty Input": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/SelectedPagesDisplay.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/SelectedPagesDisplay.stories.tsx :: Syntax Error": [ - "color-contrast" - ], - "editor/src/core/components/shared/BulkShareModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/BulkShareModal.stories.tsx :: Links Enabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/BulkUploadToServerModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/BulkUploadToServerModal.stories.tsx :: Single File": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/DropdownListWithFooter.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/DropdownListWithFooter.stories.tsx :: Empty": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/DropdownListWithFooter.stories.tsx :: Multi Select With Footer": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/EditableSecretField.stories.tsx :: Masked": [ - "label" - ], - "editor/src/core/components/shared/EditableSecretField.stories.tsx :: Masked Disabled": [ - "label" - ], - "editor/src/core/components/shared/EditableSecretField.stories.tsx :: With Error": [ - "label-title-only" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Incorrect Password": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Multiple Files Remaining": [ - "button-name" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Processing": [ - "button-name" - ], - "editor/src/core/components/shared/ErrorBoundary.stories.tsx :: Caught Error": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileCard.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileCard.stories.tsx :: Unsupported": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileDropdownMenu.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/FileDropdownMenu.stories.tsx :: No Remove": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/FileDropdownMenu.stories.tsx :: Switching": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/FileGrid.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileGrid.stories.tsx :: Search And Sort": [ - "color-contrast", - "label" - ], - "editor/src/core/components/shared/FilePickerModal.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/core/components/shared/FilePickerModal.stories.tsx :: Empty": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/FileSelectorPicker.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileUploadButton.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileUploadButton.stories.tsx :: With File Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/Footer.stories.tsx :: All Links And Cookie Banner": [ - "color-contrast" - ], - "editor/src/core/components/shared/InfoBanner.stories.tsx :: Warning": [ - "color-contrast" - ], - "editor/src/core/components/shared/MobileUploadModal.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "svg-img-alt" - ], - "editor/src/core/components/shared/MultiSelectControls.stories.tsx :: All Actions": [ - "color-contrast" - ], - "editor/src/core/components/shared/MultiSelectControls.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/NavigationWarningModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/NavigationWarningModal.stories.tsx :: With Apply And Continue": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/NavigationWarningModal.stories.tsx :: With Export And Continue": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ObscuredOverlay.stories.tsx :: Unobscured": [ - "color-contrast" - ], - "editor/src/core/components/shared/PageSelectionSyntaxHint.stories.tsx :: Compact Syntax Error": [ - "color-contrast" - ], - "editor/src/core/components/shared/PageSelectionSyntaxHint.stories.tsx :: Syntax Error": [ - "color-contrast" - ], - "editor/src/core/components/shared/PolicyBadges.stories.tsx :: Default": [ - "scrollable-region-focusable" - ], - "editor/src/core/components/shared/PolicyBadges.stories.tsx :: Empty": [ - "scrollable-region-focusable" - ], - "editor/src/core/components/shared/PolicyBadges.stories.tsx :: Enforcing": [ - "scrollable-region-focusable" - ], - "editor/src/core/components/shared/ShareFileModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ShareFileModal.stories.tsx :: Links Enabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ShareManagementModal.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/shared/ShareManagementModal.stories.tsx :: Links Enabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/UpdateModal.stories.tsx :: Default": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/shared/UpdateModal.stories.tsx :: Desktop Install Blocked": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/shared/UpdateModal.stories.tsx :: Desktop Install Ready To Restart": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/shared/UploadToServerModal.stories.tsx :: Already Uploaded": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/UploadToServerModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/UserSelector.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/ZipWarningModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ZipWarningModal.stories.tsx :: Single File": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/config/RestartConfirmationModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/config/SettingsStickyFooter.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/SettingsStickyFooter.stories.tsx :: Saving": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/GeneralSection.stories.tsx :: Admin Banner": [ - "color-contrast", - "label" - ], - "editor/src/core/components/shared/config/configSections/GeneralSection.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/core/components/shared/config/configSections/GeneralSection.stories.tsx :: With Backend Version": [ - "color-contrast", - "label" - ], - "editor/src/core/components/shared/config/configSections/HelpSection.stories.tsx :: Admin": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/HelpSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/HotkeysSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/LegalSection.stories.tsx :: With Analytics Enabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/Overview.stories.tsx :: Loaded": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/Overview.stories.tsx :: With Warning": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ProviderCard.stories.tsx :: Configured": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ProviderCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ProviderCard.stories.tsx :: Read Only": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/filePreview/HoverOverlay.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/CreateSessionFlow.stories.tsx :: Creating": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/CreateSessionFlow.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/CreateSessionFlow.stories.tsx :: No File Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/SharedSigningLauncher.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/SharedSigningLauncher.stories.tsx :: Pending Requests": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ConfigureSignatureDefaultsStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ConfigureSignatureDefaultsStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ConfigureSignatureDefaultsStep.stories.tsx :: Invisible Signature": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ReviewSessionStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ReviewSessionStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ReviewSessionStep.stories.tsx :: Invisible Signature": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectDocumentStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectParticipantsStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectParticipantsStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectParticipantsStep.stories.tsx :: With Selection": [ - "color-contrast" - ], - "editor/src/core/components/shared/sliderWithInput/SliderWithInput.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/shared/sliderWithInput/SliderWithInput.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/shared/wetSignature/DrawSignatureCanvas.stories.tsx :: Default": [ - "aria-input-field-name" - ], - "editor/src/core/components/shared/wetSignature/DrawSignatureCanvas.stories.tsx :: Disabled": [ - "aria-input-field-name" - ], - "editor/src/core/components/shared/wetSignature/TypeSignatureText.stories.tsx :: Default": [ - "aria-input-field-name" - ], - "editor/src/core/components/shared/wetSignature/TypeSignatureText.stories.tsx :: Disabled": [ - "aria-input-field-name" - ], - "editor/src/core/components/shared/wetSignature/TypeSignatureText.stories.tsx :: Empty": [ - "aria-input-field-name" - ], - "editor/src/core/components/shared/wetSignature/UploadSignatureImage.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/toast/ToastRenderer.stories.tsx :: With Action Button": [ - "color-contrast" - ], - "editor/src/core/components/tools/ToolPanelModePrompt.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/addPageNumbers/PageNumberPreview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addPageNumbers/PageNumberPreview.stories.tsx :: With Quick Grid": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampPreview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampPreview.stories.tsx :: With Quick Grid": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampPreview.stories.tsx :: With Text": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampSetupSettings.stories.tsx :: Image Stamp": [ - "color-contrast" - ], - "editor/src/core/components/tools/addWatermark/AddWatermarkSingleStepSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/AddWatermarkSingleStepSettings.stories.tsx :: Text Only": [ - "button-name", - "label" - ], - "editor/src/core/components/tools/addWatermark/AddWatermarkSingleStepSettings.stories.tsx :: Text Watermark": [ - "button-name", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkFormatting.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkFormatting.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkFormatting.stories.tsx :: Without Flatten Option": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkImageFile.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addWatermark/WatermarkImageFile.stories.tsx :: With Selected Image": [ - "color-contrast" - ], - "editor/src/core/components/tools/addWatermark/WatermarkStyleSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkStyleSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkTextStyle.stories.tsx :: Default": [ - "button-name", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkTextStyle.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.stories.tsx :: Adjusted": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.stories.tsx :: Adjusted": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastSingleStepSettings.stories.tsx :: Adjusted": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastSingleStepSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastSingleStepSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/automate/AutomationCreation.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/automate/AutomationCreation.stories.tsx :: Edit Existing": [ - "color-contrast" - ], - "editor/src/core/components/tools/automate/AutomationCreation.stories.tsx :: Embedded Hide Metadata": [ - "color-contrast" - ], - "editor/src/core/components/tools/automate/AutomationImportModal.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/core/components/tools/automate/ToolConfigurationModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/automate/ToolConfigurationModal.stories.tsx :: With Settings": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/automate/ToolList.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/automate/ToolSelector.stories.tsx :: Custom Placeholder": [ - "color-contrast" - ], - "editor/src/core/components/tools/automate/ToolSelector.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.stories.tsx :: Manual Duplex": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertSignAutomationSettings.stories.tsx :: Auto Sign Mode": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertSignAutomationSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertSignAutomationSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFilesSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFilesSettings.stories.tsx :: Jks": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFilesSettings.stories.tsx :: Pkcs 12": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFormatSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFormatSettings.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateSelector.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateSelector.stories.tsx :: Pem Format": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateTypeSettings.stories.tsx :: All Sources Available": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateTypeSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateTypeSettings.stories.tsx :: Server Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureAppearanceSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureAppearanceSettings.stories.tsx :: Visible Signature": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsDisplay.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsDisplay.stories.tsx :: Minimal Details": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsInput.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsInput.stories.tsx :: Visible Signature": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/WetSignatureInput.stories.tsx :: Upload Certificate": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/AddParticipantsFlow.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/CertificateConfigModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/CertificateConfigModal.stories.tsx :: Disabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/CertificateConfigModal.stories.tsx :: Multiple Signatures": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/SelectSignatureModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/ParticipantListPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/ParticipantListPanel.stories.tsx :: Finalized": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionActionsPanel.stories.tsx :: All Signed": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionActionsPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionDetailPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionDetailPanel.stories.tsx :: Finalized": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SignControlsPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SignControlsPanel.stories.tsx :: No Signature Chosen": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/AddSignaturesStep.stories.tsx :: Default": [ - "aria-input-field-name" - ], - "editor/src/core/components/tools/certSign/steps/AddSignaturesStep.stories.tsx :: Disabled": [ - "aria-input-field-name" - ], - "editor/src/core/components/tools/certSign/steps/AddSignaturesStep.stories.tsx :: Placement Mode": [ - "aria-input-field-name" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: Upload Ready": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: User Certificate": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/ReviewSignatureStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/ReviewSignatureStep.stories.tsx :: Multiple Signatures": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/ReviewSignatureStep.stories.tsx :: Uploaded Certificate Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: Default": [ - "aria-input-field-name" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: Disabled": [ - "aria-input-field-name" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: Type Mode": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: With Signature": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignaturePlacementStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignaturePlacementStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignaturePlacementStep.stories.tsx :: Placed": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/AdvancedOptionsStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/AdvancedOptionsStep.stories.tsx :: With Custom Metadata": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/CustomMetadataStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/CustomMetadataStep.stories.tsx :: With Entries": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/DocumentDatesStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/DocumentDatesStep.stories.tsx :: Filled": [ - "button-name" - ], - "editor/src/core/components/tools/compare/ComparePixelWorkbenchView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/compare/ComparePixelWorkbenchView.stories.tsx :: No Differences": [ - "color-contrast" - ], - "editor/src/core/components/tools/compare/ComparePixelWorkbenchView.stories.tsx :: With Warnings": [ - "color-contrast" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: File Size Method": [ - "label" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: Line Art Enabled": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromEmailSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromEmailSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromWebSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromWebSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertToPdfaSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertToPdfaSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertToPdfaSettings.stories.tsx :: Strict Mode": [ - "label" - ], - "editor/src/core/components/tools/editTableOfContents/BookmarkEditor.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/BookmarkEditor.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsSettings.stories.tsx :: Loading With Error": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsSettings.stories.tsx :: No File Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: With Error": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: With Results": [ - "color-contrast" - ], - "editor/src/core/components/tools/fullscreen/DetailedToolItem.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/fullscreen/DetailedToolItem.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoResults.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoResults.stories.tsx :: Partial Error": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/ComplianceSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/merge/MergeFileSorter.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/ocr/LanguagePicker.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/ocr/OCRSettings.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/redact/RedactAdvancedSettings.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/tools/redact/RedactSingleStepSettings.stories.tsx :: Automatic With Words": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/redact/RedactSingleStepSettings.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/tools/redact/RedactSingleStepSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/redact/WordsToRedactInput.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/redact/WordsToRedactInput.stories.tsx :: With Words": [ - "color-contrast" - ], - "editor/src/core/components/tools/removeBlanks/RemoveBlanksSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/removeBlanks/RemoveBlanksSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/removeBlanks/RemoveBlanksSettings.stories.tsx :: Include Blank Pages": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/replaceColor/ReplaceColorSettings.stories.tsx :: Custom Color": [ - "button-name", - "label" - ], - "editor/src/core/components/tools/replaceColor/ReplaceColorSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/replaceColor/ReplaceColorSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/shared/ErrorNotification.stories.tsx :: Custom Title": [ - "button-name" - ], - "editor/src/core/components/tools/shared/ErrorNotification.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/tools/shared/NumberInputWithUnit.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/shared/NumberInputWithUnit.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/shared/NumberInputWithUnit.stories.tsx :: With Min Max": [ - "label" - ], - "editor/src/core/components/tools/shared/ResultsPreview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ResultsPreview.stories.tsx :: Single File": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ToolStep.stories.tsx :: Collapsed": [ - "color-contrast" - ], - "editor/src/core/components/tools/showJS/ShowJSView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/showJS/ShowJSView.stories.tsx :: With Download": [ - "color-contrast" - ], - "editor/src/core/components/tools/toolPicker/FavoriteStar.stories.tsx :: Default": [ - "aria-prohibited-attr" - ], - "editor/src/core/components/tools/toolPicker/FavoriteStar.stories.tsx :: Favorited": [ - "aria-prohibited-attr" - ], - "editor/src/core/components/tools/toolPicker/FavoriteStar.stories.tsx :: Sizes": [ - "aria-prohibited-attr" - ], - "editor/src/core/components/tools/toolPicker/ToolSearch.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/toolPicker/ToolSearch.stories.tsx :: Dropdown Mode": [ - "color-contrast" - ], - "editor/src/core/components/tools/toolPicker/ToolSearch.stories.tsx :: Unstyled": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: Default": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: Error": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: Multiple Signatures": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: No Signatures": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureSettings.stories.tsx :: With Cert File": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureSection.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureSection.stories.tsx :: Invalid With Error": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureSection.stories.tsx :: Self Signed Minimal Data": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureStatusBadge.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureStatusBadge.stories.tsx :: Invalid": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureStatusBadge.stories.tsx :: Untrusted Signer": [ - "aria-allowed-attr" - ], - "editor/src/core/components/viewer/nonpdf/TextViewer.stories.tsx :: Markdown": [ - "color-contrast" - ], - "editor/src/core/tokens/Tokens.stories.tsx :: Colours": ["color-contrast"], - "editor/src/core/tokens/Tokens.stories.tsx :: Typography": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: Success": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: Tone Matrix": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: With Action": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Accents": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Disabled Dark": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Justify": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Loading": ["button-name"], - "editor/src/core/ui/Button.stories.tsx :: Padding": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Shape": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Sizes": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Variants": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: With Icons": ["color-contrast"], - "editor/src/core/ui/Card.stories.tsx :: Accent Matrix": ["color-contrast"], - "editor/src/core/ui/Card.stories.tsx :: In Context Metrics Inside Card": [ - "color-contrast" - ], - "editor/src/core/ui/Card.stories.tsx :: In Context Product Grid": [ - "color-contrast" - ], - "editor/src/core/ui/Card.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Default": ["button-name"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Loading": ["button-name"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Tick": ["button-name"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Tick While Loading": [ - "button-name" - ], - "editor/src/core/ui/ChatFABWindow.stories.tsx :: Closed": [ - "scrollable-region-focusable" - ], - "editor/src/core/ui/ChatFABWindow.stories.tsx :: Open": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/ChatFABWindow.stories.tsx :: Toggle": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Chip.stories.tsx :: Accents": ["color-contrast"], - "editor/src/core/ui/Chip.stories.tsx :: Dashed Add": ["nested-interactive"], - "editor/src/core/ui/Chip.stories.tsx :: In Context Op Chain": [ - "color-contrast" - ], - "editor/src/core/ui/Chip.stories.tsx :: Playground": [ - "color-contrast", - "nested-interactive" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: In Context Quickstart": [ - "color-contrast" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: In Context Two Up Comparison": [ - "color-contrast" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: Long Scrolling": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/Collapsible.stories.tsx :: Accordion": [ - "scrollable-region-focusable" - ], - "editor/src/core/ui/Collapsible.stories.tsx :: Default": [ - "scrollable-region-focusable" - ], - "editor/src/core/ui/DataRow.stories.tsx :: Single": ["color-contrast"], - "editor/src/core/ui/DataRow.stories.tsx :: Summary": ["color-contrast"], - "editor/src/core/ui/Drawer.stories.tsx :: Playground": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/core/ui/Drawer.stories.tsx :: With Footer": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/core/ui/Dropdown.stories.tsx :: Align Start": ["color-contrast"], - "editor/src/core/ui/Dropdown.stories.tsx :: Basic": ["color-contrast"], - "editor/src/core/ui/Dropdown.stories.tsx :: With Divider": ["color-contrast"], - "editor/src/core/ui/Dropdown.stories.tsx :: With Trailing Hints": [ - "color-contrast" - ], - "editor/src/core/ui/EmptyState.stories.tsx :: In Card": ["color-contrast"], - "editor/src/core/ui/EmptyState.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/EmptyState.stories.tsx :: With CT As": ["color-contrast"], - "editor/src/core/ui/FilePicker.stories.tsx :: Accept Pdf": ["color-contrast"], - "editor/src/core/ui/FilePicker.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/FilePicker.stories.tsx :: Multiple": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Checkbox Grid Of Categories": [ - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Checkbox Single": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Full Form": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Input Default": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Input Error": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Input With Icon": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Radio Group": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Radio Horizontal": [ - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Slider Confidence": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Slider Retention": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/Inline.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/Inline.stories.tsx :: Wrap": ["color-contrast"], - "editor/src/core/ui/ListRow.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/ListRow.stories.tsx :: In Card": ["color-contrast"], - "editor/src/core/ui/ListRow.stories.tsx :: Interactive": ["color-contrast"], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Error": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Preselected": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Sm Size": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select Default": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select Error": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select Sm Size": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select With Values": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Decimal": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Default": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Error": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Sm Size": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input With Unit": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Default": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Error": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Searchable": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Sm Size": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider Default": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider Disabled": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider No Label": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider With Marks": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Watermark Form": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MethodBadge.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/MethodBadge.stories.tsx :: In Row": ["color-contrast"], - "editor/src/core/ui/MethodBadge.stories.tsx :: Matrix": ["color-contrast"], - "editor/src/core/ui/MetricCard.stories.tsx :: Free Tier Strip": [ - "color-contrast" - ], - "editor/src/core/ui/MetricCard.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/MetricCard.stories.tsx :: Pro Tier Strip": [ - "color-contrast" - ], - "editor/src/core/ui/MetricStrip.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/NavItem.stories.tsx :: In Context Sidebar Group": [ - "color-contrast" - ], - "editor/src/core/ui/NavItem.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/NavItem.stories.tsx :: With Trailing Badge": [ - "color-contrast" - ], - "editor/src/core/ui/PanelHeader.stories.tsx :: With Actions": [ - "color-contrast" - ], - "editor/src/core/ui/ProgressBar.stories.tsx :: In Context Usage Meter": [ - "color-contrast" - ], - "editor/src/core/ui/ProgressBar.stories.tsx :: Playground": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/ProgressBar.stories.tsx :: Threshold Ladder": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/SectionDivider.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/ui/SectionHeader.stories.tsx :: Collapsible": [ - "color-contrast" - ], - "editor/src/core/ui/SectionHeader.stories.tsx :: Static": ["color-contrast"], - "editor/src/core/ui/SegmentedControl.stories.tsx :: Variants": [ - "color-contrast" - ], - "editor/src/core/ui/SegmentedControl.stories.tsx :: With Icons": [ - "color-contrast" - ], - "editor/src/core/ui/SettingsRow.stories.tsx :: List": ["label"], - "editor/src/core/ui/SettingsRow.stories.tsx :: Select Control": ["label"], - "editor/src/core/ui/SettingsRow.stories.tsx :: Toggle": ["label"], - "editor/src/core/ui/SettingsRow.stories.tsx :: With Description": [ - "color-contrast", - "label" - ], - "editor/src/core/ui/SettingsShell.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/Stack.stories.tsx :: Gap Sizes": ["color-contrast"], - "editor/src/core/ui/Stack.stories.tsx :: In Card": ["color-contrast"], - "editor/src/core/ui/StatTile.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/StatTile.stories.tsx :: Tone Row": ["color-contrast"], - "editor/src/core/ui/StatusBadge.stories.tsx :: All Tones": ["color-contrast"], - "editor/src/core/ui/StatusBadge.stories.tsx :: Sizes": ["color-contrast"], - "editor/src/core/ui/StepIndicator.stories.tsx :: Small": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/StepIndicator.stories.tsx :: Step 1": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/StepIndicator.stories.tsx :: Step 2": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/StepIndicator.stories.tsx :: Step 3": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/Table.stories.tsx :: Basic": ["color-contrast"], - "editor/src/core/ui/Table.stories.tsx :: Empty": ["color-contrast"], - "editor/src/core/ui/Table.stories.tsx :: Interactive": ["color-contrast"], - "editor/src/core/ui/Tabs.stories.tsx :: In Context Document Verticals": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Tabs.stories.tsx :: Playground": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Tabs.stories.tsx :: With Disabled Tab": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Toast.stories.tsx :: Triggers": ["color-contrast"], - "editor/src/core/ui/ToggleSwitch.stories.tsx :: In Context Settings Rows": [ - "color-contrast" - ], - "editor/src/core/ui/ToggleSwitch.stories.tsx :: With Description": [ - "color-contrast" - ], - "editor/src/portal/components/AppShell.stories.tsx :: Mobile": [ - "color-contrast" - ], - "editor/src/portal/components/AppShell.stories.tsx :: With Home View": [ - "color-contrast" - ], - "editor/src/portal/components/AssistantPanel.stories.tsx :: Reply Fails": [ - "aria-allowed-role" - ], - "editor/src/portal/components/AssistantPanel.stories.tsx :: Slow Reply": [ - "aria-allowed-role" - ], - "editor/src/portal/components/AssistantPanel.stories.tsx :: Suggestions Only": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Closed": [ - "aria-hidden-focus" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Full Flow": [ - "aria-hidden-focus", - "color-contrast" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Loading While Closed": [ - "aria-hidden-focus" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Unread Result": [ - "aria-hidden-focus" - ], - "editor/src/portal/components/DownloadEditorModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/EditorStatusCard.stories.tsx :: Deployment Unavailable": [ - "color-contrast" - ], - "editor/src/portal/components/EditorStatusCard.stories.tsx :: With Setup Checklist": [ - "color-contrast" - ], - "editor/src/portal/components/ErrorBoundary.stories.tsx :: Caught Error": [ - "color-contrast" - ], - "editor/src/portal/components/HomeHero.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/HomeHero.stories.tsx :: Free Tier": [ - "color-contrast" - ], - "editor/src/portal/components/LinkAccountFooterItem.stories.tsx :: Unlinked": [ - "color-contrast" - ], - "editor/src/portal/components/PortalChrome.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/ProcessorFlow.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/ProcessorFlow.stories.tsx :: Idle Empty": [ - "color-contrast" - ], - "editor/src/portal/components/ProcessorFlow.stories.tsx :: Playground": [ - "color-contrast" - ], - "editor/src/portal/components/SearchModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/SearchModal.stories.tsx :: Empty Catalogue": [ - "color-contrast" - ], - "editor/src/portal/components/SetupChecklist.stories.tsx :: Almost Done": [ - "color-contrast" - ], - "editor/src/portal/components/SetupChecklist.stories.tsx :: In Progress": [ - "color-contrast" - ], - "editor/src/portal/components/SetupChecklist.stories.tsx :: Not Started": [ - "color-contrast" - ], - "editor/src/portal/components/Sidebar.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/Sidebar.stories.tsx :: Enterprise Tier": [ - "color-contrast" - ], - "editor/src/portal/components/Sidebar.stories.tsx :: Free Tier": [ - "color-contrast" - ], - "editor/src/portal/components/WelcomeBanner.stories.tsx :: With Setup Checklist": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/AccountLinkPanel.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/account-link/AccountLinkPanel.stories.tsx :: Load Forbidden": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/AccountLinkPanel.stories.tsx :: Not Linked": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Error": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Linked": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Linking": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Not Linked": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Unconfigured": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountModal.stories.tsx :: Reauth": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkedInstancesTable.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/account-link/LinkedInstancesTable.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/billing/ActivationChoiceModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/BundleCheckoutModal.stories.tsx :: First Purchase": [ - "color-contrast" - ], - "editor/src/portal/components/billing/BundleCheckoutModal.stories.tsx :: Top Up": [ - "color-contrast" - ], - "editor/src/portal/components/billing/CardPlaceholder.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/EnterpriseUpsell.stories.tsx :: Bare": [ - "color-contrast" - ], - "editor/src/portal/components/billing/EnterpriseUpsell.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/FreePdfEditorsCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/FreePlanView.stories.tsx :: Leader": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/FreePlanView.stories.tsx :: Member": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/InvoicesList.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/LinkAccountPrompt.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PaymentMethodCard.stories.tsx :: Managed In Stripe": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PaymentMethodCard.stories.tsx :: With Card": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PdfsProcessedCard.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PdfsProcessedCard.stories.tsx :: Unsynced Only": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PdfsProcessedCard.stories.tsx :: With Breakdown": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PdfsProcessedCard.stories.tsx :: With Unsynced": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PrepaidCapacityCard.stories.tsx :: Bundle Healthy": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/PrepaidCapacityCard.stories.tsx :: Bundle Low": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/PrepaidCapacityCard.stories.tsx :: Offer Nudge": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PrepayModalHeader.stories.tsx :: Step Of Three": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PrepayModalHeader.stories.tsx :: Step Of Two": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: Approaching Cap": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: Editing": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: No Cap": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: Within Cap": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SpendThisMonthCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SpendThisMonthCard.stories.tsx :: With Free Remaining": [ - "color-contrast" - ], - "editor/src/portal/components/billing/StripeCheckoutModal.stories.tsx :: Set Cap": [ - "color-contrast" - ], - "editor/src/portal/components/billing/StripeCheckoutModal.stories.tsx :: Uncapped": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SubscribedPlanView.stories.tsx :: Approaching Cap": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SubscribedPlanView.stories.tsx :: Leader": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SubscribedPlanView.stories.tsx :: With Prepaid": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/WalletMeter.stories.tsx :: Free Approaching Limit": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/WalletMeter.stories.tsx :: Free Limit Reached": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/WalletMeter.stories.tsx :: Free Plenty Left": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/docs/AuthenticationSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/ComponentsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsNav.stories.tsx :: Badged Leaf Active": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsNav.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/EndpointReferenceSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/ErrorsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/GettingStartedSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/LangSnippet.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/LangSnippet.stories.tsx :: Single Language": [ - "color-contrast" - ], - "editor/src/portal/components/docs/PlaybooksSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/RateLimitsSection.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/docs/RateLimitsSection.stories.tsx :: Free": [ - "color-contrast" - ], - "editor/src/portal/components/docs/RateLimitsSection.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/docs/SdksSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/SdksSection.stories.tsx :: Ga Only": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/SkillsSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/WebhooksSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentAudit.stories.tsx :: Approved": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentAudit.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentDrawer.stories.tsx :: Default": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentDrawer.stories.tsx :: Sensitive": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentExtractions.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentExtractions.stories.tsx :: Masked": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentExtractions.stories.tsx :: Unlocked": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentOverview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Granted": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Granted Four Eyes": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Locked Four Eyes": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ReviewQueue.stories.tsx :: Default": [ - "aria-prohibited-attr", - "color-contrast", - "empty-table-header", - "nested-interactive" - ], - "editor/src/portal/components/documents/ReviewQueue.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ReviewQueueTable.stories.tsx :: Default": [ - "aria-prohibited-attr", - "color-contrast", - "empty-table-header", - "nested-interactive" - ], - "editor/src/portal/components/documents/ReviewQueueTable.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/documents/ReviewQueueTable.stories.tsx :: Needs Review": [ - "color-contrast", - "empty-table-header", - "nested-interactive" - ], - "editor/src/portal/components/editor-admin/CredentialRotationCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/CredentialRotationCard.stories.tsx :: Recently Rotated": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentSummaryStrip.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentSummaryStrip.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentTargets.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentTargets.stories.tsx :: Free": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentTargets.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/InstanceHealthTable.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/InstanceHealthTable.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/InstanceHealthTable.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/OfflineActivationCard.stories.tsx :: Available": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/OfflineActivationCard.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/PairingPanel.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/PairingPanel.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeyCard.stories.tsx :: Personal": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeyCard.stories.tsx :: Revoked": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeysTab.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeysTab.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeysTab.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditExportModal.stories.tsx :: Export Fails": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditExportModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Non Lead Forbidden": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Team Lead Scoped": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/CreateKeyModal.stories.tsx :: Form": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/DeploymentsTab.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/DeploymentsTab.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/DeploymentsTab.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ModelsTab.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ModelsTab.stories.tsx :: Free": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ModelsTab.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/SectionHeader.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/SectionHeader.stories.tsx :: Short Sub": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Failed Step": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Multiple Selected": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Nothing Selected": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Settings": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineStepSettings.stories.tsx :: No Settings": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineStepSettings.stories.tsx :: Unsupported": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelinesTable.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/pipelines/PipelinesTable.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: Incompatible Preceding Output": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: No Matches": [ - "color-contrast" - ], - "editor/src/portal/components/policies/CatalogueSummary.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/policies/CatalogueSummary.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/policies/ClassificationLabelsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyCategoryCard.stories.tsx :: Coming Soon": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyCategoryCard.stories.tsx :: Configured": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyCategoryCard.stories.tsx :: Not Set Up": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: Active": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: Custom No Activity": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: Paused": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: With Flagged Items": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyExternalApiConfig.stories.tsx :: Custom Api Escape Hatch": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyExternalApiConfig.stories.tsx :: Notify Configured": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyFieldRow.stories.tsx :: Chips": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyFieldRow.stories.tsx :: Select": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyFieldRow.stories.tsx :: Text": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewConfig.stories.tsx :: Configured": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewConfig.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewReadConfig.stories.tsx :: Connection Selected": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewReadConfig.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicySetupWizard.stories.tsx :: Classification": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicySetupWizard.stories.tsx :: Create": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/policies/PolicySetupWizard.stories.tsx :: Edit": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/procurement/ActionModal.stories.tsx :: Pay": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ActionModal.stories.tsx :: Request Paid": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ActionModal.stories.tsx :: Sign": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ActionModal.stories.tsx :: Upload PO": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealJourney.stories.tsx :: At Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealJourney.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealJourney.stories.tsx :: Live": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Agreement": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Live": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Payment": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Quote": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Complete": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Download": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Paid Addon": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Sign Action": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocumentLedger.stories.tsx :: At Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocumentLedger.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/LockedState.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx :: Agreeing": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx :: Downloading": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementBanner.stories.tsx :: Deal Underway": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementBanner.stories.tsx :: Upsell": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: License": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: License Trial": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Schedule Call": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Manage": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Manage Maxed": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Setup": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementFlow.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementFlow.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementFlow.stories.tsx :: Unlinked": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementHome.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License Downloading": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License Online Only": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: Live": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: Payment": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: Payment Pending": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/QuoteBuilder.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/StageStepper.stories.tsx :: At Agreement": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/StageStepper.stories.tsx :: At Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/StageStepper.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionForm.stories.tsx :: Conditional Fields Revealed": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionForm.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionForm.stories.tsx :: Filled": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionModal.stories.tsx :: Edit": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionModal.stories.tsx :: Fixed Type": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionPicker.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionPicker.stories.tsx :: Delegated Create": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionPicker.stories.tsx :: Load Error": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionPicker.stories.tsx :: Preset Scoped": [ - "color-contrast" - ], - "editor/src/portal/components/sources/KpiStrip.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/sources/KpiStrip.stories.tsx :: Ready": [ - "color-contrast" - ], - "editor/src/portal/components/sources/S3ConnectionPicker.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/sources/S3ConnectionPicker.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourceModal.stories.tsx :: Choose Type": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourceModal.stories.tsx :: Edit Folder": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourceModal.stories.tsx :: Edit Webhook": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourcesTable.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/users/ConfirmModal.stories.tsx :: Danger": [ - "color-contrast" - ], - "editor/src/portal/components/users/ConfirmModal.stories.tsx :: Neutral": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Create Account Form": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: No Admin Role": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Scoped To Team": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Self Hosted Direct Create": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Self Hosted No Mail": [ - "color-contrast" - ], - "editor/src/portal/components/users/MoveToTeamModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/NewTeamModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/PendingInvitations.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/PendingInvitations.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/users/PendingInvitations.stories.tsx :: Expires Today": [ - "color-contrast" - ], - "editor/src/portal/components/users/RenameTeamModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/ResetPasswordModal.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/users/ResetPasswordModal.stories.tsx :: With Email": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Large Team": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Member States": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Org Only": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Saas Team Leader": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Single Team": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Team Wide Processor": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: With Guests": [ - "color-contrast" - ], - "editor/src/portal/data/Endpoints.stories.tsx :: By Vertical": [ - "color-contrast" - ], - "editor/src/portal/data/Ops.stories.tsx :: Agents": ["color-contrast"], - "editor/src/portal/data/Ops.stories.tsx :: Library By Category": [ - "color-contrast" - ], - "editor/src/portal/data/Ops.stories.tsx :: Pipeline Ops": ["color-contrast"], - "editor/src/portal/data/Ops.stories.tsx :: Sources And Destinations": [ - "color-contrast" - ], - "editor/src/portal/theme/MantineIntegration.stories.tsx :: Side By Side": [ - "color-contrast" - ], - "editor/src/portal/views/DeveloperDocs.stories.tsx :: Default": [ - "color-contrast", - "landmark-unique" - ], - "editor/src/portal/views/Documents.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/views/Documents.stories.tsx :: Empty": ["color-contrast"], - "editor/src/portal/views/Home.stories.tsx :: Enterprise Tier": [ - "color-contrast", - "landmark-no-duplicate-banner", - "landmark-unique" - ], - "editor/src/portal/views/Home.stories.tsx :: Free Tier": ["color-contrast"], - "editor/src/portal/views/Home.stories.tsx :: Pro Tier": [ - "color-contrast", - "landmark-no-duplicate-banner", - "landmark-unique" - ], - "editor/src/portal/views/Home.stories.tsx :: Subscribed In Procurement": [ - "color-contrast", - "landmark-no-duplicate-banner", - "landmark-unique" - ], - "editor/src/portal/views/Integrations.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/views/Integrations.stories.tsx :: No Connections": [ - "color-contrast" - ], - "editor/src/portal/views/PipelineBuilder.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/views/Pipelines.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/views/Pipelines.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/views/Policies.stories.tsx :: Default": ["color-contrast"], - "editor/src/portal/views/Policies.stories.tsx :: Empty": ["color-contrast"], - "editor/src/portal/views/Sources.stories.tsx :: Default": ["color-contrast"], - "editor/src/portal/views/Sources.stories.tsx :: Empty": ["color-contrast"], - "editor/src/proprietary/auth/ui/AuthScreens.stories.tsx :: First Time Setup": [ - "color-contrast" - ], - "editor/src/proprietary/auth/ui/AuthScreens.stories.tsx :: Signup": [ - "aria-hidden-focus" - ], - "editor/src/proprietary/components/policies/ClassificationCategoryManager.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/policies/PolicyPiiField.stories.tsx :: With Selection": [ - "color-contrast" - ], - "editor/src/proprietary/components/policies/PolicyRedactConfig.stories.tsx :: With Selection": [ - "color-contrast" - ], - "editor/src/proprietary/components/policies/PolicyWatermarkConfig.stories.tsx :: Default": [ - "button-name", - "label" - ], - "editor/src/proprietary/components/policies/PolicyWatermarkConfig.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/proprietary/components/shared/ChangeUserPasswordModal.stories.tsx :: Default": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/ChangeUserPasswordModal.stories.tsx :: Mail Disabled": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/ChangeUserPasswordModal.stories.tsx :: Non Email Username": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/DividerWithText.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/DividerWithText.stories.tsx :: Subcategory": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/ManageBillingButton.stories.tsx :: Custom Return Url": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/ManageBillingButton.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/UpdateSeatsModal.stories.tsx :: At Minimum": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/UpdateSeatsModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/GeneralWithLoginLanding.stories.tsx :: Admin Banner": [ - "color-contrast", - "label" - ], - "editor/src/proprietary/components/shared/config/GeneralWithLoginLanding.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/proprietary/components/shared/config/GeneralWithLoginLanding.stories.tsx :: With Backend Version": [ - "color-contrast", - "label" - ], - "editor/src/proprietary/components/shared/config/OverviewHeader.stories.tsx :: Signed In": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AccountSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AccountSection.stories.tsx :: Mfa Enabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAiGeneralSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Audit Logging Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Enabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamsSection.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamsSection.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamsSection.stories.tsx :: Login Disabled": [ - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/apiKeys/RefreshModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditClearDataSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditClearDataSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.stories.tsx :: All Fields Enabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: Interactive": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: With Filters Applied": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditStatsCards.stories.tsx :: Day": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditStatsCards.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditStatsCards.stories.tsx :: Month": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditSystemStatus.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditSystemStatus.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.stories.tsx :: With Currency Selector": [ - "color-contrast", - "label" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.stories.tsx :: Current Tier": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.stories.tsx :: Hosted Checkout Success": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PricingBadge.stories.tsx :: Savings": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/EmailStage.stories.tsx :: Filled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/EmailStage.stories.tsx :: With Error": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/ErrorStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/ErrorStage.stories.tsx :: Network Error": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PlanSelectionStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PlanSelectionStage.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PlanSelectionStage.stories.tsx :: With Savings": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Polling": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Timeout": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Upgrade Complete": [ - "color-contrast" - ], - "editor/src/proprietary/components/workflow/ParticipantView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/routes/login/OAuthButtons.stories.tsx :: Vertical": [ - "image-redundant-alt" - ] -} +{} diff --git a/frontend/.storybook/a11y-baseline.json b/frontend/.storybook/a11y-baseline.json index 0245e3101a..0967ef424b 100644 --- a/frontend/.storybook/a11y-baseline.json +++ b/frontend/.storybook/a11y-baseline.json @@ -1,2640 +1 @@ -{ - "editor/src/core/assets/Brand.stories.tsx :: Logos": [ - "scrollable-region-focusable" - ], - "editor/src/core/components/StorageStatsCard.stories.tsx :: Default": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/StorageStatsCard.stories.tsx :: Nearing Quota": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/StorageStatsCard.stories.tsx :: No Quota": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ColorPicker.stories.tsx :: Default": [ - "aria-input-field-name", - "button-name", - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ColorPicker.stories.tsx :: With Opacity": [ - "aria-input-field-name", - "button-name", - "color-contrast" - ], - "editor/src/core/components/annotation/shared/DrawingCanvas.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/DrawingCanvas.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/DrawingControls.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ImageUploader.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ImageUploader.stories.tsx :: With Background Removal": [ - "color-contrast" - ], - "editor/src/core/components/annotation/shared/ImageUploader.stories.tsx :: With Label And Hint": [ - "color-contrast" - ], - "editor/src/core/components/annotation/tools/DrawingTool.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/tools/DrawingTool.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/annotation/tools/ImageTool.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/annotation/tools/ImageTool.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/fileManager/CompactFileDetails.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/fileManager/CompactFileDetails.stories.tsx :: Multiple Files": [ - "color-contrast" - ], - "editor/src/core/components/fileManager/FileDetails.stories.tsx :: Compact": [ - "color-contrast" - ], - "editor/src/core/components/fileManager/FileDetails.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/fileManager/FileDetails.stories.tsx :: Empty": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx :: Cloud Only": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx :: Local And Cloud Choice": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx :: With Files": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: In Folder": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: Local Only With Save To Server": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx :: Multi Select": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileGrid.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileGrid.stories.tsx :: List Mode": [ - "aria-required-children" - ], - "editor/src/core/components/filesPage/FileOriginBadge.stories.tsx :: Cloud": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FileOriginBadge.stories.tsx :: Shared With Me": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/FolderNameDialog.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/filesPage/FolderNameDialog.stories.tsx :: Rename": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/FolderThumbnail.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: Empty": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: With Create Folder": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/MoveToFolderDialog.stories.tsx :: With Disabled Descendant": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionHistoryModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionHistoryModal.stories.tsx :: No File Selected": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionTimeline.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionTimeline.stories.tsx :: Long Chain Collapsed": [ - "color-contrast" - ], - "editor/src/core/components/filesPage/VersionTimeline.stories.tsx :: No Header": [ - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Admin Overview Login Disabled": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Admin Overview Login Enabled": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Analytics Choice": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Analytics Choice Error": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Desktop Install": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: First Login": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: First Login Default Credentials": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Mfa Setup": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Security Check": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Server License": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Server License Over Limit": [ - "aria-dialog-name" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Stepped Flow Example": [ - "aria-dialog-name", - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Tour Overview": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingModalSlide.stories.tsx :: Welcome": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingSlideShell.stories.tsx :: Default": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/OnboardingSlideShell.stories.tsx :: Not Dismissible": [ - "aria-dialog-name", - "aria-progressbar-name" - ], - "editor/src/core/components/onboarding/OnboardingSlideShell.stories.tsx :: Stepped With Back": [ - "aria-dialog-name", - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/core/components/onboarding/slides/WelcomeSlide.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/pageEditor/bulkSelectionPanel/SelectedPagesDisplay.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/BulkShareModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/BulkShareModal.stories.tsx :: Links Enabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/BulkUploadToServerModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/BulkUploadToServerModal.stories.tsx :: Single File": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ButtonToggle.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/ButtonToggle.stories.tsx :: Small": [ - "color-contrast" - ], - "editor/src/core/components/shared/DropdownListWithFooter.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/DropdownListWithFooter.stories.tsx :: Empty": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/DropdownListWithFooter.stories.tsx :: Multi Select With Footer": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/EditableSecretField.stories.tsx :: Masked": [ - "label" - ], - "editor/src/core/components/shared/EditableSecretField.stories.tsx :: Masked Disabled": [ - "label" - ], - "editor/src/core/components/shared/EditableSecretField.stories.tsx :: With Error": [ - "color-contrast", - "label-title-only" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Incorrect Password": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Multiple Files Remaining": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/EncryptedPdfUnlockModal.stories.tsx :: Processing": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ErrorBoundary.stories.tsx :: Caught Error": [ - "color-contrast" - ], - "editor/src/core/components/shared/ErrorBoundary.stories.tsx :: Custom Fallback": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileCard.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileCard.stories.tsx :: Unsupported": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileDropdownMenu.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/FileDropdownMenu.stories.tsx :: No Remove": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/FileDropdownMenu.stories.tsx :: Switching": [ - "aria-allowed-attr" - ], - "editor/src/core/components/shared/FileGrid.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileGrid.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileGrid.stories.tsx :: Search And Sort": [ - "color-contrast", - "label" - ], - "editor/src/core/components/shared/FilePickerModal.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/core/components/shared/FilePickerModal.stories.tsx :: Empty": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/FileSelectorPicker.stories.tsx :: Custom Placeholder": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileSelectorPicker.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/FileSelectorPicker.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/Footer.stories.tsx :: All Links And Cookie Banner": [ - "color-contrast" - ], - "editor/src/core/components/shared/Footer.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/InfoBanner.stories.tsx :: Warning": [ - "color-contrast" - ], - "editor/src/core/components/shared/MobileUploadModal.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "svg-img-alt" - ], - "editor/src/core/components/shared/MultiSelectControls.stories.tsx :: All Actions": [ - "color-contrast" - ], - "editor/src/core/components/shared/NavigationWarningModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/NavigationWarningModal.stories.tsx :: With Apply And Continue": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/NavigationWarningModal.stories.tsx :: With Export And Continue": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ShareFileModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ShareFileModal.stories.tsx :: Links Enabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ShareManagementModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ShareManagementModal.stories.tsx :: Links Enabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/UpdateModal.stories.tsx :: Default": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/shared/UpdateModal.stories.tsx :: Desktop Install Blocked": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/shared/UpdateModal.stories.tsx :: Desktop Install Ready To Restart": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/core/components/shared/UploadToServerModal.stories.tsx :: Already Uploaded": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/UploadToServerModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/UserSelector.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/ZipWarningModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/ZipWarningModal.stories.tsx :: Single File": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/config/LoginRequiredBanner.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/OverviewHeader.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/PendingBadge.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/PendingBadge.stories.tsx :: Large Size": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/RestartConfirmationModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/shared/config/SettingsStickyFooter.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/SettingsStickyFooter.stories.tsx :: Saving": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/GeneralSection.stories.tsx :: Admin Banner": [ - "label" - ], - "editor/src/core/components/shared/config/configSections/GeneralSection.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/shared/config/configSections/GeneralSection.stories.tsx :: With Backend Version": [ - "label" - ], - "editor/src/core/components/shared/config/configSections/HelpSection.stories.tsx :: Admin": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/HelpSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/HotkeysSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/LegalSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/LegalSection.stories.tsx :: Minimal Links": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/LegalSection.stories.tsx :: With Analytics Enabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/Overview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/Overview.stories.tsx :: Loaded": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/Overview.stories.tsx :: With Warning": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ProviderCard.stories.tsx :: Configured": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ProviderCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ProviderCard.stories.tsx :: Read Only": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.stories.tsx :: Frontend": [ - "color-contrast" - ], - "editor/src/core/components/shared/filePreview/DocumentThumbnail.stories.tsx :: Encrypted": [ - "color-contrast" - ], - "editor/src/core/components/shared/filePreview/DocumentThumbnail.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/CreateSessionFlow.stories.tsx :: Creating": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/CreateSessionFlow.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/CreateSessionFlow.stories.tsx :: No File Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/SharedSigningLauncher.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/SharedSigningLauncher.stories.tsx :: Pending Requests": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ConfigureSignatureDefaultsStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ConfigureSignatureDefaultsStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ConfigureSignatureDefaultsStep.stories.tsx :: Invisible Signature": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ReviewSessionStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ReviewSessionStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/ReviewSessionStep.stories.tsx :: Invisible Signature": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectDocumentStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectDocumentStep.stories.tsx :: Multiple Files Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectDocumentStep.stories.tsx :: No File Selected": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectParticipantsStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectParticipantsStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/signing/steps/SelectParticipantsStep.stories.tsx :: With Selection": [ - "color-contrast" - ], - "editor/src/core/components/shared/sliderWithInput/SliderWithInput.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/shared/sliderWithInput/SliderWithInput.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/shared/wetSignature/DrawSignatureCanvas.stories.tsx :: Default": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/DrawSignatureCanvas.stories.tsx :: Disabled": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/TypeSignatureText.stories.tsx :: Default": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/TypeSignatureText.stories.tsx :: Disabled": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/TypeSignatureText.stories.tsx :: Empty": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/UploadSignatureImage.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/UploadSignatureImage.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/shared/wetSignature/UploadSignatureImage.stories.tsx :: With Signature": [ - "color-contrast" - ], - "editor/src/core/components/tools/ToolLoadingFallback.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/ToolLoadingFallback.stories.tsx :: With Tool Name": [ - "color-contrast" - ], - "editor/src/core/components/tools/ToolPanelModePrompt.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/ToolRenderer.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addPageNumbers/PageNumberPreview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addPageNumbers/PageNumberPreview.stories.tsx :: With Quick Grid": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampPreview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampPreview.stories.tsx :: With Quick Grid": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampPreview.stories.tsx :: With Text": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampSetupSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampSetupSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampSetupSettings.stories.tsx :: Image Stamp": [ - "color-contrast" - ], - "editor/src/core/components/tools/addStamp/StampSetupSettings.stories.tsx :: Text Stamp With Preview": [ - "color-contrast" - ], - "editor/src/core/components/tools/addWatermark/AddWatermarkSingleStepSettings.stories.tsx :: Disabled": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/addWatermark/AddWatermarkSingleStepSettings.stories.tsx :: Text Only": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/core/components/tools/addWatermark/AddWatermarkSingleStepSettings.stories.tsx :: Text Watermark": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkFormatting.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkFormatting.stories.tsx :: Disabled": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkFormatting.stories.tsx :: Without Flatten Option": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkStyleSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkStyleSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkTextStyle.stories.tsx :: Default": [ - "button-name", - "label" - ], - "editor/src/core/components/tools/addWatermark/WatermarkTextStyle.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.stories.tsx :: Adjusted": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastBasicSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.stories.tsx :: Adjusted": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastColorSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastSingleStepSettings.stories.tsx :: Adjusted": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastSingleStepSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/adjustContrast/AdjustContrastSingleStepSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/automate/AutomationCreation.stories.tsx :: Edit Existing": [ - "color-contrast" - ], - "editor/src/core/components/tools/automate/AutomationImportModal.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/core/components/tools/automate/ToolConfigurationModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/automate/ToolConfigurationModal.stories.tsx :: With Settings": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/bookletImposition/BookletImpositionSettings.stories.tsx :: Manual Duplex": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertSignAutomationSettings.stories.tsx :: Auto Sign Mode": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertSignAutomationSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFilesSettings.stories.tsx :: Auto Sign Mode": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateFormatSettings.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateSelector.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateSelector.stories.tsx :: Pem Format": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateTypeSettings.stories.tsx :: All Sources Available": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/CertificateTypeSettings.stories.tsx :: Server Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/HardwareCertificateSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureAppearanceSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureAppearanceSettings.stories.tsx :: Visible Signature": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsDisplay.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsDisplay.stories.tsx :: Invisible": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsDisplay.stories.tsx :: Minimal Details": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsInput.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsInput.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/SignatureSettingsInput.stories.tsx :: Visible Signature": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/WetSignatureInput.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/WetSignatureInput.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/WetSignatureInput.stories.tsx :: Upload Certificate": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/AddParticipantsFlow.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/CertificateConfigModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/CertificateConfigModal.stories.tsx :: Disabled": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/CertificateConfigModal.stories.tsx :: Multiple Signatures": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/modals/SelectSignatureModal.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/tools/certSign/panels/ParticipantListPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/ParticipantListPanel.stories.tsx :: Finalized": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionActionsPanel.stories.tsx :: All Signed": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionActionsPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionActionsPanel.stories.tsx :: Finalized": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionDetailPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SessionDetailPanel.stories.tsx :: Finalized": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SignControlsPanel.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/panels/SignControlsPanel.stories.tsx :: No Signature Chosen": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/AddSignaturesStep.stories.tsx :: Default": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/AddSignaturesStep.stories.tsx :: Disabled": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/AddSignaturesStep.stories.tsx :: Placement Mode": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: Upload Ready": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/CertificateSelectionStep.stories.tsx :: User Certificate": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/ReviewSignatureStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/ReviewSignatureStep.stories.tsx :: Multiple Signatures": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/ReviewSignatureStep.stories.tsx :: Uploaded Certificate Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: Default": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: Disabled": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: Type Mode": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignatureCreationStep.stories.tsx :: With Signature": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignaturePlacementStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignaturePlacementStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/certSign/steps/SignaturePlacementStep.stories.tsx :: Placed": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/AdvancedOptionsStep.stories.tsx :: With Custom Metadata": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/CustomMetadataStep.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/CustomMetadataStep.stories.tsx :: With Entries": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/DocumentDatesStep.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/changeMetadata/steps/DocumentDatesStep.stories.tsx :: Filled": [ - "button-name" - ], - "editor/src/core/components/tools/compare/ComparePixelWorkbenchView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/compare/ComparePixelWorkbenchView.stories.tsx :: No Differences": [ - "color-contrast" - ], - "editor/src/core/components/tools/compare/ComparePixelWorkbenchView.stories.tsx :: With Warnings": [ - "color-contrast" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "color-contrast", - "label" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: Disabled": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: File Size Method": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/compress/CompressSettings.stories.tsx :: Line Art Enabled": [ - "aria-input-field-name", - "color-contrast", - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromEbookSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertFromEbookSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertFromEmailSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromEmailSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromImageSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertFromSvgSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertFromSvgSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertFromWebSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/convert/ConvertFromWebSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/convert/ConvertToEpubSettings.stories.tsx :: Azw 3 Output": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertToEpubSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertToEpubSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/convert/ConvertToPdfaSettings.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/convert/ConvertToPdfaSettings.stories.tsx :: Disabled": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/convert/ConvertToPdfaSettings.stories.tsx :: Strict Mode": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/convert/GroupedFormatDropdown.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/crop/CropAutomationSettings.stories.tsx :: Custom Area": [ - "color-contrast" - ], - "editor/src/core/components/tools/crop/CropAutomationSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/crop/CropAutomationSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/crop/CropCoordinateInputs.stories.tsx :: Automation Info": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/BookmarkEditor.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/BookmarkEditor.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/BookmarkEditor.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsSettings.stories.tsx :: Loading With Error": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsSettings.stories.tsx :: No File Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: With Error": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.stories.tsx :: With Results": [ - "color-contrast" - ], - "editor/src/core/components/tools/flatten/FlattenSettings.stories.tsx :: Custom Render Dpi": [ - "color-contrast" - ], - "editor/src/core/components/tools/flatten/FlattenSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/flatten/FlattenSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/flatten/FlattenSettings.stories.tsx :: Flatten Only Forms": [ - "color-contrast" - ], - "editor/src/core/components/tools/fullscreen/DetailedToolItem.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/fullscreen/DetailedToolItem.stories.tsx :: Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoReportView.stories.tsx :: No Data": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoResults.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoResults.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/GetPdfInfoResults.stories.tsx :: Partial Error": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/ComplianceSection.stories.tsx :: All Passed": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/ComplianceSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/ComplianceSection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/KeyValueSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/KeyValueSection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/OtherSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/OtherSection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/PerPageSection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/SummarySection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/SummarySection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/SummarySection.stories.tsx :: Hidden Title": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/sections/TableOfContentsSection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/shared/KeyValueList.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/shared/KeyValueList.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/shared/ScrollableCodeBlock.stories.tsx :: Custom Empty Message": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/shared/ScrollableCodeBlock.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/getPdfInfo/shared/SectionBlock.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/ocr/LanguagePicker.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/ocr/OCRSettings.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/redact/RedactAdvancedSettings.stories.tsx :: Default": [ - "button-name" - ], - "editor/src/core/components/tools/redact/RedactSingleStepSettings.stories.tsx :: Automatic With Words": [ - "button-name" - ], - "editor/src/core/components/tools/redact/RedactSingleStepSettings.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/redact/WordsToRedactInput.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/removeBlanks/RemoveBlanksSettings.stories.tsx :: Default": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/removeBlanks/RemoveBlanksSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/removeBlanks/RemoveBlanksSettings.stories.tsx :: Include Blank Pages": [ - "aria-input-field-name", - "label" - ], - "editor/src/core/components/tools/removePages/RemovePagesSettings.stories.tsx :: Invalid Input": [ - "color-contrast" - ], - "editor/src/core/components/tools/replaceColor/ReplaceColorSettings.stories.tsx :: Custom Color": [ - "button-name", - "label" - ], - "editor/src/core/components/tools/replaceColor/ReplaceColorSettings.stories.tsx :: Default": [ - "label" - ], - "editor/src/core/components/tools/replaceColor/ReplaceColorSettings.stories.tsx :: Disabled": [ - "label" - ], - "editor/src/core/components/tools/sanitize/SanitizeSettings.stories.tsx :: All Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/sanitize/SanitizeSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/sanitize/SanitizeSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/scannerImageSplit/ScannerImageSplitSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/scannerImageSplit/ScannerImageSplitSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ErrorNotification.stories.tsx :: Custom Title": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/shared/ErrorNotification.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/components/tools/shared/FileMetadata.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/FileMetadata.stories.tsx :: Unknown Type": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/NavigationControls.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/NavigationControls.stories.tsx :: Last File": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/NoToolsFound.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/NumberInputWithUnit.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/shared/NumberInputWithUnit.stories.tsx :: Disabled": [ - "color-contrast", - "label" - ], - "editor/src/core/components/tools/shared/NumberInputWithUnit.stories.tsx :: With Min Max": [ - "label" - ], - "editor/src/core/components/tools/shared/ResultsPreview.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ResultsPreview.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ResultsPreview.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ResultsPreview.stories.tsx :: Single File": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ToolStep.stories.tsx :: Collapsed": [ - "color-contrast" - ], - "editor/src/core/components/tools/shared/ToolStep.stories.tsx :: With Help Text And Number": [ - "color-contrast" - ], - "editor/src/core/components/tools/showJS/ShowJSView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/showJS/ShowJSView.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/showJS/ShowJSView.stories.tsx :: With Download": [ - "color-contrast" - ], - "editor/src/core/components/tools/sign/SavedSignaturesSection.stories.tsx :: Admin With Shared Delete": [ - "color-contrast" - ], - "editor/src/core/components/tools/sign/SavedSignaturesSection.stories.tsx :: At Capacity": [ - "color-contrast" - ], - "editor/src/core/components/tools/sign/SavedSignaturesSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/sign/SavedSignaturesSection.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/tools/split/SplitSettings.stories.tsx :: By Poster": [ - "color-contrast" - ], - "editor/src/core/components/tools/split/SplitSettings.stories.tsx :: By Sections": [ - "color-contrast" - ], - "editor/src/core/components/tools/split/SplitSettings.stories.tsx :: No Method Selected": [ - "color-contrast" - ], - "editor/src/core/components/tools/timestampPdf/TimestampPdfSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/timestampPdf/TimestampPdfSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/toolPicker/FavoriteStar.stories.tsx :: Default": [ - "aria-prohibited-attr" - ], - "editor/src/core/components/tools/toolPicker/FavoriteStar.stories.tsx :: Favorited": [ - "aria-prohibited-attr" - ], - "editor/src/core/components/tools/toolPicker/FavoriteStar.stories.tsx :: Sizes": [ - "aria-prohibited-attr" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: Default": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: Error": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: Multiple Signatures": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureReportView.stories.tsx :: No Signatures": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureSettings.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureSettings.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/ValidateSignatureSettings.stories.tsx :: With Cert File": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/FieldBlock.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/FieldBlock.stories.tsx :: Empty Value": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/FileSummaryHeader.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/FileSummaryHeader.stories.tsx :: Missing Metadata": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/FileSummaryHeader.stories.tsx :: No Signatures": [ - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureSection.stories.tsx :: Default": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureSection.stories.tsx :: Invalid With Error": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureSection.stories.tsx :: Self Signed Minimal Data": [ - "aria-allowed-attr", - "color-contrast" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureStatusBadge.stories.tsx :: Default": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureStatusBadge.stories.tsx :: Invalid": [ - "aria-allowed-attr" - ], - "editor/src/core/components/tools/validateSignature/reportView/SignatureStatusBadge.stories.tsx :: Untrusted Signer": [ - "aria-allowed-attr" - ], - "editor/src/core/components/viewer/DocumentReadyWrapper.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/viewer/LocalEmbedPDF.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/viewer/LocalEmbedPDF.stories.tsx :: Unsupported File": [ - "color-contrast" - ], - "editor/src/core/components/viewer/NonPdfViewer.stories.tsx :: Csv": [ - "color-contrast" - ], - "editor/src/core/components/viewer/NonPdfViewer.stories.tsx :: Unsupported": [ - "color-contrast" - ], - "editor/src/core/components/viewer/SearchInterface.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/viewer/SearchInterface.stories.tsx :: Hidden": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/CsvViewer.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/CsvViewer.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/CsvViewer.stories.tsx :: Tsv": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/HtmlViewer.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/JsonViewer.stories.tsx :: Invalid Json": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/NonPdfBanner.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/TextViewer.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/core/components/viewer/nonpdf/TextViewer.stories.tsx :: Markdown": [ - "color-contrast" - ], - "editor/src/core/tokens/Tokens.stories.tsx :: Colours": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: Success": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: Tone Matrix": ["color-contrast"], - "editor/src/core/ui/Banner.stories.tsx :: With Action": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Accents": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Disabled Dark": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Loading": ["button-name"], - "editor/src/core/ui/Button.stories.tsx :: Padding": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Shape": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Sizes": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: Variants": ["color-contrast"], - "editor/src/core/ui/Button.stories.tsx :: With Icons": ["color-contrast"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Default": ["button-name"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Loading": ["button-name"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Tick": ["button-name"], - "editor/src/core/ui/ChatFABButton.stories.tsx :: Tick While Loading": [ - "button-name" - ], - "editor/src/core/ui/ChatFABWindow.stories.tsx :: Closed": [ - "scrollable-region-focusable" - ], - "editor/src/core/ui/ChatFABWindow.stories.tsx :: Open": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/ChatFABWindow.stories.tsx :: Toggle": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Chip.stories.tsx :: Accents": ["color-contrast"], - "editor/src/core/ui/Chip.stories.tsx :: Dashed Add": ["nested-interactive"], - "editor/src/core/ui/Chip.stories.tsx :: In Context Op Chain": [ - "color-contrast" - ], - "editor/src/core/ui/Chip.stories.tsx :: Playground": [ - "color-contrast", - "nested-interactive" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: In Context Quickstart": [ - "color-contrast" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: In Context Two Up Comparison": [ - "color-contrast" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: Long Scrolling": [ - "color-contrast" - ], - "editor/src/core/ui/CodeBlock.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/Collapsible.stories.tsx :: Accordion": [ - "scrollable-region-focusable" - ], - "editor/src/core/ui/Collapsible.stories.tsx :: Default": [ - "scrollable-region-focusable" - ], - "editor/src/core/ui/Drawer.stories.tsx :: Playground": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/core/ui/Drawer.stories.tsx :: With Footer": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/core/ui/EmptyState.stories.tsx :: With CT As": ["color-contrast"], - "editor/src/core/ui/FilePicker.stories.tsx :: Accept Pdf": ["color-contrast"], - "editor/src/core/ui/FilePicker.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/FilePicker.stories.tsx :: Multiple": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Checkbox Grid Of Categories": [ - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Full Form": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Input Default": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Input Error": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Input With Icon": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Radio Group": ["color-contrast"], - "editor/src/core/ui/Forms.stories.tsx :: Radio Horizontal": [ - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Slider Confidence": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/Forms.stories.tsx :: Slider Retention": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/Inline.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/Inline.stories.tsx :: Space Between": ["color-contrast"], - "editor/src/core/ui/Inline.stories.tsx :: Wrap": ["color-contrast"], - "editor/src/core/ui/ListRow.stories.tsx :: In Card": ["color-contrast"], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Default": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Error": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Preselected": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Color Input Sm Size": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select Default": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select Error": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select Sm Size": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Multi Select With Values": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Decimal": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Default": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Error": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input Sm Size": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Number Input With Unit": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Default": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Error": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Searchable": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Select Sm Size": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider Default": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider Disabled": [ - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider No Label": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Slider With Marks": [ - "aria-input-field-name", - "color-contrast" - ], - "editor/src/core/ui/MantineForms.stories.tsx :: Watermark Form": [ - "button-name", - "color-contrast" - ], - "editor/src/core/ui/MethodBadge.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/MethodBadge.stories.tsx :: In Row": ["color-contrast"], - "editor/src/core/ui/MethodBadge.stories.tsx :: Matrix": ["color-contrast"], - "editor/src/core/ui/MetricCard.stories.tsx :: Playground": ["color-contrast"], - "editor/src/core/ui/MetricCard.stories.tsx :: Pro Tier Strip": [ - "color-contrast" - ], - "editor/src/core/ui/MetricStrip.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/NavItem.stories.tsx :: In Context Sidebar Group": [ - "color-contrast" - ], - "editor/src/core/ui/PanelHeader.stories.tsx :: With Actions": [ - "color-contrast" - ], - "editor/src/core/ui/ProgressBar.stories.tsx :: In Context Usage Meter": [ - "color-contrast" - ], - "editor/src/core/ui/ProgressBar.stories.tsx :: Playground": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/ProgressBar.stories.tsx :: Threshold Ladder": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/SegmentedControl.stories.tsx :: Variants": [ - "color-contrast" - ], - "editor/src/core/ui/SegmentedControl.stories.tsx :: With Icons": [ - "color-contrast" - ], - "editor/src/core/ui/SettingsRow.stories.tsx :: List": ["label"], - "editor/src/core/ui/SettingsRow.stories.tsx :: Select Control": ["label"], - "editor/src/core/ui/SettingsRow.stories.tsx :: Toggle": ["label"], - "editor/src/core/ui/SettingsRow.stories.tsx :: With Description": ["label"], - "editor/src/core/ui/SettingsShell.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/StatTile.stories.tsx :: Tone Row": ["color-contrast"], - "editor/src/core/ui/StatusBadge.stories.tsx :: All Tones": ["color-contrast"], - "editor/src/core/ui/StatusBadge.stories.tsx :: Default": ["color-contrast"], - "editor/src/core/ui/StatusBadge.stories.tsx :: Live": ["color-contrast"], - "editor/src/core/ui/StatusBadge.stories.tsx :: Sizes": ["color-contrast"], - "editor/src/core/ui/StepIndicator.stories.tsx :: Small": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/StepIndicator.stories.tsx :: Step 1": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/StepIndicator.stories.tsx :: Step 2": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/StepIndicator.stories.tsx :: Step 3": [ - "aria-progressbar-name" - ], - "editor/src/core/ui/Table.stories.tsx :: Basic": ["color-contrast"], - "editor/src/core/ui/Table.stories.tsx :: Interactive": ["color-contrast"], - "editor/src/core/ui/Tabs.stories.tsx :: In Context Document Verticals": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Tabs.stories.tsx :: Playground": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Tabs.stories.tsx :: With Disabled Tab": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/core/ui/Toast.stories.tsx :: Triggers": ["color-contrast"], - "editor/src/portal/components/AppShell.stories.tsx :: Mobile": [ - "color-contrast" - ], - "editor/src/portal/components/AppShell.stories.tsx :: With Home View": [ - "color-contrast" - ], - "editor/src/portal/components/AssistantPanel.stories.tsx :: Reply Fails": [ - "aria-allowed-role" - ], - "editor/src/portal/components/AssistantPanel.stories.tsx :: Slow Reply": [ - "aria-allowed-role" - ], - "editor/src/portal/components/AssistantPanel.stories.tsx :: Suggestions Only": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Closed": [ - "aria-hidden-focus" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Full Flow": [ - "aria-hidden-focus", - "color-contrast" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Loading While Closed": [ - "aria-hidden-focus" - ], - "editor/src/portal/components/ChatFABWidget.stories.tsx :: Unread Result": [ - "aria-hidden-focus" - ], - "editor/src/portal/components/EditorStatusCard.stories.tsx :: Deployment Unavailable": [ - "color-contrast" - ], - "editor/src/portal/components/EditorStatusCard.stories.tsx :: With Setup Checklist": [ - "color-contrast" - ], - "editor/src/portal/components/ErrorBoundary.stories.tsx :: Caught Error": [ - "color-contrast" - ], - "editor/src/portal/components/HomeHero.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/HomeHero.stories.tsx :: Free Tier": [ - "color-contrast" - ], - "editor/src/portal/components/PortalChrome.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/ProcessorFlow.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/ProcessorFlow.stories.tsx :: Idle Empty": [ - "color-contrast" - ], - "editor/src/portal/components/ProcessorFlow.stories.tsx :: Playground": [ - "color-contrast" - ], - "editor/src/portal/components/SearchModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/SearchModal.stories.tsx :: Empty Catalogue": [ - "color-contrast" - ], - "editor/src/portal/components/SetupChecklist.stories.tsx :: Almost Done": [ - "color-contrast" - ], - "editor/src/portal/components/SetupChecklist.stories.tsx :: In Progress": [ - "color-contrast" - ], - "editor/src/portal/components/SetupChecklist.stories.tsx :: Not Started": [ - "color-contrast" - ], - "editor/src/portal/components/Sidebar.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/Sidebar.stories.tsx :: Enterprise Tier": [ - "color-contrast" - ], - "editor/src/portal/components/Sidebar.stories.tsx :: Free Tier": [ - "color-contrast" - ], - "editor/src/portal/components/WelcomeBanner.stories.tsx :: With Setup Checklist": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/AccountLinkPanel.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/account-link/AccountLinkPanel.stories.tsx :: Load Forbidden": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/AccountLinkPanel.stories.tsx :: Not Linked": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Error": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Linked": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Linking": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Not Linked": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkAccountCard.stories.tsx :: Unconfigured": [ - "color-contrast" - ], - "editor/src/portal/components/account-link/LinkedInstancesTable.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/billing/ActivationChoiceModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/BundleCheckoutModal.stories.tsx :: First Purchase": [ - "color-contrast" - ], - "editor/src/portal/components/billing/BundleCheckoutModal.stories.tsx :: Top Up": [ - "color-contrast" - ], - "editor/src/portal/components/billing/CardPlaceholder.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/FreePlanView.stories.tsx :: Leader": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/FreePlanView.stories.tsx :: Member": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/InvoicesList.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/LinkAccountPrompt.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/billing/PrepaidCapacityCard.stories.tsx :: Bundle Healthy": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/PrepaidCapacityCard.stories.tsx :: Bundle Low": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: Approaching Cap": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: Editing": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SpendLimitCard.stories.tsx :: Within Cap": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SpendThisMonthCard.stories.tsx :: With Free Remaining": [ - "color-contrast" - ], - "editor/src/portal/components/billing/StripeCheckoutModal.stories.tsx :: Set Cap": [ - "color-contrast" - ], - "editor/src/portal/components/billing/StripeCheckoutModal.stories.tsx :: Uncapped": [ - "color-contrast" - ], - "editor/src/portal/components/billing/SubscribedPlanView.stories.tsx :: Approaching Cap": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SubscribedPlanView.stories.tsx :: Leader": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/SubscribedPlanView.stories.tsx :: With Prepaid": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/WalletMeter.stories.tsx :: Free Approaching Limit": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/WalletMeter.stories.tsx :: Free Limit Reached": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/billing/WalletMeter.stories.tsx :: Free Plenty Left": [ - "aria-progressbar-name", - "color-contrast" - ], - "editor/src/portal/components/docs/AuthenticationSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/ComponentsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsNav.stories.tsx :: Badged Leaf Active": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsNav.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/DocsSection.stories.tsx :: Without Lead": [ - "color-contrast" - ], - "editor/src/portal/components/docs/EndpointReferenceSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/ErrorsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/GettingStartedSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/LangSnippet.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/docs/LangSnippet.stories.tsx :: Single Language": [ - "color-contrast" - ], - "editor/src/portal/components/docs/PlaybooksSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/RateLimitsSection.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/docs/RateLimitsSection.stories.tsx :: Free": [ - "color-contrast" - ], - "editor/src/portal/components/docs/RateLimitsSection.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/docs/SdksSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/SdksSection.stories.tsx :: Ga Only": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/SkillsSection.stories.tsx :: Default": [ - "color-contrast", - "heading-order" - ], - "editor/src/portal/components/docs/WebhooksSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentAudit.stories.tsx :: Approved": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentAudit.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentDrawer.stories.tsx :: Default": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentDrawer.stories.tsx :: Sensitive": [ - "aria-allowed-role", - "color-contrast" - ], - "editor/src/portal/components/documents/DocumentExtractions.stories.tsx :: Masked": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Granted": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Granted Four Eyes": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ElevationBanner.stories.tsx :: Locked Four Eyes": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ReviewQueue.stories.tsx :: Default": [ - "aria-prohibited-attr", - "color-contrast", - "empty-table-header", - "nested-interactive" - ], - "editor/src/portal/components/documents/ReviewQueue.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/documents/ReviewQueueTable.stories.tsx :: Default": [ - "aria-prohibited-attr", - "color-contrast", - "empty-table-header", - "nested-interactive" - ], - "editor/src/portal/components/documents/ReviewQueueTable.stories.tsx :: Empty": [ - "empty-table-header" - ], - "editor/src/portal/components/documents/ReviewQueueTable.stories.tsx :: Needs Review": [ - "color-contrast", - "empty-table-header", - "nested-interactive" - ], - "editor/src/portal/components/editor-admin/CredentialRotationCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/CredentialRotationCard.stories.tsx :: Recently Rotated": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentSummaryStrip.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentSummaryStrip.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentTargets.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentTargets.stories.tsx :: Free": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/DeploymentTargets.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/InstanceHealthTable.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/InstanceHealthTable.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/OfflineActivationCard.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/PairingPanel.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/editor-admin/PairingPanel.stories.tsx :: Pro": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeyCard.stories.tsx :: Personal": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeyCard.stories.tsx :: Revoked": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeysTab.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeysTab.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ApiKeysTab.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditExportModal.stories.tsx :: Export Fails": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditExportModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Non Lead Forbidden": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/AuditTab.stories.tsx :: Team Lead Scoped": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/CreateKeyModal.stories.tsx :: Form": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ModelsTab.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/portal/components/infrastructure/ModelsTab.stories.tsx :: Free": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineDefinitionModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: Editing": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: Paused": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: Testing": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: With Failed Run": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineHeader.stories.tsx :: With Run Result": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Failed Step": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineInspector.stories.tsx :: Settings": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineStepSettings.stories.tsx :: No Settings": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelineStepSettings.stories.tsx :: Unsupported": [ - "color-contrast" - ], - "editor/src/portal/components/pipelines/PipelinesTable.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/pipelines/PipelinesTable.stories.tsx :: Empty": [ - "empty-table-header" - ], - "editor/src/portal/components/pipelines/ToolPicker.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyCategoryCard.stories.tsx :: Coming Soon": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyCategoryCard.stories.tsx :: Configured": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyCategoryCard.stories.tsx :: Not Set Up": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: Active": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: Custom No Activity": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: Paused": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyDetailPanel.stories.tsx :: With Flagged Items": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyExternalApiConfig.stories.tsx :: Custom Api Escape Hatch": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyExternalApiConfig.stories.tsx :: Notify Configured": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyFieldRow.stories.tsx :: Chips": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyFieldRow.stories.tsx :: Select": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyFieldRow.stories.tsx :: Text": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewConfig.stories.tsx :: Configured": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewConfig.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewReadConfig.stories.tsx :: Connection Selected": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicyPurviewReadConfig.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicySetupWizard.stories.tsx :: Classification": [ - "color-contrast" - ], - "editor/src/portal/components/policies/PolicySetupWizard.stories.tsx :: Create": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/policies/PolicySetupWizard.stories.tsx :: Edit": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/procurement/DealJourney.stories.tsx :: At Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealJourney.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealJourney.stories.tsx :: Live": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Agreement": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Live": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Payment": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Quote": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DealStatusHero.stories.tsx :: Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Complete": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Download": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Paid Addon": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocRow.stories.tsx :: Sign Action": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocumentLedger.stories.tsx :: At Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/DocumentLedger.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/LockedState.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx :: Agreeing": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementAgreement.stories.tsx :: Downloading": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementBanner.stories.tsx :: Deal Underway": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementBanner.stories.tsx :: Upsell": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: License": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: License Trial": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Manage": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementExtras.stories.tsx :: Trial Setup": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementFlow.stories.tsx :: Default": [ - "color-contrast", - "scrollable-region-focusable" - ], - "editor/src/portal/components/procurement/ProcurementFlow.stories.tsx :: Unlinked": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementHome.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License Downloading": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: License Online Only": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/ProcurementStages.stories.tsx :: Payment": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/QuoteBuilder.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/procurement/StageStepper.stories.tsx :: Locked": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionForm.stories.tsx :: Conditional Fields Revealed": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionForm.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionForm.stories.tsx :: Filled": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionModal.stories.tsx :: Edit": [ - "color-contrast" - ], - "editor/src/portal/components/sources/ConnectionModal.stories.tsx :: Fixed Type": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourceModal.stories.tsx :: Edit Folder": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourceModal.stories.tsx :: Edit Webhook": [ - "color-contrast" - ], - "editor/src/portal/components/sources/SourcesTable.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/components/users/ConfirmModal.stories.tsx :: Danger": [ - "color-contrast" - ], - "editor/src/portal/components/users/ConfirmModal.stories.tsx :: Neutral": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Create Account Form": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: No Admin Role": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Open": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Scoped To Team": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Self Hosted Direct Create": [ - "color-contrast" - ], - "editor/src/portal/components/users/InviteMemberModal.stories.tsx :: Self Hosted No Mail": [ - "color-contrast" - ], - "editor/src/portal/components/users/MoveToTeamModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/NewTeamModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/PendingInvitations.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/PendingInvitations.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/portal/components/users/PendingInvitations.stories.tsx :: Expires Today": [ - "color-contrast" - ], - "editor/src/portal/components/users/RenameTeamModal.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/ResetPasswordModal.stories.tsx :: Default": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/users/ResetPasswordModal.stories.tsx :: With Email": [ - "color-contrast", - "label" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Large Team": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Member States": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Org Only": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Saas Team Leader": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Single Team": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: Team Wide Processor": [ - "color-contrast" - ], - "editor/src/portal/components/users/UsersDirectory.stories.tsx :: With Guests": [ - "color-contrast" - ], - "editor/src/portal/data/Endpoints.stories.tsx :: By Vertical": [ - "color-contrast" - ], - "editor/src/portal/data/Ops.stories.tsx :: Agents": ["color-contrast"], - "editor/src/portal/data/Ops.stories.tsx :: Library By Category": [ - "color-contrast" - ], - "editor/src/portal/data/Ops.stories.tsx :: Pipeline Ops": ["color-contrast"], - "editor/src/portal/theme/MantineIntegration.stories.tsx :: Side By Side": [ - "color-contrast" - ], - "editor/src/portal/views/DeveloperDocs.stories.tsx :: Default": [ - "color-contrast", - "landmark-unique" - ], - "editor/src/portal/views/Documents.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/views/Documents.stories.tsx :: Empty": ["color-contrast"], - "editor/src/portal/views/Home.stories.tsx :: Enterprise Tier": [ - "color-contrast", - "landmark-no-duplicate-banner", - "landmark-unique" - ], - "editor/src/portal/views/Home.stories.tsx :: Free Tier": ["color-contrast"], - "editor/src/portal/views/Home.stories.tsx :: Pro Tier": [ - "color-contrast", - "landmark-no-duplicate-banner", - "landmark-unique" - ], - "editor/src/portal/views/Home.stories.tsx :: Subscribed In Procurement": [ - "color-contrast", - "landmark-no-duplicate-banner", - "landmark-unique" - ], - "editor/src/portal/views/Integrations.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/portal/views/Integrations.stories.tsx :: No Connections": [ - "color-contrast" - ], - "editor/src/portal/views/Pipelines.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/views/Pipelines.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/portal/views/Sources.stories.tsx :: Default": ["color-contrast"], - "editor/src/portal/views/Sources.stories.tsx :: Empty": ["color-contrast"], - "editor/src/proprietary/auth/ui/AuthScreens.stories.tsx :: Signup": [ - "aria-hidden-focus" - ], - "editor/src/proprietary/auth/ui/EmailPasswordForm.stories.tsx :: With Errors": [ - "color-contrast" - ], - "editor/src/proprietary/components/policies/PolicyPiiField.stories.tsx :: With Selection": [ - "color-contrast" - ], - "editor/src/proprietary/components/policies/PolicyRedactConfig.stories.tsx :: With Selection": [ - "color-contrast" - ], - "editor/src/proprietary/components/policies/PolicyWatermarkConfig.stories.tsx :: Default": [ - "button-name", - "color-contrast", - "label" - ], - "editor/src/proprietary/components/policies/PolicyWatermarkConfig.stories.tsx :: Disabled": [ - "color-contrast", - "label" - ], - "editor/src/proprietary/components/shared/ChangeUserPasswordModal.stories.tsx :: Default": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/ChangeUserPasswordModal.stories.tsx :: Mail Disabled": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/ChangeUserPasswordModal.stories.tsx :: Non Email Username": [ - "aria-dialog-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/DividerWithText.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/UpdateSeatsModal.stories.tsx :: At Minimum": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/UpdateSeatsModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/EnterpriseRequiredBanner.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/GeneralWithLoginLanding.stories.tsx :: Admin Banner": [ - "label" - ], - "editor/src/proprietary/components/shared/config/GeneralWithLoginLanding.stories.tsx :: Default": [ - "label" - ], - "editor/src/proprietary/components/shared/config/GeneralWithLoginLanding.stories.tsx :: With Backend Version": [ - "label" - ], - "editor/src/proprietary/components/shared/config/OverviewHeader.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/OverviewHeader.stories.tsx :: Signed In": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AccountSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AccountSection.stories.tsx :: Mfa Enabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AccountSection.stories.tsx :: Sso User": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAiDocumentsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAiGeneralSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAiLimitsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAiModelsSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Audit Logging Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Enabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminAuditSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminFolderAccessSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/AdminFolderAccessSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/ApiKeys.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/ApiKeys.stories.tsx :: Load Error": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/ApiKeys.stories.tsx :: Loading": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/LoginAgreementEditor.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/LoginAgreementEditor.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamsSection.stories.tsx :: Default": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamsSection.stories.tsx :: Empty": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/TeamsSection.stories.tsx :: Login Disabled": [ - "color-contrast", - "empty-table-header" - ], - "editor/src/proprietary/components/shared/config/configSections/apiKeys/RefreshModal.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditClearDataSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditClearDataSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditEventsTable.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditEventsTable.stories.tsx :: With File Metadata Columns": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.stories.tsx :: All Fields Enabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: Interactive": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.stories.tsx :: With Filters Applied": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditStatsCards.stories.tsx :: Day": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditStatsCards.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditStatsCards.stories.tsx :: Month": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditSystemStatus.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/audit/AuditSystemStatus.stories.tsx :: Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.stories.tsx :: Login Disabled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.stories.tsx :: With Currency Selector": [ - "color-contrast", - "label" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.stories.tsx :: Current Tier": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.stories.tsx :: Enterprise Plan": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/plan/PlanCard.stories.tsx :: Free Plan": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsChart.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsChart.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsTable.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/config/configSections/usage/UsageAnalyticsTable.stories.tsx :: Empty": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.stories.tsx :: Default": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.stories.tsx :: Hosted Checkout Success": [ - "button-name", - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PriceDisplay.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PriceDisplay.stories.tsx :: Enterprise With Total": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PriceDisplay.stories.tsx :: Simple": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PricingBadge.stories.tsx :: Current": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PricingBadge.stories.tsx :: Popular": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/components/PricingBadge.stories.tsx :: Savings": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/EmailStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/EmailStage.stories.tsx :: Filled": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/EmailStage.stories.tsx :: With Error": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/ErrorStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/ErrorStage.stories.tsx :: Network Error": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.stories.tsx :: Redirecting": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PlanSelectionStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PlanSelectionStage.stories.tsx :: Enterprise": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/PlanSelectionStage.stories.tsx :: With Savings": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Polling": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Timeout": [ - "color-contrast" - ], - "editor/src/proprietary/components/shared/stripeCheckout/stages/SuccessStage.stories.tsx :: Upgrade Complete": [ - "color-contrast" - ], - "editor/src/proprietary/components/workflow/ParticipantView.stories.tsx :: Completed": [ - "color-contrast" - ], - "editor/src/proprietary/components/workflow/ParticipantView.stories.tsx :: Default": [ - "color-contrast" - ], - "editor/src/proprietary/components/workflow/ParticipantView.stories.tsx :: Expired": [ - "color-contrast" - ], - "editor/src/proprietary/routes/login/OAuthButtons.stories.tsx :: Vertical": [ - "image-redundant-alt" - ] -} +{} diff --git a/frontend/.storybook/a11y-changed.mjs b/frontend/.storybook/a11y-changed.mjs index 5ff7a9e633..f6daef5dbb 100644 --- a/frontend/.storybook/a11y-changed.mjs +++ b/frontend/.storybook/a11y-changed.mjs @@ -10,7 +10,8 @@ // embedded interpreter runs on Windows, but sed/grep/sort do not exist for // developers calling tasks from PowerShell. import { execFileSync } from "node:child_process"; -import { existsSync } from "node:fs"; +import { readdirSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; const base = process.argv[2] || "origin/main"; @@ -40,9 +41,25 @@ for (const f of changed) { continue; } if (TEST.test(f) || !SOURCE.test(f)) continue; - const sibling = f.replace(SOURCE, ""); - for (const s of [`${sibling}.stories.tsx`, `${sibling}.stories.ts`]) - if (existsSync(s)) stories.add(s); + // A story file does not have to match its source's case — tokens.css sits + // beside Tokens.stories.tsx. Deriving the name from the source and trusting + // existsSync silently skips those on a case-sensitive filesystem, and on a + // case-insensitive one feeds the scan a path no result will ever match. Read + // the directory instead and compare case-insensitively, then use the name as + // it is actually spelled on disk. + const dir = dirname(f) || "."; + const stem = basename(f).replace(SOURCE, "").toLowerCase(); + let entries; + try { + entries = readdirSync(dir); + } catch { + continue; + } + for (const entry of entries) { + if (!STORY.test(entry)) continue; + if (entry.replace(STORY, "").toLowerCase() !== stem) continue; + stories.add(join(dir, entry).split("\\").join("/")); + } } // One line, each path quoted: the output is interpolated into a task command, diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index e9d7e7f3d9..f568b34347 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -19,6 +19,11 @@ import { LinkProvider, type LinkState } from "@portal/contexts/LinkContext"; import { ThemeProvider, useTheme } from "@portal/contexts/ThemeContext"; import { UIProvider } from "@portal/contexts/UIContext"; import { SuiProvider } from "@portal/theme/SuiProvider"; +import { MantineProvider } from "@mantine/core"; +import { + mantineTheme as editorMantineTheme, + editorCssVariablesResolver, +} from "@core/theme/mantineTheme"; import { handlers } from "@portal/mocks/handlers"; import { configureSupabase } from "@proprietary/auth/supabase/supabaseClient"; import i18next from "i18next"; @@ -199,6 +204,37 @@ const withLocale: Decorator = (Story, context) => { return ; }; +/** + * Applies the Mantine theme the story's component actually runs under in the + * app: PortalApp wraps the Processor in SuiProvider, while the editor wraps + * everything else in its own ThemeProvider. Getting this wrong is not just + * cosmetic — the two themes carry different neutral ramps, so rendering an + * editor component under the Processor's theme drops it onto Mantine's stock + * greys and reports contrast failures the app doesn't have. + */ +function StoryTheme({ + isPortalStory, + colorScheme, + children, +}: { + isPortalStory: boolean; + colorScheme: "light" | "dark"; + children: React.ReactNode; +}) { + if (isPortalStory) { + return {children}; + } + return ( + + {children} + + ); +} + const withProviders: Decorator = (Story, context) => { const tier = (context.globals.tier as Tier) ?? "pro"; const linkState = @@ -214,16 +250,21 @@ const withProviders: Decorator = (Story, context) => { // the portal's base.css keys its reset/typography on. Give portal stories // the same wrapper (and only them — the scoping exists precisely so portal // styles never apply to editor components). - const isPortalStory = (context.parameters.fileName ?? "").includes( - "/portal/", - ); + // `fileName` is only injected by the dev/build pipeline — under the Vitest + // runner it is absent, so path alone would silently drop every portal story + // onto the editor theme (where portal-only palette entries like `amber` + // resolve to nothing and render unstyled). The title prefix is the fallback + // that survives both environments. + const isPortalStory = + (context.parameters.fileName ?? "").includes("/portal/") || + context.title.startsWith("Portal/"); return ( - + {/* LinkProvider must wrap TierProvider: TierContext derives its tier from useLink() (matches App.tsx's nesting). */} @@ -241,7 +282,7 @@ const withProviders: Decorator = (Story, context) => { - + @@ -274,6 +315,14 @@ const preview: Preview = { // any violation. Context is left at the addon default (the document root) // so it resolves under both the Storybook UI and the Vitest browser mount. test: "error", + context: { + // Nodes carrying this attribute render a facsimile of the user's own + // document — their stamp text, their watermark, in the colour and + // opacity they chose. WCAG contrast governs the interface, not the + // content authored through it, and the controls that set those values + // are checked normally. + exclude: ["[data-user-content-preview]"], + }, }, }, globalTypes: { diff --git a/frontend/editor/public/locales/en-GB/translation.toml b/frontend/editor/public/locales/en-GB/translation.toml index ae494e9582..a91d52cca0 100644 --- a/frontend/editor/public/locales/en-GB/translation.toml +++ b/frontend/editor/public/locales/en-GB/translation.toml @@ -3001,12 +3001,15 @@ tooltip = "Runs in the cloud (included, no extra charge)" tooltip = "Pick colour from screen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Choose colour" [common] back = "Back" cancel = "Cancel" close = "Close" +codeSample = "Code sample" collapse = "Collapse" confirm = "Confirm" continue = "Continue" @@ -3023,6 +3026,7 @@ refresh = "Refresh" remaining = "Remaining" retry = "Retry" save = "Save" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Clear selected" @@ -3185,6 +3189,7 @@ title = "Compression Method" [compress.settings] desiredSize = "Desired File Size" desiredSizePlaceholder = "Enter size" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Compression is an easy way to reduce your file size. Pick File Size to enter a target size and have us adjust quality for you. Pick Quality to set compression strength manually." @@ -3824,11 +3829,13 @@ shareSelected = "Share Files" sharing = "Sharing" showAll = "Show All" showHistory = "Show History" +sortBy = "Sort files" sortByDate = "Sort by Date" sortByName = "Sort by Name" sortBySize = "Sort by Size" storage = "Storage" storageState = "Storage" +storageUsed = "Storage used" synced = "Synced" title = "Upload PDF Files" toolChain = "Tools Applied" @@ -4168,6 +4175,7 @@ noFilesInStorageOpen = "No files available in storage. Open some files first." open = "Open" openFile = "Open File" openFiles = "Open Files" +selectFile = "Select {{name}}" selectFromStorage = "Select from Storage" upload = "Upload" uploadFile = "Upload File" @@ -5127,6 +5135,7 @@ activeFiles = "The Active Files view shows all of the PDFs you allTools = "This is the Tools panel, where you can browse and select from all available PDF tools." close = "Close" cropSettings = "Now that we've selected the file we want crop, we can configure the Crop tool to choose the area that we want to crop the PDF to." +dialogLabel = "Onboarding" fileCheckbox = "Clicking one of the files selects it for processing. You can select multiple files for batch operations." fileReplacement = "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools." filesButton = "The Files button on the Quick Access bar allows you to upload PDFs to use the tools on." @@ -5563,6 +5572,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs used" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5642,6 +5652,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -6045,6 +6056,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Choose the plan that fits your needs" title = "Available Plans" @@ -6280,6 +6292,7 @@ revoked = "Revoked" unnamed = "Unnamed instance" [portal.accountLink.instances.columns] +actions = "Actions" instance = "Instance" lastSeen = "Last seen" linked = "Linked" @@ -6624,6 +6637,7 @@ reachedTitle = "Monthly spend limit reached" title = "Couldn't open Stripe portal" [portal.billing.walletMeter] +barAria = "Free PDFs used" capSuffix_one = "of {{allowance}} free PDFs used" capSuffix_other = "of {{allowance}} free PDFs used" eyebrow = "Processor trial" @@ -7184,6 +7198,7 @@ sensitiveTitle = "Sensitive — access required" [portal.documents.table.columns] action = "Pipeline / Action" +actions = "Actions" document = "Document" product = "Product" status = "Status" @@ -7481,6 +7496,7 @@ rolledBack = "Rolled back" rolling = "Rolling out" [portal.infrastructure.deployments] +loadAria = "Load for {{name}}" msValue = "{{value}} ms" throughputValue = "{{value}}/min" @@ -7526,6 +7542,7 @@ disabled = "Disabled" [portal.infrastructure.models] heading = "Models" +loadAria = "Load for {{name}}" msValue = "{{value}} ms" subheading = "The model catalogue and routing that powers document processing across your workspace." @@ -7892,6 +7909,7 @@ paused = "Paused" [portal.pipelines.table] name = "Pipeline" +open = "Open" sources = "Sources" status = "Status" steps = "Steps" @@ -8783,6 +8801,7 @@ unused = "Unused" [portal.sources.table] documents = "Documents" +open = "Open" source = "Source" status = "Status" usedBy = "Policies" diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 8ce96c2552..69211b6360 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -3005,12 +3005,15 @@ tooltip = "This operation will use your cloud credits" tooltip = "Pick color from screen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Choose color" [common] back = "Back" cancel = "Cancel" close = "Close" +codeSample = "Code sample" collapse = "Collapse" confirm = "Confirm" continue = "Continue" @@ -3028,6 +3031,7 @@ refresh = "Refresh" remaining = "Remaining" retry = "Retry" save = "Save" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Clear selected" @@ -3190,6 +3194,7 @@ title = "Compression Method" [compress.settings] desiredSize = "Desired File Size" desiredSizePlaceholder = "Enter size" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Compression is an easy way to reduce your file size. Pick File Size to enter a target size and have us adjust quality for you. Pick Quality to set compression strength manually." @@ -3833,11 +3838,13 @@ shareSelected = "Share Files" sharing = "Sharing" showAll = "Show All" showHistory = "Show History" +sortBy = "Sort files" sortByDate = "Sort by Date" sortByName = "Sort by Name" sortBySize = "Sort by Size" storage = "Storage" storageState = "Storage" +storageUsed = "Storage used" synced = "Synced" title = "Upload PDF Files" toolChain = "Tools Applied" @@ -4177,6 +4184,7 @@ noFilesInStorageOpen = "No files available in storage. Open some files first." open = "Open" openFile = "Open File" openFiles = "Open Files" +selectFile = "Select {{name}}" selectFromStorage = "Select from Storage" upload = "Upload" uploadFile = "Upload File" @@ -5169,6 +5177,7 @@ activeFiles = "The Active Files view shows all of the PDFs you allTools = "This is the Tools panel, where you can browse and select from all available PDF tools." close = "Close" cropSettings = "Now that we've selected the file we want crop, we can configure the Crop tool to choose the area that we want to crop the PDF to." +dialogLabel = "Onboarding" fileCheckbox = "Files on the workbench are selected for processing. You can select multiple files for batch operations using the left files sidebar." fileReplacement = "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools." filesButton = "The Files button on the Quick Access bar allows you to upload PDFs to use the tools on." @@ -5605,6 +5614,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs used" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5684,6 +5694,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -6087,6 +6098,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Choose the plan that fits your needs" title = "Available Plans" @@ -6322,6 +6334,7 @@ revoked = "Revoked" unnamed = "Unnamed instance" [portal.accountLink.instances.columns] +actions = "Actions" instance = "Instance" lastSeen = "Last seen" linked = "Linked" @@ -6666,6 +6679,7 @@ reachedTitle = "Monthly spend limit reached" title = "Couldn't open Stripe portal" [portal.billing.walletMeter] +barAria = "Free PDFs used" capSuffix_one = "of {{allowance}} free PDFs used" capSuffix_other = "of {{allowance}} free PDFs used" eyebrow = "Processor trial" @@ -7559,6 +7573,7 @@ rolledBack = "Rolled back" rolling = "Rolling out" [portal.infrastructure.deployments] +loadAria = "Load for {{name}}" msValue = "{{value}} ms" throughputValue = "{{value}}/min" @@ -7604,6 +7619,7 @@ disabled = "Disabled" [portal.infrastructure.models] heading = "Models" +loadAria = "Load for {{name}}" msValue = "{{value}} ms" subheading = "The model catalogue and routing that powers document processing across your workspace." diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css index 581f3af671..9cbda9b6ca 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css +++ b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css @@ -95,7 +95,7 @@ margin-bottom: 8px; } .payg-planhead__lbl--free { - color: var(--c-success); + color: var(--color-green-dark); } .payg-planhead__lbl--meter { color: var(--payg-accent); @@ -242,7 +242,7 @@ background: color-mix(in srgb, var(--c-success) 14%, transparent); } [data-mantine-color-scheme="dark"] .payg-hero__credit { - color: var(--c-success); + color: var(--color-green-dark); background: color-mix(in srgb, var(--c-success) 18%, transparent); } @@ -459,11 +459,11 @@ } .payg-gate[data-enabled="true"] .payg-gate__chip { background: color-mix(in srgb, var(--c-success) 16%, transparent); - color: var(--c-success); + color: var(--color-green-dark); } .payg-gate[data-enabled="false"] .payg-gate__chip { background: color-mix(in srgb, var(--c-danger) 16%, transparent); - color: var(--c-danger); + color: var(--color-red-dark); } .payg-gate__label { font-size: 0.8125rem; @@ -486,11 +486,11 @@ background: var(--c-surface-sunken); } .payg-gate__tag[data-variant="pause"] { - color: var(--c-danger); + color: var(--color-red-dark); background: color-mix(in srgb, var(--c-danger) 12%, transparent); } [data-mantine-color-scheme="dark"] .payg-gate__tag[data-variant="pause"] { - color: var(--c-danger); + color: var(--color-red-dark); background: color-mix(in srgb, var(--c-danger) 18%, transparent); } diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css index 9e89fa38eb..af391f0aeb 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css +++ b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css @@ -298,7 +298,7 @@ font-size: 1rem !important; } .paygf-explainer__icon--free { - color: var(--c-success); + color: var(--color-green-dark); } .paygf-explainer__icon--paid { color: var(--payg-accent); diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css index d0ed711434..b6b72f504a 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css +++ b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css @@ -10,7 +10,7 @@ .scc { --scc-accent: var(--c-primary); - --scc-accent-text: var(--c-primary); + --scc-accent-text: var(--c-accent-text); --scc-accent-soft: color-mix(in srgb, var(--c-primary) 12%, transparent); --scc-accent-border: color-mix(in srgb, var(--c-primary) 25%, transparent); --scc-chip-bg: var(--c-surface-sunken); @@ -24,7 +24,7 @@ [data-mantine-color-scheme="dark"] .scc { /* Chip surface/border track the neutral --c-* tokens (base rule); only the brand-azure accent is tuned brighter for dark. */ - --scc-accent-text: var(--c-primary); + --scc-accent-text: var(--c-accent-text); --scc-accent-soft: color-mix(in srgb, var(--c-primary) 16%, transparent); } diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx index abfb0110ca..8811e537b2 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx @@ -60,6 +60,7 @@ export function FreeMeterPanel({ snap }: { snap: FreeSnapshot }) { = ({ {storageStats.quota && ( 80 diff --git a/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx b/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx index ec854426e6..bfc53d5252 100644 --- a/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx +++ b/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx @@ -51,6 +51,13 @@ export const ColorPicker: React.FC = ({ format="hex" value={selectedColor} onChange={onColorChange} + // The saturation area and hue bar are role="slider" divs; these are + // their only accessible names. + saturationLabel={t( + "colorPicker.saturation", + "Saturation and brightness", + )} + hueLabel={t("colorPicker.hue", "Hue")} swatches={[ "#000000", "#0066cc", @@ -73,6 +80,7 @@ export const ColorPicker: React.FC = ({ max={100} value={opacity} onChange={onOpacityChange} + thumbLabel={resolvedOpacityLabel} marks={[ { value: 25, label: "25%" }, { value: 50, label: "50%" }, diff --git a/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx b/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx index 94d2e35e47..5bce53410e 100644 --- a/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx +++ b/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx @@ -161,7 +161,7 @@ const AddFileCard = ({ icon={icons.uploadIconName} width="1.25rem" height="1.25rem" - style={{ color: "var(--c-primary)", flexShrink: 0 }} + style={{ color: "var(--c-accent-text)", flexShrink: 0 }} /> {isUploadHover && ( = ({ {currentFile && ` • v${currentFile.versionNumber || 1}`} {hasMultipleFiles && ( - + {currentFileIndex + 1} of {selectedFiles.length} )} diff --git a/frontend/editor/src/core/components/fileManager/DragOverlay.tsx b/frontend/editor/src/core/components/fileManager/DragOverlay.tsx index 023bb59d14..04af539346 100644 --- a/frontend/editor/src/core/components/fileManager/DragOverlay.tsx +++ b/frontend/editor/src/core/components/fileManager/DragOverlay.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Stack, Text, useMantineTheme, alpha } from "@mantine/core"; +import { Stack, Text } from "@mantine/core"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import { useTranslation } from "react-i18next"; @@ -9,7 +9,6 @@ interface DragOverlayProps { const DragOverlay: React.FC = ({ isVisible }) => { const { t } = useTranslation(); - const theme = useMantineTheme(); if (!isVisible) return null; @@ -21,8 +20,9 @@ const DragOverlay: React.FC = ({ isVisible }) => { left: 0, right: 0, bottom: 0, - backgroundColor: alpha(theme.colors.blue[6], 0.1), - border: `0.125rem dashed ${theme.colors.blue[6]}`, + // The prompt below is the drop affordance on its own. Tinting the whole + // region and ringing it in dashed accent reads as a second, competing + // surface, so the overlay stays transparent. borderRadius: "1.875rem", display: "flex", alignItems: "center", @@ -32,10 +32,12 @@ const DragOverlay: React.FC = ({ isVisible }) => { }} > + {/* Muted ink rather than the accent shade: it has to read on whatever + the overlay happens to sit on, in either scheme. */} - + {t("fileManager.dropFilesHere", "Drop files here to upload")} diff --git a/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx b/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx index 59b42ef954..890906abda 100644 --- a/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx +++ b/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx @@ -103,7 +103,7 @@ const EmptyFilesState: React.FC = () => { icon={icons.uploadIconName} width="1.25rem" height="1.25rem" - style={{ color: "var(--c-primary)" }} + style={{ color: "var(--c-accent-text)" }} /> {isUploadHover && ( diff --git a/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx b/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx index f43df3d86c..4f94155d4e 100644 --- a/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx +++ b/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx @@ -86,19 +86,29 @@ const FileInfoCard: React.FC = ({ }} > - + {t("fileManager.details", "File Details")} - + {/* The viewport is focusable and named so keyboard users can scroll the + detail list once it overflows. */} + diff --git a/frontend/editor/src/core/components/filesPage/FileGrid.tsx b/frontend/editor/src/core/components/filesPage/FileGrid.tsx index ce9be12ad2..7bed49bd0b 100644 --- a/frontend/editor/src/core/components/filesPage/FileGrid.tsx +++ b/frontend/editor/src/core/components/filesPage/FileGrid.tsx @@ -906,35 +906,47 @@ function ListView({ return (
    + {/* Each direct child is a columnheader: a role="row" may only own cells, so + the sort controls and the select-all box have to sit inside one. */}
    {onSetSelection && visibleFileIds.length > 0 ? ( - { - onSetSelection(allSelected ? new Set() : new Set(visibleFileIds)); - }} - aria-label={ - allSelected - ? t("filesPage.deselectAll", "Clear selection") - : t("filesPage.selectAll", "Select all") - } - /> + + { + onSetSelection( + allSelected ? new Set() : new Set(visibleFileIds), + ); + }} + aria-label={ + allSelected + ? t("filesPage.deselectAll", "Clear selection") + : t("filesPage.selectAll", "Select all") + } + /> + ) : (
    @@ -1091,7 +1103,12 @@ function FolderRow({ className={`files-page-list-row${isDropTarget ? " is-drop-target" : ""}`} >
    ); } @@ -1254,35 +1275,40 @@ function FileRow({ isInWorkspace ? " is-in-workspace" : "" }`} > - {/* Checkbox only shows in multi-select mode (see FileCard). When the - checkbox is hidden the first grid column collapses, but the row's - CSS grid keeps the columns aligned via the named template, so no - empty cell shows. */} + {/* Each direct child is a gridcell: a role="row" may only own cells, so the + checkbox and the actions menu have to sit inside one. + + The checkbox only shows in multi-select mode (see FileCard). When it is + hidden the first grid column collapses, but the row's CSS grid keeps the + columns aligned via the named template, so no empty cell shows. */} {multiSelectActive ? ( - { - // Toggle this file in/out of the selection without modifier keys. - e.stopPropagation(); - onClick({ - ...e, - shiftKey: false, - ctrlKey: true, - metaKey: true, - } as unknown as React.MouseEvent); - }} - onChange={() => { - /* handled by onClick */ - }} - aria-label={t("filesPage.selectFile", "Select file {{name}}", { - name: file.name, - })} - /> + + { + // Toggle this file in/out of the selection without modifier keys. + e.stopPropagation(); + onClick({ + ...e, + shiftKey: false, + ctrlKey: true, + metaKey: true, + } as unknown as React.MouseEvent); + }} + onChange={() => { + /* handled by onClick */ + }} + aria-label={t("filesPage.selectFile", "Select file {{name}}", { + name: file.name, + })} + /> + ) : ( // Empty cell preserves grid column alignment. ); } diff --git a/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx b/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx index a4e9b6e3f9..3d94610e0b 100644 --- a/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx +++ b/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx @@ -32,12 +32,12 @@ const styles = { }, cloud: { background: "color-mix(in srgb, var(--c-primary) 16%, transparent)", - color: "var(--c-primary)", + color: "var(--c-accent-text)", }, shared: { background: "color-mix(in srgb, var(--mantine-color-orange-6) 16%, transparent)", - color: "var(--mantine-color-orange-6)", + color: "var(--color-amber-dark)", }, }; diff --git a/frontend/editor/src/core/components/filesPage/FilesPage.css b/frontend/editor/src/core/components/filesPage/FilesPage.css index 4815a1b31e..368bae2521 100644 --- a/frontend/editor/src/core/components/filesPage/FilesPage.css +++ b/frontend/editor/src/core/components/filesPage/FilesPage.css @@ -339,6 +339,9 @@ } .files-page-list-row.is-header [data-sortable="true"] { + /* Block so the hit area and hover tint fill the columnheader cell that wraps + it, rather than hugging the label text. */ + display: block; cursor: pointer; padding: 0.2rem 0.4rem; margin: -0.2rem -0.4rem; @@ -741,7 +744,7 @@ height: 5rem; border-radius: 50%; background: color-mix(in srgb, var(--c-primary) 12%, transparent); - color: var(--c-primary); + color: var(--c-accent-text); margin-bottom: 0.25rem; } @@ -980,7 +983,7 @@ .files-page-details-version-timeline-count { margin-left: auto; font-weight: 600; - color: var(--c-primary); + color: var(--c-accent-text); text-transform: none; letter-spacing: 0; } @@ -1112,7 +1115,29 @@ .files-page-details-version-timeline-expand-btn:hover span { color: var(--c-text); } - +.files-page-details-version-timeline-delta { + font-size: 0.82rem; + color: var(--c-text); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-flex; + align-items: baseline; + gap: 0.25rem; +} +.files-page-details-version-timeline-delta.is-origin { + font-weight: 400; + color: var(--c-text-subtle); + font-style: italic; +} +.files-page-details-version-timeline-delta-plus { + color: var(--c-accent-text); + font-weight: 700; +} +.files-page-details-version-timeline-spacer { + flex: 1; +} .files-page-details-version-timeline-chevron { color: var(--c-text-subtle); transition: transform 0.15s ease; @@ -1120,7 +1145,7 @@ .files-page-details-version-timeline-chevron.is-expanded { transform: rotate(180deg); - color: var(--c-primary); + color: var(--c-accent-text); } .files-page-details-version-timeline-expanded { @@ -1185,7 +1210,7 @@ justify-content: center; gap: 1rem; pointer-events: none; - color: var(--c-primary); + color: var(--c-accent-text); font-weight: 600; font-size: 1.1rem; z-index: 10; diff --git a/frontend/editor/src/core/components/filesPage/FolderThumbnail.tsx b/frontend/editor/src/core/components/filesPage/FolderThumbnail.tsx index 39554a21ee..928a75eb3c 100644 --- a/frontend/editor/src/core/components/filesPage/FolderThumbnail.tsx +++ b/frontend/editor/src/core/components/filesPage/FolderThumbnail.tsx @@ -151,7 +151,10 @@ export function FolderThumbnail({ borderRadius: "999px", background: "var(--c-surface, #fff)", border: `1px solid ${accent}`, - color: accent, + // The ring carries the folder's accent; the numeral does not. + // Folder colours are user-chosen and many are too light to read + // as text on the white pill. + color: "var(--c-text)", fontSize: "0.7rem", fontWeight: 700, display: "inline-flex", diff --git a/frontend/editor/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css b/frontend/editor/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css index f9439dcdd2..58108b55c1 100644 --- a/frontend/editor/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css +++ b/frontend/editor/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css @@ -325,7 +325,7 @@ .v2Badge { background: var(--c-primary-tint); - color: var(--c-accent-fg); + color: var(--c-accent-text); padding: 3px 9px; border-radius: 6px; font-size: 12px; diff --git a/frontend/editor/src/core/components/onboarding/OnboardingSlideShell.tsx b/frontend/editor/src/core/components/onboarding/OnboardingSlideShell.tsx index 49d94662eb..3d077301eb 100644 --- a/frontend/editor/src/core/components/onboarding/OnboardingSlideShell.tsx +++ b/frontend/editor/src/core/components/onboarding/OnboardingSlideShell.tsx @@ -101,7 +101,10 @@ export default function OnboardingSlideShell({ ); return ( - , because only Modal.Content lands + // props on the role="dialog" element — the slide draws its own title, so the + // dialog needs an aria-label to have an accessible name. + -
    -
    -
    - - Stirling -
    -
    - {showProgress && ( - - {t("onboarding.stepOf", "Step {{current}} of {{total}}", { - current: stepIndex + 1, - total: stepCount, - })} - - )} - {allowDismiss && ( - - + + +
    +
    +
    + - - )} -
    -
    + Stirling +
    +
    + {showProgress && ( + + {t("onboarding.stepOf", "Step {{current}} of {{total}}", { + current: stepIndex + 1, + total: stepCount, + })} + + )} + {allowDismiss && ( + + + + )} +
    +
    - {showProgress && ( -
    - {Array.from({ length: stepCount }, (_, index) => ( - - ))} -
    - )} - -
    - -
    -
    -
    - {hero} -
    -
    - -
    - {title} -
    - -
    - {body} - -
    - -
    - {backButtons.length === 0 ? ( -
    {actions}
    - ) : ( -
    -
    - {backButtons.map((button) => ( - onAction(button.action)} - variant="tertiary" - accent="neutral" - disabled={button.disabled} - aria-label={t("onboarding.buttons.back", "Back")} - > - - - ))} -
    - {actions} + {showProgress && ( +
    + {Array.from({ length: stepCount }, (_, index) => ( + + ))}
    )} + +
    + +
    +
    +
    + {hero} +
    +
    + +
    + {title} +
    + +
    + {body} + +
    + +
    + {backButtons.length === 0 ? ( +
    {actions}
    + ) : ( +
    +
    + {backButtons.map((button) => ( + onAction(button.action)} + variant="tertiary" + accent="neutral" + disabled={button.disabled} + aria-label={t("onboarding.buttons.back", "Back")} + > + + + ))} +
    + {actions} +
    + )} +
    +
    -
    -
    - + + + ); } diff --git a/frontend/editor/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx b/frontend/editor/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx index 23566a5527..aa04911832 100644 --- a/frontend/editor/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx +++ b/frontend/editor/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx @@ -50,7 +50,7 @@ export default function AnalyticsChoiceSlide({
    {analyticsError && ( -
    +
    {analyticsError}
    )} diff --git a/frontend/editor/src/core/components/onboarding/slides/FirstLoginSlide.tsx b/frontend/editor/src/core/components/onboarding/slides/FirstLoginSlide.tsx index d684592a4a..0b1f5bb8f2 100644 --- a/frontend/editor/src/core/components/onboarding/slides/FirstLoginSlide.tsx +++ b/frontend/editor/src/core/components/onboarding/slides/FirstLoginSlide.tsx @@ -125,7 +125,7 @@ function FirstLoginForm({ icon="info-rounded" width={20} height={20} - style={{ color: "var(--c-primary)", flexShrink: 0 }} + style={{ color: "var(--c-accent-text)", flexShrink: 0 }} /> {t( diff --git a/frontend/editor/src/core/components/onboarding/slides/SecurityCheckSlide.tsx b/frontend/editor/src/core/components/onboarding/slides/SecurityCheckSlide.tsx index 6101605772..7f3e9f12a5 100644 --- a/frontend/editor/src/core/components/onboarding/slides/SecurityCheckSlide.tsx +++ b/frontend/editor/src/core/components/onboarding/slides/SecurityCheckSlide.tsx @@ -26,7 +26,7 @@ export default function SecurityCheckSlide({ icon="error" width={20} height={20} - style={{ color: "var(--c-danger)", flexShrink: 0 }} + style={{ color: "var(--color-red-dark)", flexShrink: 0 }} /> {i18n.t( diff --git a/frontend/editor/src/core/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css b/frontend/editor/src/core/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css index 1ecd05808b..59729a79e0 100644 --- a/frontend/editor/src/core/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css +++ b/frontend/editor/src/core/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css @@ -222,7 +222,8 @@ /* Error helper text above the input */ .errorText { margin-top: 0.25rem; - color: var(--text-brand-accent); + /* The brand red is a fill; error copy takes the theme's error ink. */ + color: var(--color-red-dark); } /* Compact error container for inline tool settings */ @@ -237,7 +238,7 @@ /* Two-line clamp for compact error text */ .errorTextClamp { - color: var(--text-brand-accent); + color: var(--color-red-dark); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; diff --git a/frontend/editor/src/core/components/shared/DropdownListWithFooter.tsx b/frontend/editor/src/core/components/shared/DropdownListWithFooter.tsx index 0816ac9a14..4def62470d 100644 --- a/frontend/editor/src/core/components/shared/DropdownListWithFooter.tsx +++ b/frontend/editor/src/core/components/shared/DropdownListWithFooter.tsx @@ -135,7 +135,11 @@ const DropdownListWithFooter: React.FC = ({ zIndex={zIndex} > + {/* A real button: Popover.Target stamps aria-haspopup/aria-expanded on + its child, and those are only permitted on an actual control. */} = ({ padding: "8px 12px", backgroundColor: "light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))", + color: "inherit", + textAlign: "left", + width: "100%", opacity: disabled ? 0.6 : 1, cursor: disabled ? "not-allowed" : "pointer", minHeight: "36px", diff --git a/frontend/editor/src/core/components/shared/EditableSecretField.tsx b/frontend/editor/src/core/components/shared/EditableSecretField.tsx index a515a46cae..cce50f90a0 100644 --- a/frontend/editor/src/core/components/shared/EditableSecretField.tsx +++ b/frontend/editor/src/core/components/shared/EditableSecretField.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useId, useState, useRef, useEffect } from "react"; import { PasswordInput, Group, Tooltip, TextInput } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { ActionIcon } from "@app/ui/ActionIcon"; @@ -33,6 +33,7 @@ export default function EditableSecretField({ }: EditableSecretFieldProps) { const { t } = useTranslation(); const resolvedPlaceholder = placeholder ?? t("common.enterValue"); + const fieldId = useId(); const [isEditing, setIsEditing] = useState(false); const [tempValue, setTempValue] = useState(""); const inputRef = useRef(null); @@ -67,6 +68,7 @@ export default function EditableSecretField({
    {label && (
    , document.body, diff --git a/frontend/editor/src/core/ui/Dropdown.css b/frontend/editor/src/core/ui/Dropdown.css index 38b20ee1d8..8b84af29f1 100644 --- a/frontend/editor/src/core/ui/Dropdown.css +++ b/frontend/editor/src/core/ui/Dropdown.css @@ -54,7 +54,7 @@ .sui-dd__item.is-active { background: var(--c-primary-subtle); - color: var(--c-accent-fg); + color: var(--c-accent-text); font-weight: 500; } diff --git a/frontend/editor/src/core/ui/EmptyState.css b/frontend/editor/src/core/ui/EmptyState.css index bf8250bc82..bd3d7cf03e 100644 --- a/frontend/editor/src/core/ui/EmptyState.css +++ b/frontend/editor/src/core/ui/EmptyState.css @@ -22,7 +22,7 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; - color: var(--c-primary); + color: var(--c-accent-text); } .sui-empty__title { diff --git a/frontend/editor/src/core/ui/FormField.css b/frontend/editor/src/core/ui/FormField.css index f5cbe2d5fc..792845c255 100644 --- a/frontend/editor/src/core/ui/FormField.css +++ b/frontend/editor/src/core/ui/FormField.css @@ -14,7 +14,9 @@ } .sui-field__required { - color: var(--color-red); + /* The base red is a fill colour; as text on the form background it only + reaches 3.4:1. */ + color: var(--color-red-dark); } .sui-field__control { @@ -29,5 +31,5 @@ } .sui-field--error .sui-field__help { - color: var(--color-red); + color: var(--color-red-dark); } diff --git a/frontend/editor/src/core/ui/Forms.stories.tsx b/frontend/editor/src/core/ui/Forms.stories.tsx index 0d14cf3392..dff72b6da7 100644 --- a/frontend/editor/src/core/ui/Forms.stories.tsx +++ b/frontend/editor/src/core/ui/Forms.stories.tsx @@ -182,6 +182,7 @@ export const Slider_Confidence: Story = { step={0.01} onChange={setV} formatValue={(x) => x.toFixed(2)} + aria-label="Minimum confidence" /> ); @@ -203,6 +204,7 @@ export const Slider_Retention: Story = { step={1} onChange={setDays} formatValue={(d) => `${d} days`} + aria-label="Retain artifacts for" /> ); @@ -262,6 +264,7 @@ export const FullForm: Story = { step={0.01} onChange={setConf} formatValue={(v) => v.toFixed(2)} + aria-label="Confidence gate" /> diff --git a/frontend/editor/src/core/ui/ListRow.css b/frontend/editor/src/core/ui/ListRow.css index 836dba4d8f..019b679f5c 100644 --- a/frontend/editor/src/core/ui/ListRow.css +++ b/frontend/editor/src/core/ui/ListRow.css @@ -36,23 +36,23 @@ background: var(--c-surface-sunken); } .sui-listrow__leading[data-tone="success"] { - color: var(--color-green); + color: var(--color-green-dark); background: color-mix(in srgb, var(--color-green) 14%, transparent); } .sui-listrow__leading[data-tone="warning"] { - color: var(--color-amber); + color: var(--color-amber-dark); background: color-mix(in srgb, var(--color-amber) 14%, transparent); } .sui-listrow__leading[data-tone="danger"] { - color: var(--color-red); + color: var(--color-red-dark); background: color-mix(in srgb, var(--color-red) 14%, transparent); } .sui-listrow__leading[data-tone="info"] { - color: var(--c-primary); + color: var(--c-accent-text); background: color-mix(in srgb, var(--c-primary) 14%, transparent); } .sui-listrow__leading[data-tone="purple"] { - color: var(--color-purple); + color: var(--color-purple-dark); background: color-mix(in srgb, var(--color-purple) 14%, transparent); } diff --git a/frontend/editor/src/core/ui/MantineForms.css b/frontend/editor/src/core/ui/MantineForms.css index ebedf547f7..29cd64413d 100644 --- a/frontend/editor/src/core/ui/MantineForms.css +++ b/frontend/editor/src/core/ui/MantineForms.css @@ -55,7 +55,7 @@ /* Pills match SUI's Chip component: small rounded tags. */ .sui-mantine-pill { background: var(--c-primary-tint) !important; - color: var(--c-primary-hover) !important; + color: var(--c-accent-text) !important; border: 1px solid var(--c-primary-border) !important; border-radius: var(--radius-sm) !important; font-size: 0.75rem !important; diff --git a/frontend/editor/src/core/ui/MantineForms.stories.tsx b/frontend/editor/src/core/ui/MantineForms.stories.tsx index 08e2b2e179..e8860c1f30 100644 --- a/frontend/editor/src/core/ui/MantineForms.stories.tsx +++ b/frontend/editor/src/core/ui/MantineForms.stories.tsx @@ -446,6 +446,7 @@ export const Slider_Default: Story = { max={1} step={0.01} formatValue={(x) => x.toFixed(2)} + aria-label="Confidence threshold" /> ); @@ -467,6 +468,7 @@ export const Slider_WithMarks: Story = { max={365} step={1} formatValue={(d) => `${d}d`} + aria-label="Retain artifacts for" marks={[ { value: 30, label: "30d" }, { value: 90, label: "90d" }, @@ -494,6 +496,7 @@ export const Slider_NoLabel: Story = { max={100} step={1} showValue={false} + aria-label="Opacity" /> ); diff --git a/frontend/editor/src/core/ui/MethodBadge.css b/frontend/editor/src/core/ui/MethodBadge.css index 9b138ed389..1cc89c02a4 100644 --- a/frontend/editor/src/core/ui/MethodBadge.css +++ b/frontend/editor/src/core/ui/MethodBadge.css @@ -15,7 +15,7 @@ border-color: var(--color-green-border); } .sui-method--post { - color: var(--c-primary); + color: var(--c-accent-text); background: var(--c-primary-tint); border-color: var(--c-primary-border); } diff --git a/frontend/editor/src/core/ui/MetricCard.css b/frontend/editor/src/core/ui/MetricCard.css index 8c7adfeb00..520342c65a 100644 --- a/frontend/editor/src/core/ui/MetricCard.css +++ b/frontend/editor/src/core/ui/MetricCard.css @@ -83,10 +83,10 @@ font-size: 0.6875rem; } .sui-metric__delta--up { - color: var(--color-green); + color: var(--color-green-dark); } .sui-metric__delta--down { - color: var(--color-red); + color: var(--color-red-dark); } .sui-metric__delta--flat, .sui-metric__desc { diff --git a/frontend/editor/src/core/ui/NavItem.css b/frontend/editor/src/core/ui/NavItem.css index 8b9949eab5..9fd39f3eff 100644 --- a/frontend/editor/src/core/ui/NavItem.css +++ b/frontend/editor/src/core/ui/NavItem.css @@ -20,7 +20,7 @@ } .sui-navitem.is-active { background: var(--c-primary-subtle); - color: var(--c-accent-fg); + color: var(--c-accent-text); font-weight: 500; } .sui-navitem.is-active:hover { @@ -76,17 +76,17 @@ background: var(--color-red); } .sui-navitem[data-accent="blue"] .sui-navitem__icon { - color: var(--c-primary); + color: var(--c-accent-text); } .sui-navitem[data-accent="purple"] .sui-navitem__icon { - color: var(--color-purple); + color: var(--color-purple-dark); } .sui-navitem[data-accent="green"] .sui-navitem__icon { - color: var(--color-green); + color: var(--color-green-dark); } .sui-navitem[data-accent="amber"] .sui-navitem__icon { - color: var(--color-amber); + color: var(--color-amber-dark); } .sui-navitem[data-accent="red"] .sui-navitem__icon { - color: var(--color-red); + color: var(--color-red-dark); } diff --git a/frontend/editor/src/core/ui/PanelHeader.css b/frontend/editor/src/core/ui/PanelHeader.css index 02f32c3707..f8dafd2303 100644 --- a/frontend/editor/src/core/ui/PanelHeader.css +++ b/frontend/editor/src/core/ui/PanelHeader.css @@ -46,7 +46,7 @@ button.sui-panelhdr__bar:hover { height: 1.75rem; border-radius: 9999px; background: var(--mantine-color-blue-light); - color: var(--mantine-color-blue-filled); + color: var(--c-accent-text); flex-shrink: 0; } @@ -154,15 +154,15 @@ button.sui-panelhdr__bar:hover { var(--mantine-color-blue-filled) 18%, transparent ); - color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled)); + color: var(--c-accent-text); } /* Dark mode: the subtle gray close button is too dim against the dark rail — brighten it to a clearly-visible light grey (near-white on hover). */ [data-mantine-color-scheme="dark"] .sui-panelhdr__close { - color: var(--mantine-color-gray-4); + color: var(--c-text-subtle); } [data-mantine-color-scheme="dark"] .sui-panelhdr__close:hover { - color: var(--mantine-color-gray-2); + color: var(--c-text-subtle); } diff --git a/frontend/editor/src/core/ui/ProgressBar.stories.tsx b/frontend/editor/src/core/ui/ProgressBar.stories.tsx index 7c8b386dda..8c88bc039a 100644 --- a/frontend/editor/src/core/ui/ProgressBar.stories.tsx +++ b/frontend/editor/src/core/ui/ProgressBar.stories.tsx @@ -6,7 +6,12 @@ const meta: Meta = { component: ProgressBar, tags: ["autodocs"], parameters: { layout: "padded" }, - args: { value: 0.5, height: 6, thresholded: false }, + args: { + value: 0.5, + height: 6, + thresholded: false, + label: "Docs processed", + }, argTypes: { value: { control: { type: "range", min: 0, max: 1, step: 0.01 } }, height: { control: { type: "number" } }, @@ -42,7 +47,7 @@ export const ThresholdLadder: Story = { {Math.round(v * 100)}% - +
    ))}
    diff --git a/frontend/editor/src/core/ui/ProgressBar.tsx b/frontend/editor/src/core/ui/ProgressBar.tsx index e6ca30c0e1..cb11a2982a 100644 --- a/frontend/editor/src/core/ui/ProgressBar.tsx +++ b/frontend/editor/src/core/ui/ProgressBar.tsx @@ -10,8 +10,9 @@ export interface ProgressBarProps { /** Optional override colour (CSS gradient or solid). Disables threshold behaviour. */ color?: string; className?: string; - /** Accessible label for screen readers. */ - label?: string; + /** Accessible name — describe what is being measured ("Storage used"), since + * the bar carries no visible text of its own. */ + label: string; } function clamp01(n: number) { diff --git a/frontend/editor/src/core/ui/Select.tsx b/frontend/editor/src/core/ui/Select.tsx index 242ddf9740..c4861a7752 100644 --- a/frontend/editor/src/core/ui/Select.tsx +++ b/frontend/editor/src/core/ui/Select.tsx @@ -46,6 +46,7 @@ export interface SelectProps { id?: string; name?: string; "aria-label"?: string; + "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-describedby"?: string; required?: boolean; @@ -74,6 +75,7 @@ type PassthroughProps = Omit< | "id" | "name" | "aria-label" + | "aria-labelledby" | "aria-describedby" | "required" | "disabled" @@ -106,6 +108,7 @@ export function Select({ id, name, "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledBy, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, required, @@ -128,6 +131,7 @@ export function Select({ id, name, "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, required, disabled, diff --git a/frontend/editor/src/core/ui/SettingsRow.tsx b/frontend/editor/src/core/ui/SettingsRow.tsx index 65f48cddc8..8dbf9600ab 100644 --- a/frontend/editor/src/core/ui/SettingsRow.tsx +++ b/frontend/editor/src/core/ui/SettingsRow.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import { cloneElement, isValidElement, useId, type ReactNode } from "react"; import "@app/ui/SettingsRow.css"; export interface SettingsRowProps { @@ -23,17 +23,31 @@ export function SettingsRow({ control, className, }: SettingsRowProps) { + const labelId = useId(); + // The row's label is plain text beside the control, not a
    {isLoading ? (
    diff --git a/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx b/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx index daa066bd70..921de50853 100644 --- a/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx +++ b/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx @@ -93,7 +93,11 @@ export function LinkedInstancesTable({ }, { key: "actions", - header: "", + header: ( + + {t("portal.accountLink.instances.columns.actions", "Actions")} + + ), align: "right", render: (i) => i.revoked ? null : ( diff --git a/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx b/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx index b70fbb5a26..c7ab0e8704 100644 --- a/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx +++ b/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx @@ -72,6 +72,7 @@ export function PrepaidCapacityCard({ 1
    -

    {t("portal.docs.quickstart.step1.title")}

    +

    {t("portal.docs.quickstart.step1.title")}

    {t("portal.docs.quickstart.step1.body")}

    2
    -

    {t("portal.docs.quickstart.step2.title")}

    +

    {t("portal.docs.quickstart.step2.title")}

    {t("portal.docs.quickstart.step2.body")}

    3
    -

    {t("portal.docs.quickstart.step3.title")}

    +

    {t("portal.docs.quickstart.step3.title")}

    {t("portal.docs.quickstart.step3.body")}

    {playbooks.map((p) => ( -

    {p.title}

    +

    {p.title}

    {p.blurb}

    {p.steps.map((step, i) => ( diff --git a/frontend/editor/src/portal/components/docs/SdksSection.tsx b/frontend/editor/src/portal/components/docs/SdksSection.tsx index 1a0e72933e..331117af68 100644 --- a/frontend/editor/src/portal/components/docs/SdksSection.tsx +++ b/frontend/editor/src/portal/components/docs/SdksSection.tsx @@ -32,7 +32,7 @@ export function SdksSection({ sdks }: { sdks: Sdk[] }) { {sdk.icon} -

    {sdk.name}

    +

    {sdk.name}

    {badge && ( {t(badge.labelKey)} diff --git a/frontend/editor/src/portal/components/docs/SkillsSection.tsx b/frontend/editor/src/portal/components/docs/SkillsSection.tsx index ef34705c43..e373fc25c3 100644 --- a/frontend/editor/src/portal/components/docs/SkillsSection.tsx +++ b/frontend/editor/src/portal/components/docs/SkillsSection.tsx @@ -19,7 +19,7 @@ export function SkillsSection({ skills }: { skills: AgentSkill[] }) { -

    {s.name}

    +

    {s.name}

    {s.blurb}

    {s.ops} diff --git a/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx b/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx index 8e96e4d997..41a9b5c561 100644 --- a/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx +++ b/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx @@ -65,7 +65,14 @@ export function DeploymentsTab() { width: "9rem", render: (r) => (
    - + {pct(r.load)}
    ), diff --git a/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx b/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx index 6d097b33af..505f41ac24 100644 --- a/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx +++ b/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx @@ -77,7 +77,12 @@ export function ModelsTab() { width: "9rem", render: (m) => (
    - + {pct(m.load)}
    ), diff --git a/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx b/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx index 318f23aca5..7aee96c822 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx @@ -144,10 +144,15 @@ export function ProcurementAgreement({ {/* The tray scrolls, not the paper. The paper is its natural height inside it, so mid-document it runs flush to the footer with no grey beneath, and the tray's bottom padding only comes into view once the buyer reaches the end — the page ending is what shows they got there. */} + {/* Focusable and named: signing is gated on scrolling to the end, so the + tray has to be scrollable by keyboard as well as pointer. */}
    {loading &&

    {t("portal.procurement.agreement.loading")}

    } diff --git a/frontend/editor/src/portal/components/users/ResetPasswordModal.tsx b/frontend/editor/src/portal/components/users/ResetPasswordModal.tsx index d1cfe9e739..48b32d4398 100644 --- a/frontend/editor/src/portal/components/users/ResetPasswordModal.tsx +++ b/frontend/editor/src/portal/components/users/ResetPasswordModal.tsx @@ -140,7 +140,13 @@ export function ResetPasswordModal({ } >
    - + {/* FormField's label wires up to the row wrapper, not this + input, so name the input directly. */} + - +
    @@ -131,9 +134,9 @@ export function DeveloperDocs() {
    {hasToc && ( - +
    )}
    ); diff --git a/frontend/editor/src/portal/views/EditorAdmin.css b/frontend/editor/src/portal/views/EditorAdmin.css index 1ccd0d63b1..34aa81d69f 100644 --- a/frontend/editor/src/portal/views/EditorAdmin.css +++ b/frontend/editor/src/portal/views/EditorAdmin.css @@ -96,11 +96,11 @@ } .portal-editor__target-icon--blue { background: var(--c-primary-tint); - color: var(--c-primary); + color: var(--c-accent-text); } .portal-editor__target-icon--purple { background: var(--color-purple-light); - color: var(--color-purple); + color: var(--color-purple-dark); } .portal-editor__target-titles { @@ -192,15 +192,15 @@ .portal-editor__pairing-icon--blue { background: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface)); - color: var(--c-primary); + color: var(--c-accent-text); } .portal-editor__pairing-icon--purple { background: var(--color-purple-light); - color: var(--color-purple); + color: var(--color-purple-dark); } .portal-editor__pairing-icon--green { background: var(--color-green-light); - color: var(--color-green); + color: var(--color-green-dark); } .portal-editor__pairing-titles { @@ -331,7 +331,7 @@ padding: 0.125rem 0.4375rem; border-radius: var(--radius-sm); background: var(--color-purple-light); - color: var(--color-purple); + color: var(--color-purple-dark); } .portal-editor__token-row { diff --git a/frontend/editor/src/portal/views/Home.css b/frontend/editor/src/portal/views/Home.css index 732bd617a5..236966c2fc 100644 --- a/frontend/editor/src/portal/views/Home.css +++ b/frontend/editor/src/portal/views/Home.css @@ -129,5 +129,5 @@ } .portal-home__quick-row:hover .portal-home__quick-arrow { - color: var(--c-primary); + color: var(--c-accent-text); } diff --git a/frontend/editor/src/portal/views/Infrastructure.css b/frontend/editor/src/portal/views/Infrastructure.css index cd532daeeb..660bd4a69f 100644 --- a/frontend/editor/src/portal/views/Infrastructure.css +++ b/frontend/editor/src/portal/views/Infrastructure.css @@ -113,7 +113,7 @@ .portal-infra__export-error { margin: 0; font-size: 0.8125rem; - color: var(--c-danger); + color: var(--color-red-dark); } .portal-infra__export-actions { @@ -373,7 +373,7 @@ .portal-infra__attestation-link { font-size: 0.75rem; font-weight: 600; - color: var(--c-primary); + color: var(--c-accent-text); text-decoration: none; } @@ -429,7 +429,7 @@ height: 1.75rem; border-radius: var(--radius-md); background: var(--c-primary-tint); - color: var(--c-primary); + color: var(--c-accent-text); font-size: 0.875rem; } diff --git a/frontend/editor/src/portal/views/Integrations.css b/frontend/editor/src/portal/views/Integrations.css index 9d90d70b13..d006fcbb84 100644 --- a/frontend/editor/src/portal/views/Integrations.css +++ b/frontend/editor/src/portal/views/Integrations.css @@ -66,7 +66,7 @@ .portal-integrations__filter.is-active { background: var(--c-primary-tint); - color: var(--c-primary); + color: var(--c-accent-text); } .portal-integrations__filter-count { @@ -243,7 +243,7 @@ button.portal-integrations__row[aria-expanded="true"] { gap: 0.375rem; font-size: 0.8125rem; font-weight: 500; - color: var(--c-success); + color: var(--color-green-dark); } .portal-integrations__status-dot { diff --git a/frontend/editor/src/portal/views/Pipelines.css b/frontend/editor/src/portal/views/Pipelines.css index 6fac70672e..f4bc729b4d 100644 --- a/frontend/editor/src/portal/views/Pipelines.css +++ b/frontend/editor/src/portal/views/Pipelines.css @@ -62,7 +62,7 @@ border-radius: var(--radius-md); font-size: 0.875rem; background: var(--c-primary-tint); - color: var(--c-primary); + color: var(--c-accent-text); } .portal-pipelines__muted { @@ -79,7 +79,7 @@ .portal-pipelines__caret.is-open { transform: rotate(90deg); - color: var(--c-primary); + color: var(--c-accent-text); } /* Table skeleton */ diff --git a/frontend/editor/src/portal/views/Policies.css b/frontend/editor/src/portal/views/Policies.css index e1a821ee27..fedcbbf835 100644 --- a/frontend/editor/src/portal/views/Policies.css +++ b/frontend/editor/src/portal/views/Policies.css @@ -56,7 +56,7 @@ .portal-policies__setup-link { font-size: 0.8125rem; font-weight: 600; - color: var(--c-primary); + color: var(--c-accent-text); text-decoration: underline; text-underline-offset: 2px; } @@ -70,7 +70,10 @@ } .portal-policies__card--locked { - opacity: 0.7; + /* Recede via surface and ink rather than opacity, which would fade the + card's own text below the contrast floor. */ + background: var(--c-surface-sunken); + color: var(--c-text-muted); } .portal-policies__card-identity { @@ -165,7 +168,7 @@ .portal-policies__card-cta { font-size: 0.75rem; font-weight: 600; - color: var(--c-primary); + color: var(--c-accent-text); } .portal-policies__chevron-right { @@ -297,7 +300,7 @@ padding: 0; font-size: 0.75rem; font-weight: 600; - color: var(--c-primary); + color: var(--c-accent-text); cursor: pointer; } @@ -378,13 +381,13 @@ } .portal-policies__activity-icon--success { - color: var(--color-green); + color: var(--color-green-dark); } .portal-policies__activity-icon--warning { - color: var(--color-amber); + color: var(--color-amber-dark); } .portal-policies__activity-icon--info { - color: var(--c-primary); + color: var(--c-accent-text); } @keyframes portal-policies-spin { diff --git a/frontend/editor/src/portal/views/Procurement.css b/frontend/editor/src/portal/views/Procurement.css index b721768952..556fff726a 100644 --- a/frontend/editor/src/portal/views/Procurement.css +++ b/frontend/editor/src/portal/views/Procurement.css @@ -89,7 +89,7 @@ color: var(--c-text); } .portal-qb__req { - color: var(--c-danger); + color: var(--color-red-dark); } .portal-qb__field[data-invalid] input, .portal-qb__field[data-invalid] select { @@ -98,7 +98,7 @@ .portal-qb__error { margin: 10px 0 0; font-size: 12px; - color: var(--c-danger); + color: var(--color-red-dark); } .portal-qb__row { display: flex; @@ -138,7 +138,7 @@ .portal-qb__discount { margin: 6px 0 0; font-size: 11.5px; - color: var(--c-success); + color: var(--color-green-dark); } .portal-qb__opts { display: flex; @@ -179,7 +179,7 @@ color: var(--c-text); } .portal-qb__opt[data-on] .portal-qb__opt-title { - color: var(--c-primary); + color: var(--c-accent-text); } .portal-qb__opt-sub { font-size: 11.5px; @@ -342,7 +342,7 @@ color: var(--c-text); } .portal-qb__lines li[data-kind="DISCOUNT"] { - color: var(--c-success); + color: var(--color-green-dark); } .portal-qb__lines li[data-kind="INCLUDED"] span:last-child { color: var(--c-text-subtle); @@ -516,7 +516,7 @@ align-items: center; justify-content: center; /* Holds a CheckIcon, which takes its stroke from `color` and its size from its own prop. */ - color: var(--c-success); + color: var(--color-green-dark); background: var(--c-success-subtle); } .portal-hero__live-text { @@ -897,7 +897,7 @@ margin-top: 0.75rem; } .portal-proc__error { - color: var(--c-danger); + color: var(--color-red-dark); font-size: 0.8125rem; margin: 0.5rem 0 0; } diff --git a/frontend/editor/src/portal/views/Sources.css b/frontend/editor/src/portal/views/Sources.css index 132c137c89..bb39eae86c 100644 --- a/frontend/editor/src/portal/views/Sources.css +++ b/frontend/editor/src/portal/views/Sources.css @@ -86,7 +86,7 @@ .portal-sources__caret.is-open { transform: rotate(90deg); - color: var(--c-primary); + color: var(--c-accent-text); } /* Expanded detail panel */ @@ -226,7 +226,7 @@ display: block; width: 100%; margin-top: 0.5rem; - color: var(--c-primary); + color: var(--c-accent-text); } .portal-sources__chips { @@ -414,7 +414,7 @@ } .portal-sources__type-card.is-selected .portal-sources__type-icon { - color: var(--c-primary); + color: var(--c-accent-text); } .portal-sources__type-icon .portal-sources__type-svg { diff --git a/frontend/editor/src/portal/views/Users.css b/frontend/editor/src/portal/views/Users.css index 75591cc8d8..ae11d3b88d 100644 --- a/frontend/editor/src/portal/views/Users.css +++ b/frontend/editor/src/portal/views/Users.css @@ -265,7 +265,7 @@ padding: 0.5rem 0.75rem; border-radius: 8px; background: color-mix(in srgb, var(--c-danger) 12%, transparent); - color: var(--c-danger); + color: var(--color-red-dark); font-size: 0.85rem; } @@ -279,7 +279,7 @@ } .portal-users__link { - color: var(--c-primary); + color: var(--c-accent-text); text-decoration: none; } .portal-users__link:hover { @@ -337,7 +337,7 @@ background: none; border: none; cursor: pointer; - color: var(--c-primary); + color: var(--c-accent-text); font-size: 0.8rem; font-weight: 500; white-space: nowrap; @@ -492,7 +492,7 @@ background: none; border: none; border-top: 1px solid var(--c-border-subtle); - color: var(--c-primary); + color: var(--c-accent-text); font-size: 0.8rem; font-weight: 500; cursor: pointer; diff --git a/frontend/editor/src/proprietary/auth/ui/OAuthButtons.tsx b/frontend/editor/src/proprietary/auth/ui/OAuthButtons.tsx index 3644ed91bc..6ffb202cd3 100644 --- a/frontend/editor/src/proprietary/auth/ui/OAuthButtons.tsx +++ b/frontend/editor/src/proprietary/auth/ui/OAuthButtons.tsx @@ -24,6 +24,8 @@ export const oauthProviderConfig: Record< }; // Icon URLs + GENERIC_PROVIDER_ICON come from the shared oauthIcons resolver. +// Every provider icon is decorative (alt=""): the button it sits in already +// names the provider, so alt text would only repeat that name. interface OAuthButtonsProps { onProviderClick: (provider: OAuthProvider) => void; @@ -116,7 +118,7 @@ export default function OAuthButtons({ > {p.label} @@ -142,7 +144,7 @@ export default function OAuthButtons({ > {p.label} @@ -169,7 +171,7 @@ export default function OAuthButtons({ {p.label} @@ -210,7 +212,7 @@ export default function OAuthButtons({ {p.label} diff --git a/frontend/editor/src/proprietary/billing/MeterBar.tsx b/frontend/editor/src/proprietary/billing/MeterBar.tsx index ee03d62885..5049b91294 100644 --- a/frontend/editor/src/proprietary/billing/MeterBar.tsx +++ b/frontend/editor/src/proprietary/billing/MeterBar.tsx @@ -23,6 +23,8 @@ interface MeterBarProps { meta?: ReactNode; /** Hide the fill bar (e.g. uncapped). Shown by default. */ showBar?: boolean; + /** Accessible name for the fill bar — what the meter measures ("Spend limit"). */ + barLabel: string; } /** @@ -40,6 +42,7 @@ export function MeterBar({ statusLabel, meta, showBar = true, + barLabel, }: MeterBarProps) { return (
    @@ -61,6 +64,7 @@ export function MeterBar({ aria-valuenow={Math.round(pct)} aria-valuemin={0} aria-valuemax={100} + aria-label={barLabel} >
    , because only Modal.Content lands + // props on the role="dialog" element — the modal draws its own heading, so + // the dialog needs an aria-label to have an accessible name. + -
    - - - - - - - - {t("workspace.people.changePassword.title", "Change password")} - - - {t( - "workspace.people.changePassword.subtitle", - "Update the password for", - )}{" "} - {user?.username} - - + + + +
    + + + + + + + + {t( + "workspace.people.changePassword.title", + "Change password", + )} + + + {t( + "workspace.people.changePassword.subtitle", + "Update the password for", + )}{" "} + {user?.username} + + - - - setForm({ - ...form, - newPassword: event.currentTarget.value, - generateRandom: false, - }) - } - disabled={processing || disabled || form.generateRandom} - data-autofocus - /> - - setForm({ - ...form, - confirmPassword: event.currentTarget.value, - generateRandom: false, - }) - } - disabled={processing || disabled || form.generateRandom} - error={ - !form.generateRandom && - form.confirmPassword && - form.newPassword !== form.confirmPassword - ? t( - "workspace.people.changePassword.passwordMismatch", - "Passwords do not match", - ) - : undefined - } - /> - - { - const checked = event.currentTarget.checked; - setForm((prev) => ({ ...prev, generateRandom: checked })); - if (event.currentTarget.checked) { - handleGeneratePassword(); + + + setForm({ + ...form, + newPassword: event.currentTarget.value, + generateRandom: false, + }) } - }} - /> - {passwordPreview && ( - + disabled={processing || disabled || form.generateRandom} + data-autofocus + /> + + setForm({ + ...form, + confirmPassword: event.currentTarget.value, + generateRandom: false, + }) + } + disabled={processing || disabled || form.generateRandom} + error={ + !form.generateRandom && + form.confirmPassword && + form.newPassword !== form.confirmPassword + ? t( + "workspace.people.changePassword.passwordMismatch", + "Passwords do not match", + ) + : undefined + } + /> + + { + const checked = event.currentTarget.checked; + setForm((prev) => ({ ...prev, generateRandom: checked })); + if (event.currentTarget.checked) { + handleGeneratePassword(); + } + }} + /> + {passwordPreview && ( + + + {t( + "workspace.people.changePassword.generatedPreview", + "Generated password:", + )}{" "} + {passwordPreview} + + + + + + + + )} + + + + + + setForm({ ...form, sendEmail: event.currentTarget.checked }) + } + disabled={!canEmail || processing} + /> + + setForm({ + ...form, + includePassword: event.currentTarget.checked, + }) + } + disabled={!canEmail || !form.sendEmail || processing} + /> + + setForm({ + ...form, + forcePasswordChange: event.currentTarget.checked, + }) + } + disabled={processing || disabled} + /> + {!canEmail && ( + + {mailEnabled + ? t( + "workspace.people.changePassword.emailUnavailable", + "This user's email is not a valid email address. Notifications are disabled.", + ) + : t( + "workspace.people.changePassword.smtpDisabled", + "Email notifications require SMTP to be enabled in settings.", + )} + + )} + {canEmail && !form.includePassword && form.sendEmail && ( {t( - "workspace.people.changePassword.generatedPreview", - "Generated password:", - )}{" "} - {passwordPreview} + "workspace.people.changePassword.notifyOnly", + "An email will be sent without the password, letting the user know an admin changed it.", + )} - - - - - - - )} - - - - - - setForm({ ...form, sendEmail: event.currentTarget.checked }) - } - disabled={!canEmail || processing} - /> - - setForm({ - ...form, - includePassword: event.currentTarget.checked, - }) - } - disabled={!canEmail || !form.sendEmail || processing} - /> - - setForm({ - ...form, - forcePasswordChange: event.currentTarget.checked, - }) - } - disabled={processing || disabled} - /> - {!canEmail && ( - - {mailEnabled - ? t( - "workspace.people.changePassword.emailUnavailable", - "This user's email is not a valid email address. Notifications are disabled.", - ) - : t( - "workspace.people.changePassword.smtpDisabled", - "Email notifications require SMTP to be enabled in settings.", - )} - - )} - {canEmail && !form.includePassword && form.sendEmail && ( - - {t( - "workspace.people.changePassword.notifyOnly", - "An email will be sent without the password, letting the user know an admin changed it.", )} - - )} - + - - -
    - + +
    +
    + + +
    ); } diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/AccountSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/AccountSection.tsx index 7212169e6e..4f132eb399 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/AccountSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/AccountSection.tsx @@ -604,7 +604,7 @@ const AccountSection: React.FC = () => { {t("account.mfa.manualKey", "Manual setup key")}:{" "} {mfaSetupData.secret} - + {t( "account.mfa.secretWarning", "Keep this key private. Anyone with access can generate valid authentication codes.", diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx index 2ad95525a1..f788551551 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -658,7 +658,7 @@ export default function AdminConnectionsSection() { href="https://docs.stirlingpdf.com/Functionality/Mobile-Scanner" target="_blank" size="xs" - c="blue" + c="var(--c-accent-text)" > {t( "admin.settings.connections.documentation", @@ -687,7 +687,7 @@ export default function AdminConnectionsSection() { "Allow users to upload files from mobile devices by scanning a QR code", )} - + {t( "admin.settings.connections.mobileScanner.note", "Note: Requires Frontend URL to be configured. ", @@ -698,7 +698,7 @@ export default function AdminConnectionsSection() { e.preventDefault(); navigate("/settings/adminGeneral#frontendUrl"); }} - c="orange" + c="var(--color-amber-dark)" td="underline" > {t( diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminMailSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminMailSection.tsx index c8df9eaea3..c62125be3f 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminMailSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminMailSection.tsx @@ -168,7 +168,7 @@ export default function AdminMailSection() { "Allow admins to invite users via email with auto-generated passwords", )} - + {t( "admin.settings.mail.frontendUrlNote.note", "Note: Requires Frontend URL to be configured. ", @@ -179,7 +179,7 @@ export default function AdminMailSection() { e.preventDefault(); navigate("/settings/adminGeneral#frontendUrl"); }} - c="orange" + c="var(--color-amber-dark)" td="underline" > {t( diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx index 6ab2a0fef8..dfaecac1fb 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx @@ -254,7 +254,7 @@ export default function AdminStorageSharingSection() { )} {!frontendUrlConfigured && ( - + {t( "admin.settings.storage.sharing.links.frontendUrlNote", "Requires a Frontend URL. ", @@ -265,7 +265,7 @@ export default function AdminStorageSharingSection() { e.preventDefault(); navigate("/settings/adminGeneral#frontendUrl"); }} - c="orange" + c="var(--color-amber-dark)" td="underline" > {t( @@ -317,7 +317,7 @@ export default function AdminStorageSharingSection() { )} {!mailEnabled && ( - + {t( "admin.settings.storage.sharing.email.mailNote", "Requires mail configuration. ", @@ -328,7 +328,7 @@ export default function AdminStorageSharingSection() { e.preventDefault(); navigate("/settings/adminConnections"); }} - c="orange" + c="var(--color-amber-dark)" td="underline" > {t( diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/LoginAgreementEditor.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/LoginAgreementEditor.tsx index 9af89dedad..23b879f974 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/LoginAgreementEditor.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/LoginAgreementEditor.tsx @@ -269,7 +269,7 @@ export default function LoginAgreementEditor({ {loading && } {loadFailed && !loading && ( - + {t( "admin.settings.legal.loginAgreement.loadError", "Failed to load the agreement for {{locale}}. Switch language and back to retry.", diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx index d3702e7f80..76ee65abe0 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx @@ -350,7 +350,7 @@ export default function TeamDetailsSection({ if (!team) { return ( - + {t("workspace.teams.teamNotFound", "Team not found")}
    {currency && onCurrencyChange && currencyOptions && ( 0} serverReachable={folders.serverReachable} selectedFileIds={selectedFileIds} activeWorkspaceFileIds={activeWorkspaceFileIdSet} @@ -1656,39 +1692,6 @@ export default function FileManagerView() { ); } -const SearchField = React.forwardRef< - HTMLInputElement, - { value: string; onChange: (v: string) => void } ->(function SearchField({ value, onChange }, ref) { - const { t } = useTranslation(); - return ( -
    - - onChange(e.currentTarget.value)} - placeholder={t( - "filesPage.searchPlaceholder", - "Search this folder & subfolders", - )} - aria-label={t("filesPage.search", "Search")} - /> - {value && ( - onChange("")} - aria-label={t("filesPage.clearSearch", "Clear search")} - > - × - - )} -
    - ); -}); - function Breadcrumbs() { const { t } = useTranslation(); const folders = useFolders(); diff --git a/frontend/editor/src/core/components/filesPage/FilesPage.css b/frontend/editor/src/core/components/filesPage/FilesPage.css index 368bae2521..aa9d8248aa 100644 --- a/frontend/editor/src/core/components/filesPage/FilesPage.css +++ b/frontend/editor/src/core/components/filesPage/FilesPage.css @@ -34,6 +34,33 @@ gap: 0.5rem; } +.files-page-header-search { + display: flex; + align-items: center; + justify-content: center; + min-width: 0; +} + +.files-page-header-search .super-search { + flex: 0 1 24rem; + width: min(100%, 24rem); + max-width: 24rem; +} + +.files-page-header-search .super-search input { + background-color: transparent; + padding-top: 4px; + padding-bottom: 4px; + font-size: 12.5px; +} + +[data-mantine-color-scheme="dark"] + .files-page-header-search + .super-search + input { + background-color: transparent; +} + .files-page-breadcrumbs { display: flex; align-items: center; @@ -79,28 +106,6 @@ flex-shrink: 0; } -.files-page-search { - display: flex; - align-items: center; - gap: 0.35rem; - background: var(--c-surface); - border: 1px solid var(--c-border-subtle); - border-radius: 999px; - padding: 0.2rem 0.75rem; - /* Fills its grid cell; the cell's minmax(...) clamps to a sensible range. */ - width: 100%; - min-width: 0; -} - -.files-page-search input { - background: transparent; - border: none; - outline: none; - flex: 1; - color: var(--c-text); - font-size: 0.9rem; -} - .files-page-body { display: flex; flex: 1 1 auto; @@ -1376,23 +1381,6 @@ overflow-x: auto; min-width: 0; } - .files-page-search { - /* Shrink hard so the search bar doesn't eat the whole action row. - Users still see the icon + a few chars of the placeholder. - `overflow: hidden` clips the input's natural intrinsic width so - placeholder text never leaks outside the rounded pill. */ - min-width: 0; - flex: 0 1 5.5rem; - max-width: 6.5rem; - overflow: hidden; - } - .files-page-search input { - /* `min-width: 0` lets flex actually shrink the input below its - default ~20-char intrinsic size - without this, the placeholder - extends beyond the parent's clip box and bleeds onto neighbours. */ - min-width: 0; - text-overflow: ellipsis; - } /* Upload becomes an icon-only square button on mobile so the action row stops getting clipped. Scoped to `.files-page-header-actions` so the Back button at the header level keeps its visible "Back" diff --git a/frontend/editor/src/core/components/layout/Workbench.module.css b/frontend/editor/src/core/components/layout/Workbench.module.css index 51138d2ac3..fb73be8655 100644 --- a/frontend/editor/src/core/components/layout/Workbench.module.css +++ b/frontend/editor/src/core/components/layout/Workbench.module.css @@ -1,19 +1,48 @@ -/* WorkbenchBar slide-in/out animation using CSS grid trick */ +/* Positioning context for the viewer toolbar's reopen tab, which hangs below + the bar and so must sit outside the overflow-clipped wrapper below. */ +.workbenchBarShell { + position: relative; + flex-shrink: 0; + z-index: 51; +} + +/* Little pull-tab shown while the viewer tool row is retracted. Anchored to the + bar's bottom-right edge (where the retract handle sat). Small by design - it + just brings the row back. */ +.workbenchBarReopenTab { + position: absolute; + top: 100%; + /* Right-align with the retract handle inside the bar: the bar's right + margin (--nav-gutter) + 1px border + 8px bar padding + the handle's own + 6px inset. */ + right: calc(var(--nav-gutter) + 15px); + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 0.875rem; + border: 1px solid var(--c-border-subtle); + border-top: none; + border-radius: 0 0 7px 7px; + background: var(--c-bg-raised); + color: var(--c-text-subtle); + cursor: pointer; + z-index: 52; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +.workbenchBarReopenTab:hover { + color: var(--c-text); + background: var(--c-hover); +} + .workbenchBarWrapper { display: grid; grid-template-rows: 1fr; - transition: grid-template-rows 280ms ease; } -.workbenchBarWrapper[data-hidden="true"] { - grid-template-rows: 0fr; -} - -.workbenchBarWrapper[data-no-transition="true"] { - transition: none; -} - -/* Direct child must have min-height: 0 so the row can collapse below content size */ .workbenchBarInner { min-height: 0; overflow: hidden; diff --git a/frontend/editor/src/core/components/layout/Workbench.tsx b/frontend/editor/src/core/components/layout/Workbench.tsx index 96eb6b8d6f..9f54e224e8 100644 --- a/frontend/editor/src/core/components/layout/Workbench.tsx +++ b/frontend/editor/src/core/components/layout/Workbench.tsx @@ -1,5 +1,8 @@ -import { useEffect, useState, Suspense, lazy } from "react"; +import { useState, Suspense, lazy } from "react"; +import { useTranslation } from "react-i18next"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { Box, Loader, Center } from "@mantine/core"; +import { Button } from "@app/ui/Button"; import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; import { useFileHandler } from "@app/hooks/useFileHandler"; import { useAllFiles } from "@app/contexts/FileContext"; @@ -66,18 +69,13 @@ export default function Workbench() { const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null; const { addFiles } = useFileHandler(); const hasFiles = activeFiles.length > 0; - // Custom workbench views (e.g. Watched Folders) manage their own content and may - // have no workbench files, but still need the bar's view switcher so users can - // navigate back out. - const isCustomViewActive = !isBaseWorkbench(currentView); + const { t } = useTranslation(); - // Enable bar transitions after first paint so the initial hidden state shows - // without animating (landing page on load shouldn't animate the bar up). - const [barTransitionEnabled, setBarTransitionEnabled] = useState(false); - useEffect(() => { - const raf = requestAnimationFrame(() => setBarTransitionEnabled(true)); - return () => cancelAnimationFrame(raf); - }, []); + // The viewer's tool row can be retracted to give the document more height. + // State lives here (not in WorkbenchBar) so the reopen tab can hang below the + // bar, outside the bar's overflow-clipped wrapper. Scoped to the viewer. + const [viewerToolbarCollapsed, setViewerToolbarCollapsed] = useState(false); + const showReopenTab = currentView === "viewer" && viewerToolbarCollapsed; const handlePreviewClose = () => { setPreviewFile(null); @@ -219,22 +217,39 @@ export default function Workbench() { data-tour="workbench" style={{ backgroundColor: "var(--c-bg)", minWidth: 0 }} > - {/* Workbench Bar - animates in/out based on file presence */} + {/* Workbench Bar — always visible outside My Files (it hosts the + global search), even with no files loaded. */} {currentView !== "myFiles" && !customWorkbenchViews.find((v) => v.workbenchId === currentView) ?.hideTopControls && ( -
    -
    - +
    +
    +
    + +
    + {/* Reopen tab: a little handle hanging off the bar's bottom-right + while the viewer tool row is retracted. */} + {showReopenTab && ( +
    )} diff --git a/frontend/editor/src/core/components/shared/AppConfigModal.css b/frontend/editor/src/core/components/shared/AppConfigModal.css index 46033eaf97..2953632d98 100644 --- a/frontend/editor/src/core/components/shared/AppConfigModal.css +++ b/frontend/editor/src/core/components/shared/AppConfigModal.css @@ -1,4 +1,28 @@ /* AppConfigModal styles */ + +/* Deep-link highlight: pulses the control the super search jumped to + (navigated via /settings/{section}?focus={anchor}). */ +@keyframes settings-focus-pulse { + 0% { + box-shadow: 0 0 0 3px var(--mantine-color-blue-5); + background: color-mix( + in srgb, + var(--mantine-color-blue-5) 16%, + transparent + ); + } + 100% { + box-shadow: 0 0 0 6px transparent; + background: transparent; + } +} + +.settings-focus-target { + animation: settings-focus-pulse 1.8s ease-out; + border-radius: 8px; + scroll-margin: 1rem; +} + .modal-container { display: flex; gap: 0; @@ -173,16 +197,6 @@ padding-top: 1rem; } -.settings-search-select { - min-width: 10rem; -} - -.settings-search-option { - display: flex; - flex-direction: column; - gap: 0.125rem; -} - .confirm-modal-content { display: flex; flex-direction: column; diff --git a/frontend/editor/src/core/components/shared/AppConfigModal.tsx b/frontend/editor/src/core/components/shared/AppConfigModal.tsx index b5b3224985..12faf6eea5 100644 --- a/frontend/editor/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/editor/src/core/components/shared/AppConfigModal.tsx @@ -29,7 +29,6 @@ import { UnsavedChangesProvider, useUnsavedChanges, } from "@app/contexts/UnsavedChangesContext"; -import { SettingsSearchBar } from "@app/components/shared/config/SettingsSearchBar"; import { stripBasePath, withBasePath } from "@app/constants/app"; import { EDITOR_BASENAME } from "@app/routes/editorBasename"; @@ -46,6 +45,8 @@ interface AppConfigModalProps { /** Section to land on when opening. Only honoured when urlSync is off (URL * deep links win otherwise). */ initialSection?: NavKey | null; + /** Row anchor to focus when opening on a non-URL host. */ + initialFocus?: string | null; /** Host-specific sections appended after the build's registry sections. */ extraSections?: ConfigNavSection[]; /** Registry section keys to drop, for hosts a section can't run in. */ @@ -67,6 +68,7 @@ const AppConfigModalInner: React.FC = ({ onClose, urlSync = true, initialSection, + initialFocus, extraSections, hiddenSectionKeys, }) => { @@ -150,6 +152,35 @@ const AppConfigModalInner: React.FC = ({ [navigate, urlSync], ); + // Deep-link: /settings/{section}?focus={anchor} scrolls to and briefly + // highlights the matching control (used by the global super search to jump + // straight to an individual setting row). + useEffect(() => { + if (!opened) return; + const focus = urlSync + ? new URLSearchParams(location.search).get("focus") + : initialFocus; + if (!focus) return; + let raf = 0; + // Wait for the (possibly just-switched) section to render before scrolling. + const timer = window.setTimeout(() => { + raf = window.requestAnimationFrame(() => { + const el = document.getElementById(focus); + if (!el) return; + el.scrollIntoView({ behavior: "smooth", block: "center" }); + el.classList.add("settings-focus-target"); + window.setTimeout( + () => el.classList.remove("settings-focus-target"), + 1800, + ); + }); + }, 150); + return () => { + window.clearTimeout(timer); + if (raf) window.cancelAnimationFrame(raf); + }; + }, [opened, active, initialFocus, location.search, urlSync]); + // Backwards-compat: external `appConfig:navigate` events route through the // same switchSection path so they get the no-flash treatment too. useEffect(() => { @@ -185,9 +216,10 @@ const AppConfigModalInner: React.FC = ({ const runningEE = config?.runningEE ?? false; const loginEnabled = config?.enableLogin ?? false; + /** Resolves false when a dirty-state confirm kept the modal open. */ const handleClose = useCallback(async () => { const canProceed = await confirmIfDirty(); - if (!canProceed) return; + if (!canProceed) return false; // Only unwind history if settings was opened via the URL; opened via state // there's no /settings entry to pop and navigate(-1) would jump to /files. @@ -200,6 +232,7 @@ const AppConfigModalInner: React.FC = ({ } } onClose(); + return true; }, [ confirmIfDirty, location.key, @@ -214,6 +247,24 @@ const AppConfigModalInner: React.FC = ({ void handleClose(); }, [handleClose]); + // Cmd/Ctrl+K: hand over to the global super search. The bar's own shortcut + // is inert while a dialog traps focus, so the modal closes itself (through + // the same dirty-check as any other close) and asks the bar to take focus. + // Settings results deep-link straight back into this modal. + useEffect(() => { + if (!opened) return; + const onKey = (e: KeyboardEvent) => { + const combo = (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey; + if (!combo || e.code !== "KeyK") return; + e.preventDefault(); + void handleClose().then((closed) => { + if (closed) window.dispatchEvent(new Event("superSearch:focus")); + }); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [opened, handleClose]); + // Left navigation structure and icons const registrySections = useConfigNavSections( isAdmin, @@ -413,11 +464,6 @@ const AppConfigModalInner: React.FC = ({ {activeLabel} - diff --git a/frontend/editor/src/core/components/shared/FileSidebar.css b/frontend/editor/src/core/components/shared/FileSidebar.css index 126ec6fede..590f59fa7d 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.css +++ b/frontend/editor/src/core/components/shared/FileSidebar.css @@ -95,7 +95,6 @@ } /* Centre each row's icon in the narrow rail (no side padding/margin to shove it off the edge). */ -.file-sidebar[data-collapsed="true"] .file-sidebar-search-row, .file-sidebar[data-collapsed="true"] .file-sidebar-action-row, .file-sidebar[data-collapsed="true"] .file-sidebar-cloud-row { justify-content: center; @@ -139,55 +138,6 @@ color: var(--c-accent-text); } -/* ---- Search row ---- */ -.file-sidebar-search-row { - display: flex; - align-items: center; - min-height: 32px; - padding: 0 8px; - gap: 0; - cursor: pointer; - border-radius: 4px; - margin: 0; - flex-shrink: 0; - transition: background-color 0.15s ease; -} - -.file-sidebar-search-row:not(.active):hover { - background-color: var(--c-hover); -} - -.file-sidebar-search-icon { - color: var(--c-text-subtle) !important; - font-size: 18px !important; - flex-shrink: 0; -} - -.file-sidebar-search-close { - cursor: pointer; -} - -.file-sidebar-search-input { - flex: 1; - background: transparent; - border: none; - outline: none; - font-size: 14px; - color: var(--c-text); - margin-left: 12px; - min-width: 0; -} - -.file-sidebar-search-input::placeholder { - color: var(--c-text-subtle); -} - -.file-sidebar-search-label { - margin-left: 12px; - font-size: 14px; - color: var(--c-text); -} - /* ---- Scrollable content ---- */ /* This is a flex column - action rows are fixed, only the file list scrolls */ .file-sidebar-scroll { diff --git a/frontend/editor/src/core/components/shared/FileSidebar.tsx b/frontend/editor/src/core/components/shared/FileSidebar.tsx index 742334015c..f74e96220f 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebar.tsx @@ -33,11 +33,9 @@ import { GoogleDriveIcon } from "@app/components/shared/CloudStorageIcons"; import { AppSwitcher } from "@app/components/shared/AppSwitcher"; import { SidebarToggleIcon } from "@app/components/shared/SidebarToggleIcon"; import type { StirlingFileStub } from "@app/types/fileContext"; -import SearchIcon from "@mui/icons-material/Search"; import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import FolderSpecialIcon from "@mui/icons-material/FolderSpecial"; import UploadFileIcon from "@mui/icons-material/UploadFile"; -import CloseIcon from "@mui/icons-material/Close"; import AddIcon from "@mui/icons-material/Add"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import SettingsIcon from "@mui/icons-material/Settings"; @@ -102,8 +100,6 @@ export interface FileSidebarProps { onUploadFiles?: (files: File[]) => void | Promise; /** Override the Google Drive handler. */ onPickGoogleDriveFiles?: (files: File[]) => void | Promise; - /** Override the Search row click (e.g. focus the /files search input). */ - onSearchClick?: () => void; /** Extra action row inserted under Open-from-computer (e.g. New folder). */ extraAction?: { icon: React.ReactNode; @@ -155,7 +151,6 @@ const FileSidebar = forwardRef( onOpenSettings, onUploadFiles, onPickGoogleDriveFiles, - onSearchClick, extraAction, toggleAriaLabel, toggleIcon, @@ -168,9 +163,6 @@ const FileSidebar = forwardRef( // Classification off (non-SaaS / AI-off) → never show the per-row label chip, // even if a stub carries labels from an imported PDF; keeps the row plain. const classificationEnabled = useClassificationEnabled(); - const [searchActive, setSearchActive] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const searchInputRef = useRef(null); const nativeFileInputRef = useRef(null); // State (not ref) so setting it triggers a re-render - avoids racing addFiles state updates. const [pendingViewFileId, setPendingViewFileId] = useState( @@ -439,17 +431,8 @@ const FileSidebar = forwardRef( } }, [pendingViewFileId, state.files.ids, setActiveFileId, navActions]); - // Memoized so an unrelated re-render (e.g. a policy-run store tick) keeps a - // stable array identity — avoids re-running the grouping memo + backfill effect. - const filteredFileStubs = useMemo(() => { - const q = searchQuery.trim().toLowerCase(); - return q - ? allFileStubs.filter((stub) => stub.name.toLowerCase().includes(q)) - : allFileStubs; - }, [allFileStubs, searchQuery]); - // SaaS groups by classification label; core returns null → one flat, recency-sorted list. - const fileGroups = useFileSidebarGroups(filteredFileStubs); + const fileGroups = useFileSidebarGroups(allFileStubs); // Workbench membership as a Set for O(1) per-row lookups (see renderFileRow). const workbenchIds = useMemo( () => new Set(state.files.ids.map((id) => id as string)), @@ -459,12 +442,12 @@ const FileSidebar = forwardRef( // must key by their unique leaf id rather than the shared lineage (see renderFileRow). const lineageCounts = useMemo(() => { const counts = new Map(); - for (const s of filteredFileStubs) { + for (const s of allFileStubs) { const k = (s.originalFileId ?? s.id) as string; counts.set(k, (counts.get(k) ?? 0) + 1); } return counts; - }, [filteredFileStubs]); + }, [allFileStubs]); // Per-group expand/collapse, falling back to each group's default until toggled. const [groupOpen, setGroupOpen] = useState>({}); const setGroupOpenState = useCallback( @@ -473,29 +456,6 @@ const FileSidebar = forwardRef( [], ); - // Handle search activation - const handleSearchClick = useCallback(() => { - if (onSearchClick) { - onSearchClick(); - return; - } - if (collapsed && onToggleCollapse) { - onToggleCollapse(); - } - setSearchActive(true); - }, [collapsed, onToggleCollapse, onSearchClick]); - - const handleSearchClose = useCallback(() => { - setSearchActive(false); - setSearchQuery(""); - }, []); - - useEffect(() => { - if (searchActive && searchInputRef.current) { - searchInputRef.current.focus(); - } - }, [searchActive]); - // Handle Google Drive const handleGoogleDriveClick = useCallback(async () => { if (!isGoogleDriveEnabled) return; @@ -854,58 +814,9 @@ const FileSidebar = forwardRef( )}
    - {/* Box 1 — top controls (search + open / my files / cloud). No title. */} + {/* Box 1 — top controls (open / my files / cloud). No title. File + search lives in the global super search (top bar), not here. */} - {/* Search row */} - -
    e.key === "Enter" && handleSearchClick() - : undefined - } - > - {searchActive && !collapsed ? ( - { - e.stopPropagation(); - handleSearchClose(); - }} - /> - ) : ( - - )} - {!collapsed && - (searchActive ? ( - setSearchQuery(e.target.value)} - placeholder={t( - "fileSidebar.searchPlaceholder", - "Search files...", - )} - onClick={(e) => e.stopPropagation()} - /> - ) : ( - - {t("fileSidebar.search", "Search")} - - ))} -
    -
    - {/* Hidden native file input - kept outside the !collapsed gate so the "Open from computer" row below (always rendered) can fire it in either sidebar state without a silent no-op. */} @@ -1138,7 +1049,7 @@ const FileSidebar = forwardRef( {t("fileSidebar.library", "PDF Library")} - + (
    - ) : filteredFileStubs.length > 0 ? ( + ) : allFileStubs.length > 0 ? (
    {fileGroups ? ( <> @@ -1250,29 +1161,24 @@ const FileSidebar = forwardRef( "fileSidebar.viewAll", "View all {{count}} files", { - count: filteredFileStubs.length, + count: allFileStubs.length, }, )} ) : ( - filteredFileStubs.map(renderFileRow) + allFileStubs.map(renderFileRow) )}
    ) : ( - !searchActive && ( -
    -

    - {t("fileSidebar.noFiles", "No files yet")} -

    -

    - {t( - "fileSidebar.dropHint", - "Open files to get started", - )} -

    -
    - ) +
    +

    + {t("fileSidebar.noFiles", "No files yet")} +

    +

    + {t("fileSidebar.dropHint", "Open files to get started")} +

    +
    )}
    )} diff --git a/frontend/editor/src/core/components/shared/TextInput.tsx b/frontend/editor/src/core/components/shared/TextInput.tsx index 06147fb99b..7d927bb011 100644 --- a/frontend/editor/src/core/components/shared/TextInput.tsx +++ b/frontend/editor/src/core/components/shared/TextInput.tsx @@ -36,6 +36,13 @@ export interface TextInputProps { readOnly?: boolean; /** Accessibility label */ "aria-label"?: string; + /** ARIA role override (e.g. "combobox" for inputs driving a listbox). */ + role?: React.AriaRole; + /** Combobox wiring — forwarded to the native input. */ + "aria-expanded"?: boolean; + "aria-controls"?: string; + "aria-activedescendant"?: string; + "aria-autocomplete"?: React.AriaAttributes["aria-autocomplete"]; /** Focus event handler */ onFocus?: () => void; /** Allow the icon to receive pointer events (e.g. when icon is a clickable button) */ diff --git a/frontend/editor/src/core/components/shared/WorkbenchBar.css b/frontend/editor/src/core/components/shared/WorkbenchBar.css index b324fd8222..f805f69721 100644 --- a/frontend/editor/src/core/components/shared/WorkbenchBar.css +++ b/frontend/editor/src/core/components/shared/WorkbenchBar.css @@ -2,11 +2,17 @@ /* Horizontal toolbar at the top of the workbench area. */ .workbench-bar { + /* Flex-wrap layout that reflows by content width (measured in JS, which sets + data-wrapped). Roomy: row 1 is [views | search | globals] and the tool + icons sit on their own full-width row below. Crowded (data-wrapped): the + search bumps to its own row, leaving [views | globals] on row 1 and the + tools on row 3 — three rows total. `order` drives the reflow. */ display: flex; flex-wrap: wrap; align-items: center; align-content: flex-start; - min-height: 40px; + column-gap: 8px; + min-height: 38px; padding: 0 8px; /* No left margin: the file sidebar's own 0.5rem padding already provides the gutter on that side, so adding one here would double it and leave the bar @@ -21,12 +27,16 @@ /* ---- View switcher (left) ---- */ .workbench-bar-views { + order: 1; + flex-shrink: 0; display: flex; align-items: center; gap: 2px; - flex-shrink: 0; - order: 1; - height: 40px; + /* min-width:0 lets it shrink instead of forcing a wrap; overflow clips labels + on very narrow viewports. */ + min-width: 0; + overflow: hidden; + height: 38px; z-index: 0; } @@ -34,7 +44,7 @@ display: inline-flex; align-items: center; gap: 6px; - padding: 4px 10px; + padding: 2px 8px; border: none; border-radius: 8px; background: transparent; @@ -86,27 +96,47 @@ height: 16px; } -/* ---- Center: tool buttons ---- */ - -/* Single-row: center sits between views and globals */ -.workbench-bar-center { +/* ---- Super search ---- */ +/* Roomy: grows to fill the centre between views and globals. */ +.workbench-bar-search { order: 2; flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: center; - gap: 2px; - height: 40px; - overflow: hidden; + height: 38px; + transform: translateX(var(--workbench-bar-search-offset, 0px)); } -/* Two-row: center drops below views+globals */ -.workbench-bar[data-wrapped="true"] .workbench-bar-center { +.workbench-bar-search .super-search { + flex: 0 1 24rem; + width: min(100%, 24rem); + max-width: 24rem; +} + +.workbench-bar-search .super-search input { + background-color: transparent; + padding-top: 4px; + padding-bottom: 4px; + font-size: 12.5px; +} + +[data-mantine-color-scheme="dark"] .workbench-bar-search .super-search input { + background-color: transparent; +} + +/* Crowded: drops to its own full-width row between the top row and the tools. */ +.workbench-bar[data-wrapped="true"] .workbench-bar-search { order: 3; flex: 0 0 100%; height: auto; padding: 4px 0; + transform: none; +} + +/* Two-row: the tool row scrolls sideways instead of wrapping. */ +.workbench-bar[data-wrapped="true"] .workbench-bar-center { border-top: 1px solid var(--c-border-subtle); justify-content: flex-start; overflow-x: auto; @@ -118,21 +148,68 @@ flex-shrink: 0; } -/* ---- Right: global buttons (theme / language / download) ---- */ +/* ---- Tool buttons (own full-width row, always below the top row) ---- */ +.workbench-bar-center { + order: 4; + flex: 0 0 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 2px; + /* Symmetric side padding leaves room for the retract handle pinned right + without knocking the centred tool icons off-centre. */ + padding: 4px 36px; + border-top: 1px solid var(--c-border-subtle); +} + +/* Retract / reopen handle for the viewer tool row. */ +.workbench-bar-toolbar-handle { + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 6px; + background: transparent; + color: var(--c-text-subtle); + cursor: pointer; + padding: 0; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +.workbench-bar-toolbar-handle:hover { + color: var(--c-text); + background: var(--c-hover); +} + +/* Pinned to the right edge of the tool row. */ +.workbench-bar-toolbar-handle-retract { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + width: 28px; + height: 28px; +} + +/* ---- Right: global buttons (print / download / save / close), row 1 ---- */ .workbench-bar-globals { + order: 3; + flex-shrink: 0; display: flex; align-items: center; gap: 2px; - flex-shrink: 0; - order: 3; - height: 40px; + min-width: 0; + height: 38px; margin-left: auto; } -/* In two-row mode globals moves to row 1 right side */ +/* Crowded: globals stays on row 1, moving up beside the views. */ .workbench-bar[data-wrapped="true"] .workbench-bar-globals { order: 2; - margin-left: auto; } /* Shared action icon style - applies to both center and global buttons. diff --git a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx index 55ca01d307..241d658bad 100644 --- a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx +++ b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx @@ -1,6 +1,6 @@ import React, { useCallback, - useEffect, + useLayoutEffect, useMemo, useRef, useSyncExternalStore, @@ -33,6 +33,8 @@ import { ViewerContext, useViewer } from "@app/contexts/ViewerContext"; import { WorkbenchType, isBaseWorkbench } from "@app/types/workbench"; import { Tooltip } from "@app/components/shared/Tooltip"; import LocalIcon from "@app/components/shared/LocalIcon"; +import SuperSearch from "@app/components/shared/superSearch/SuperSearch"; +import { useEditorSearchScopes } from "@app/hooks/useSuperSearch"; import ViewerShareButton from "@app/components/viewer/ViewerShareButton"; import { useSharingEnabled } from "@app/hooks/useSharingEnabled"; import { usePolicyFileBadges } from "@app/hooks/usePolicyFileBadges"; @@ -53,6 +55,7 @@ import InsertDriveFileOutlinedIcon from "@mui/icons-material/InsertDriveFileOutl import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; import CloseIcon from "@mui/icons-material/Close"; import PrintIcon from "@mui/icons-material/Print"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; import "@app/components/shared/WorkbenchBar.css"; @@ -68,6 +71,10 @@ interface WorkbenchBarProps { currentView: WorkbenchType; setCurrentView: (view: WorkbenchType) => void; hasFiles: boolean; + /** Whether the viewer's tool row is currently retracted. */ + viewerToolbarCollapsed?: boolean; + /** Setter for the viewer tool-row retract state (owned by Workbench). */ + onCollapseViewerToolbar?: (collapsed: boolean) => void; } function renderWithTooltip( @@ -92,9 +99,12 @@ export default function WorkbenchBar({ currentView, setCurrentView, hasFiles, + viewerToolbarCollapsed = false, + onCollapseViewerToolbar, }: WorkbenchBarProps) { const { t } = useTranslation(); const navigate = useNavigate(); + const searchScopes = useEditorSearchScopes(); const returnRoute = useSyncExternalStore( subscribeFilesPageReturnRoute, getFilesPageReturnRoute, @@ -115,6 +125,7 @@ export default function WorkbenchBar({ } = useToolWorkflow(); const { selectedTool } = useNavigationState(); const isCustomView = !isBaseWorkbench(currentView); + const isViewer = currentView === "viewer"; const disableForFullscreen = toolPanelMode === "fullscreen" && leftPanelView === "toolPicker"; const terminology = useFileActionTerminology(); @@ -435,34 +446,47 @@ export default function WorkbenchBar({ })), ]; + // Reflow the top row by content width: when the views + globals leave too + // little room for a usable search, bump the search to its own row const barRef = useRef(null); - - useEffect(() => { + useLayoutEffect(() => { const bar = barRef.current; if (!bar) return; - + const MIN_SEARCH_WIDTH = 320; const measure = () => { const viewsEl = bar.querySelector(".workbench-bar-views"); const globalsEl = bar.querySelector( ".workbench-bar-globals", ); - const centerEl = bar.querySelector(".workbench-bar-center"); - const viewsWidth = viewsEl?.offsetWidth ?? 0; const globalsWidth = globalsEl?.offsetWidth ?? 0; - const centerChildren = centerEl - ? (Array.from(centerEl.children) as HTMLElement[]) - : []; - const centerWidth = - centerChildren.reduce((sum, el) => sum + el.offsetWidth, 0) + - Math.max(0, centerChildren.length - 1) * 2; // gap: 2px - - const needed = viewsWidth + centerWidth + globalsWidth + 24; // 24px bar padding - bar.dataset.wrapped = String(needed > bar.clientWidth); + // clientWidth minus the two side clusters, the bar's 16px h-padding and + // the two 8px column gaps flanking the search. + const available = bar.clientWidth - viewsWidth - globalsWidth - 16 - 16; + const wrapped = available < MIN_SEARCH_WIDTH; + bar.dataset.wrapped = String(wrapped); + // Centre the search on the bar rather than its slot — clamped to the + // slot's spare width, because the shift is a transform (no layout) and + // an unclamped value would paint the pill over the adjacent cluster. + const slotEl = bar.querySelector(".workbench-bar-search"); + const pillEl = slotEl?.querySelector(".super-search"); + const slack = Math.max( + 0, + ((slotEl?.offsetWidth ?? 0) - (pillEl?.offsetWidth ?? 0)) / 2, + ); + const centred = (globalsWidth - viewsWidth) / 2; + const offset = Math.min(slack, Math.max(-slack, centred)); + bar.style.setProperty( + "--workbench-bar-search-offset", + wrapped ? "0px" : `${offset}px`, + ); }; - const ro = new ResizeObserver(measure); ro.observe(bar); + const viewsEl = bar.querySelector(".workbench-bar-views"); + const globalsEl = bar.querySelector(".workbench-bar-globals"); + if (viewsEl) ro.observe(viewsEl); + if (globalsEl) ro.observe(globalsEl); measure(); return () => ro.disconnect(); }, []); @@ -471,7 +495,7 @@ export default function WorkbenchBar({
    {/* Left: optional "Back to My Files" + view switcher */} @@ -524,27 +548,50 @@ export default function WorkbenchBar({ )}
    - {/* Tool buttons - second row, only rendered when buttons exist */} - {sectionsWithButtons.length > 0 && ( -
    - {sectionsWithButtons.map( - ({ section, buttons: sectionButtons }, idx) => ( - - {idx > 0 &&
    } - {sectionButtons.map((btn) => { - const content = renderButton(btn); - if (!content) return null; - return ( -
    - {content} -
    - ); - })} - - ), - )} -
    - )} + {/* Global super search - always present, even on the homepage */} +
    + +
    + + {/* Tool buttons - second row, only rendered when buttons exist. In the + viewer the row is retractable: a handle on its right edge hides the + whole row; Workbench then shows a tab below the bar to bring it back. */} + {sectionsWithButtons.length > 0 && + !(isViewer && viewerToolbarCollapsed) && ( +
    + {sectionsWithButtons.map( + ({ section, buttons: sectionButtons }, idx) => ( + + {idx > 0 &&
    } + {sectionButtons.map((btn) => { + const content = renderButton(btn); + if (!content) return null; + return ( +
    + {content} +
    + ); + })} + + ), + )} + {isViewer && onCollapseViewerToolbar && ( +
    + )} {/* Right: Global buttons - export group left, close anchored right */}
    diff --git a/frontend/editor/src/core/components/shared/config/SettingsSearchBar.stories.tsx b/frontend/editor/src/core/components/shared/config/SettingsSearchBar.stories.tsx deleted file mode 100644 index 28d5a1c3e0..0000000000 --- a/frontend/editor/src/core/components/shared/config/SettingsSearchBar.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { SettingsSearchBar } from "@app/components/shared/config/SettingsSearchBar"; -import type { ConfigNavSection } from "@app/components/shared/config/configNavSections"; - -const mockConfigNavSections: ConfigNavSection[] = [ - { - title: "Preferences", - items: [ - { - key: "general", - label: "General", - icon: "settings-rounded", - component: null, - }, - { - key: "hotkeys", - label: "Keyboard Shortcuts", - icon: "keyboard-rounded", - component: null, - }, - ], - }, - { - title: "Workspace", - items: [ - { - key: "people", - label: "People", - icon: "group-rounded", - component: null, - }, - { - key: "teams", - label: "Teams", - icon: "groups-rounded", - component: null, - disabled: true, - }, - ], - }, -]; - -const meta = { - title: "Shared/Config/SettingsSearchBar", - component: SettingsSearchBar, - parameters: { layout: "padded" }, - args: { - configNavSections: mockConfigNavSections, - onNavigate: async () => {}, - isMobile: false, - }, -} satisfies Meta; -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; - -export const Mobile: Story = { - args: { - isMobile: true, - }, -}; diff --git a/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx b/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx deleted file mode 100644 index 397347dcf6..0000000000 --- a/frontend/editor/src/core/components/shared/config/SettingsSearchBar.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { useMemo, useState, useCallback } from "react"; -import { Select, Text } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import LocalIcon from "@app/components/shared/LocalIcon"; -import { NavKey, VALID_NAV_KEYS } from "@app/components/shared/config/types"; -import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; -import type { - ConfigNavSection, - ConfigNavItem, -} from "@app/components/shared/config/configNavSections"; - -interface SettingsSearchBarProps { - configNavSections: ConfigNavSection[]; - onNavigate: (key: NavKey) => Promise; - isMobile: boolean; -} - -interface SettingsSearchOption { - value: NavKey; - label: string; - sectionTitle: string; - destinationPath: string; - searchableContent: string[]; - matchedContext?: string; -} - -const SETTINGS_SEARCH_TRANSLATION_PREFIXES: Partial> = - { - general: ["settings.general"], - hotkeys: ["settings.hotkeys"], - account: ["account"], - people: ["settings.workspace"], - teams: ["settings.workspace", "settings.team"], - "api-keys": ["settings.developer"], - connectionMode: ["settings.connection"], - planBilling: ["settings.planBilling"], - adminGeneral: ["admin.settings.general"], - adminFeatures: ["admin.settings.features"], - adminEndpoints: ["admin.settings.endpoints"], - adminDatabase: ["admin.settings.database"], - adminAdvanced: ["admin.settings.advanced"], - adminSecurity: ["admin.settings.security"], - adminMcp: ["admin.settings.mcp"], - adminConnections: [ - "admin.settings.connections", - "admin.settings.mail", - "admin.settings.security", - "admin.settings.telegram", - "admin.settings.premium", - "admin.settings.general", - "settings.securityAuth", - "settings.connection", - ], - adminPlan: [ - "settings.planBilling", - "admin.settings.premium", - "settings.licensingAnalytics", - ], - adminAudit: ["settings.licensingAnalytics"], - adminUsage: ["settings.licensingAnalytics"], - adminLegal: ["admin.settings.legal"], - adminPrivacy: ["admin.settings.privacy"], - }; - -const getTranslationPrefixesForNavKey = (key: string): string[] => { - const explicitPrefixes = SETTINGS_SEARCH_TRANSLATION_PREFIXES[key] ?? []; - - const inferredPrefixes: string[] = []; - - if (key.startsWith("admin")) { - const adminSuffix = key.replace(/^admin/, ""); - const normalizedAdminSuffix = - adminSuffix.charAt(0).toLowerCase() + adminSuffix.slice(1); - inferredPrefixes.push(`admin.settings.${normalizedAdminSuffix}`); - } else { - inferredPrefixes.push(`settings.${key}`); - } - - return Array.from(new Set([...explicitPrefixes, ...inferredPrefixes])); -}; - -const flattenTranslationStrings = (value: unknown): string[] => { - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed ? [trimmed] : []; - } - - if (Array.isArray(value)) { - return value.flatMap(flattenTranslationStrings); - } - - if (value && typeof value === "object") { - return Object.values(value as Record).flatMap( - flattenTranslationStrings, - ); - } - - return []; -}; - -const buildMatchSnippet = (text: string, query: string): string => { - const normalizedText = text.toLocaleLowerCase(); - const normalizedQuery = query.toLocaleLowerCase(); - const matchIndex = normalizedText.indexOf(normalizedQuery); - - if (matchIndex === -1) { - return text; - } - - const maxLength = 84; - const contextPadding = 28; - const start = Math.max(0, matchIndex - contextPadding); - const end = Math.min(text.length, matchIndex + query.length + contextPadding); - const snippet = text.slice(start, end); - - if (snippet.length <= maxLength) { - return `${start > 0 ? "…" : ""}${snippet}${end < text.length ? "…" : ""}`; - } - - return `${start > 0 ? "…" : ""}${snippet.slice(0, maxLength)}${end < text.length ? "…" : ""}`; -}; - -export const SettingsSearchBar: React.FC = ({ - configNavSections, - onNavigate, - isMobile, -}) => { - const { t } = useTranslation(); - const [searchValue, setSearchValue] = useState(""); - - // Build a global index from every accessible settings tab in the modal navigation. - // This does not render section components, so API calls still happen only when a tab is opened. - const searchableSections = useMemo(() => { - return configNavSections.flatMap((section) => - section.items - .filter((item: ConfigNavItem) => !item.disabled) - .map((item: ConfigNavItem) => { - const translationPrefixes = getTranslationPrefixesForNavKey(item.key); - const translationContent = translationPrefixes.flatMap((prefix) => - flattenTranslationStrings( - t(prefix, { returnObjects: true, defaultValue: {} }), - ), - ); - - const searchableContent = Array.from( - new Set([ - item.label, - section.title, - `/settings/${item.key}`, - ...translationContent, - ]), - ); - - return { - value: item.key, - label: item.label, - sectionTitle: section.title, - destinationPath: `/settings/${item.key}`, - searchableContent, - }; - }), - ); - }, [configNavSections, t]); - - const filteredSearchableSections = useMemo(() => { - const query = searchValue.trim(); - if (!query) { - return searchableSections; - } - - const normalizedQuery = query.toLocaleLowerCase(); - - return searchableSections.reduce( - (accumulator, option) => { - const matchedEntry = option.searchableContent.find((entry) => - entry.toLocaleLowerCase().includes(normalizedQuery), - ); - - if (!matchedEntry) { - return accumulator; - } - - accumulator.push({ - ...option, - matchedContext: buildMatchSnippet(matchedEntry, query), - }); - - return accumulator; - }, - [], - ); - }, [searchValue, searchableSections]); - - const handleSearchNavigation = useCallback( - async (value: string | null) => { - if (!value) return; - if (!VALID_NAV_KEYS.includes(value as NavKey)) return; - await onNavigate(value as NavKey); - setSearchValue(""); - }, - [onNavigate], - ); - - return ( - setQuery(e.target.value)} - placeholder={t("portal.search.placeholder")} - aria-label={t("portal.search.ariaLabel")} - className="portal-search__input" - autoComplete="off" - spellCheck={false} - /> - - ESC - -
    - -
    - {isLoading && ( -
    - - - - -
    - )} - {isEmpty && ( - - )} - {!isLoading && - !isEmpty && - Object.entries(groups).map(([group, items]) => ( -
    -
    {group}
    - {items.map((item) => ( - - ))} -
    - ))} -
    -
    - - ); -} diff --git a/frontend/editor/src/portal/components/docs/DocsNav.tsx b/frontend/editor/src/portal/components/docs/DocsNav.tsx index 1063d5e98f..2f6258fca9 100644 --- a/frontend/editor/src/portal/components/docs/DocsNav.tsx +++ b/frontend/editor/src/portal/components/docs/DocsNav.tsx @@ -8,7 +8,8 @@ import type { DocsNavSection } from "@portal/api/docs"; * path ("functionality/security" is a child of "functionality"), so sub-sections * nest under their parent. The root "Overview" section is static (always open, no * toggle); every other section collapses, and only the branch leading to the - * active doc opens by default. (Search lives in DocsSearch above this.) + * active doc opens by default. (Full-text docs search lives in the global + * super search.) */ // Matches the generator's ROOT_SECTION_ID: the intro section is never collapsible. diff --git a/frontend/editor/src/portal/components/docs/DocsSearch.tsx b/frontend/editor/src/portal/components/docs/DocsSearch.tsx deleted file mode 100644 index 49bdb3101f..0000000000 --- a/frontend/editor/src/portal/components/docs/DocsSearch.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@app/ui"; -import type { SearchResult, Segment } from "@portal/docs/search"; - -/** Render highlighted segments, wrapping matched runs in . */ -function Highlighted({ segments }: { segments: Segment[] }) { - return ( - <> - {segments.map((s, i) => - s.hit ? ( - - {s.text} - - ) : ( - {s.text} - ), - )} - - ); -} - -/** - * Docs search box + results. While a query is active it shows a ranked list of - * matching docs — each with its section, a highlighted title, and a content - * snippet — that navigates on click (or Enter). Arrow keys move the selection. - */ -export function DocsSearch({ - query, - onQueryChange, - results, - onSelect, -}: { - query: string; - onQueryChange: (q: string) => void; - results: SearchResult[]; - onSelect: (docId: string) => void; -}) { - const { t } = useTranslation(); - // -1 = nothing pre-selected; arrow keys drive this, the mouse uses CSS :hover. - const [activeIndex, setActiveIndex] = useState(-1); - const listRef = useRef(null); - const hasQuery = query.trim().length > 0; - - useEffect(() => setActiveIndex(-1), [query]); - - useEffect(() => { - listRef.current - ?.querySelector('[data-active="true"]') - ?.scrollIntoView?.({ block: "nearest" }); - }, [activeIndex]); - - const onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - onQueryChange(""); - return; - } - if (!results.length) return; - if (e.key === "ArrowDown") { - e.preventDefault(); - setActiveIndex((i) => Math.min(i + 1, results.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setActiveIndex((i) => Math.max(i - 1, 0)); - } else if (e.key === "Enter") { - e.preventDefault(); - const hit = results[activeIndex >= 0 ? activeIndex : 0]; - if (hit) onSelect(hit.id); - } - }; - - return ( -
    -
    - - ⌕ - - onQueryChange(e.target.value)} - onKeyDown={onKeyDown} - aria-label={t("portal.docs.search.placeholder")} - /> -
    - - {hasQuery && ( -
    - {results.length === 0 ? ( -

    - {t("portal.docs.search.empty")} -

    - ) : ( - <> -
    - {t("portal.docs.search.results", { count: results.length })} -
    -
      - {results.map((r, i) => ( -
    • - -
    • - ))} -
    - - )} -
    - )} -
    - ); -} diff --git a/frontend/editor/src/portal/components/users/UsersDirectory.stories.tsx b/frontend/editor/src/portal/components/users/UsersDirectory.stories.tsx index ff4fb4b6f1..28e84691fb 100644 --- a/frontend/editor/src/portal/components/users/UsersDirectory.stories.tsx +++ b/frontend/editor/src/portal/components/users/UsersDirectory.stories.tsx @@ -23,6 +23,7 @@ const FULL_CAPS: UsersCapabilities = { seats: false, manageGrants: true, removeScope: "org", + listingRequiresAdmin: true, }; /** SaaS team-leader: invite / rename / remove-member only, no org group. */ @@ -44,6 +45,7 @@ const SAAS_CAPS: UsersCapabilities = { seats: true, manageGrants: false, removeScope: "team", + listingRequiresAdmin: false, }; /** A full org: one org owner and two teams, each with a leader. */ diff --git a/frontend/editor/src/portal/components/users/UsersDirectory.tsx b/frontend/editor/src/portal/components/users/UsersDirectory.tsx index 2e78a6d4be..4367656d0b 100644 --- a/frontend/editor/src/portal/components/users/UsersDirectory.tsx +++ b/frontend/editor/src/portal/components/users/UsersDirectory.tsx @@ -215,7 +215,8 @@ export function UsersDirectory({ function renderRow(m: Member) { const access = m.portalAccess ?? "none"; return ( -
    + // data-member-id lets deep links (?member=) scroll to and flash a row. +
    diff --git a/frontend/editor/src/portal/contexts/UIContext.tsx b/frontend/editor/src/portal/contexts/UIContext.tsx index ba0676ab6d..b5ebada257 100644 --- a/frontend/editor/src/portal/contexts/UIContext.tsx +++ b/frontend/editor/src/portal/contexts/UIContext.tsx @@ -7,11 +7,6 @@ import { } from "react"; interface UIContextValue { - searchOpen: boolean; - openSearch: () => void; - closeSearch: () => void; - toggleSearch: () => void; - /** Off-canvas sidebar drawer on small screens (no-op chrome on desktop). */ mobileNavOpen: boolean; openMobileNav: () => void; @@ -32,7 +27,8 @@ interface UIContextValue { * modal pick its own default. Cleared back to `null` on close. */ settingsInitialSection: string | null; - openSettings: (section?: string) => void; + settingsInitialFocus: string | null; + openSettings: (section?: string, focus?: string) => void; closeSettings: () => void; /** @@ -81,7 +77,6 @@ function writeSidebarCollapsed(collapsed: boolean): void { } export function UIProvider({ children }: { children: ReactNode }) { - const [searchOpen, setSearchOpen] = useState(false); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(readSidebarCollapsed); @@ -90,6 +85,9 @@ export function UIProvider({ children }: { children: ReactNode }) { const [settingsInitialSection, setSettingsInitialSection] = useState< string | null >(null); + const [settingsInitialFocus, setSettingsInitialFocus] = useState< + string | null + >(null); const [linkModalOpen, setLinkModalOpen] = useState(false); const [trialSetupRequested, setTrialSetupRequested] = useState(false); const [linkModalMode, setLinkModalMode] = useState<"link" | "reauth">("link"); @@ -101,16 +99,8 @@ export function UIProvider({ children }: { children: ReactNode }) { const value = useMemo( () => ({ - // Opening any overlay (search, settings, link modal) dismisses the mobile - // nav drawer so overlays never stack on top of it. - searchOpen, - openSearch: () => { - setMobileNavOpen(false); - setSearchOpen(true); - }, - closeSearch: () => setSearchOpen(false), - toggleSearch: () => setSearchOpen((o) => !o), - + // Opening any overlay (settings, link modal) dismisses the mobile nav + // drawer so overlays never stack on top of it. mobileNavOpen, openMobileNav: () => setMobileNavOpen(true), closeMobileNav: () => setMobileNavOpen(false), @@ -131,14 +121,17 @@ export function UIProvider({ children }: { children: ReactNode }) { settingsOpen, settingsInitialSection, - openSettings: (section?: string) => { + settingsInitialFocus, + openSettings: (section?: string, focus?: string) => { setMobileNavOpen(false); setSettingsInitialSection(section ?? null); + setSettingsInitialFocus(focus ?? null); setSettingsOpen(true); }, closeSettings: () => { setSettingsOpen(false); setSettingsInitialSection(null); + setSettingsInitialFocus(null); }, linkModalOpen, @@ -152,6 +145,7 @@ export function UIProvider({ children }: { children: ReactNode }) { setReopenSettingsAfterLink("account-link"); setSettingsOpen(false); setSettingsInitialSection(null); + setSettingsInitialFocus(null); } setLinkModalOpen(true); }, @@ -166,18 +160,19 @@ export function UIProvider({ children }: { children: ReactNode }) { setLinkModalMode("link"); if (reopenSettingsAfterLink) { setSettingsInitialSection(reopenSettingsAfterLink); + setSettingsInitialFocus(null); setSettingsOpen(true); setReopenSettingsAfterLink(null); } }, }), [ - searchOpen, mobileNavOpen, sidebarCollapsed, assistantOpen, settingsOpen, settingsInitialSection, + settingsInitialFocus, linkModalOpen, linkModalMode, reopenSettingsAfterLink, diff --git a/frontend/editor/src/portal/hooks/usePortalSearchResults.test.ts b/frontend/editor/src/portal/hooks/usePortalSearchResults.test.ts new file mode 100644 index 0000000000..4be356bab0 --- /dev/null +++ b/frontend/editor/src/portal/hooks/usePortalSearchResults.test.ts @@ -0,0 +1,442 @@ +import { createElement, type ReactNode } from "react"; +import { renderHook, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useTranslation: () => ({ + t: ( + key: string, + fallbackOrOptions?: string | Record, + ) => { + if (key === "portal.policies.defaultName") { + return `${(fallbackOrOptions as Record)?.category as string} Policy`; + } + if (typeof fallbackOrOptions === "string") return fallbackOrOptions; + const labels: Record = { + "portal.nav.users": "Users", + "portal.nav.policies": "Policies", + "portal.nav.pipelines": "Pipelines", + "portal.nav.sources": "Sources", + "portal.nav.editor": "Editor", + "superSearch.group.processor": "Processor", + "superSearch.group.settings": "Settings", + "superSearch.group.tools": "Tools", + "settings.email.smtpHost": "SMTP host", + "settings.email.title": "Email", + }; + return labels[key] ?? key; + }, + }), + }; +}); + +vi.mock("react-router-dom", () => ({ + useNavigate: vi.fn(() => vi.fn()), +})); + +vi.mock("@app/contexts/AppConfigContext", () => ({ + useAppConfig: vi.fn(() => ({ + config: { + isAdmin: false, + enableLogin: true, + }, + })), +})); + +vi.mock("@app/contexts/ToolRegistryContext", () => ({ + useToolRegistry: vi.fn(() => ({ + allTools: {}, + })), +})); + +vi.mock("@portal/contexts/TierContext", () => ({ + useTier: vi.fn(() => ({ + tier: "pro", + })), +})); + +const mockOpenSettings = vi.fn(); +vi.mock("@portal/contexts/UIContext", () => ({ + useUI: vi.fn(() => ({ + openSettings: mockOpenSettings, + })), +})); + +vi.mock("@app/data/toolsTaxonomy", () => ({ + getToolUrlPath: vi.fn((id: string) => `/tools/${id}`), + isComingSoonTool: vi.fn(() => false), +})); + +vi.mock("@app/data/settingsSearchIndex", () => ({ + SETTINGS_SEARCH_INDEX: [ + { + section: "email", + anchor: "smtp-host", + labelKey: "settings.email.smtpHost", + labelFallback: "SMTP host", + keywords: ["smtp"], + }, + ], +})); + +vi.mock("@app/data/settingsSectionRegistry", () => ({ + SETTINGS_SECTION_REGISTRY: [ + { + key: "email", + labelKey: "settings.email.title", + labelFallback: "Email", + keywords: ["smtp", "email"], + requiresLogin: true, + }, + ], +})); + +vi.mock("@app/data/settingsContentSearch", () => ({ + findSettingsContentMatch: vi.fn(() => null), + buildMatchSnippet: vi.fn(() => ""), +})); + +vi.mock("@app/data/processorSearchIndex", () => ({ + PROCESSOR_SEARCH_INDEX: [ + { + id: "users", + labelKey: "portal.nav.users", + labelFallback: "Users", + path: "/portal/users", + keywords: ["members"], + }, + { + id: "policies", + labelKey: "portal.nav.policies", + labelFallback: "Policies", + path: "/portal/policies", + keywords: ["rules"], + }, + { + id: "pipelines", + labelKey: "portal.nav.pipelines", + labelFallback: "Pipelines", + path: "/portal/pipelines", + keywords: ["automation"], + }, + { + id: "sources", + labelKey: "portal.nav.sources", + labelFallback: "Sources", + path: "/portal/sources", + keywords: ["connectors"], + }, + { + id: "docs", + labelKey: "portal.nav.docs", + labelFallback: "Documentation", + path: "/portal/docs", + keywords: ["docs"], + }, + ], + // Tests run as an org admin; per-scope access gating has its own coverage + // in the stubbed suite. + isPortalEntityScopeAccessible: () => true, +})); + +// The roster is fetched through the flavor-resolved usersBackend (the same +// path the shared users query uses), not @portal/api/users directly. +vi.mock("@app/portal/usersBackend", () => ({ + usersBackend: { + fetchUsers: vi.fn(), + }, +})); + +// Keep the real (pure) assemblePolicies; only the network fetchers are mocked. +vi.mock("@portal/api/policies", async (importOriginal) => ({ + ...(await importOriginal()), + fetchPoliciesList: vi.fn(), + fetchPolicyRuns: vi.fn(), +})); + +vi.mock("@portal/api/pipelines", () => ({ + fetchPipelines: vi.fn(), +})); + +vi.mock("@portal/api/sources", () => ({ + fetchSources: vi.fn(), +})); + +import type { CatalogueEntry } from "@portal/api/policies"; +import { fetchPoliciesList, fetchPolicyRuns } from "@portal/api/policies"; +import type { PipelineView } from "@portal/api/pipelines"; +import { fetchPipelines } from "@portal/api/pipelines"; +import { fetchSources } from "@portal/api/sources"; +import type { Member, UsersResponse } from "@portal/api/users"; +import { usersBackend } from "@app/portal/usersBackend"; +import { + rankDocsResults, + rankPortalPipelineResults, + rankPortalPolicyResults, +} from "@portal/search/entitySearch"; +import { usePortalSearchResults } from "@portal/hooks/usePortalSearchResults"; + +function makePolicyEntry(overrides?: Partial): CatalogueEntry { + return { + category: { + id: "security", + label: "Security", + tone: "purple", + desc: "Protect sensitive documents", + }, + config: { + summary: "", + rules: [], + scopeLabel: "", + fields: [], + defaultOperations: [], + }, + policy: { + category: { + id: "security", + label: "Security", + tone: "purple", + desc: "Protect sensitive documents", + }, + config: { + summary: "", + rules: [], + scopeLabel: "", + fields: [], + defaultOperations: [], + }, + state: { + configured: true, + status: "active", + sources: [], + scopeTypes: [], + reviewerEmail: "", + fieldValues: {}, + backendId: "policy-security", + }, + steps: [], + stats: { + enforced: 0, + dataProcessed: "0 B", + activeFor: "0d", + }, + activity: [], + }, + ...overrides, + }; +} + +function makePipelineView( + id: string, + name: string, + trigger = "manual", +): PipelineView { + return { + id, + name, + enabled: true, + status: "active", + trigger, + sources: [], + steps: [], + output: "inline", + owner: "alice", + }; +} + +function makeMember(overrides?: Partial): Member { + return { + id: "member-1", + name: "Alice Admin", + email: "alice@example.com", + role: "admin", + status: "active", + lastActive: "1m ago", + ...overrides, + }; +} + +function makeUsersResponse(members: Member[]): UsersResponse { + return { + summary: { + totalMembers: members.length, + pendingInvites: 0, + seatsUsed: members.length, + seatLimit: null, + }, + members, + roles: [], + access: { + tier: "pro", + seatsUsed: members.length, + seatLimit: null, + }, + mailEnabled: true, + emailInvitesEnabled: true, + }; +} + +function createDeferred() { + let resolve: ((value: T) => void) | undefined; + let reject: ((reason?: unknown) => void) | undefined; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { + promise, + resolve: (value: T) => resolve?.(value), + reject: (reason?: unknown) => reject?.(reason), + }; +} + +function queryWrapper() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client }, children); +} + +describe("usePortalSearchResults helpers", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockOpenSettings.mockReset(); + vi.mocked(usersBackend.fetchUsers).mockResolvedValue(makeUsersResponse([])); + vi.mocked(fetchPoliciesList).mockResolvedValue([]); + vi.mocked(fetchPolicyRuns).mockResolvedValue([]); + vi.mocked(fetchPipelines).mockResolvedValue({ kpis: [], pipelines: [] }); + vi.mocked(fetchSources).mockResolvedValue({ kpis: [], sources: [] }); + }); + + it("ranks configured policies under the policies group", () => { + const openPolicy = vi.fn(); + const results = rankPortalPolicyResults( + [makePolicyEntry()], + "security policy", + (key: string, options?: Record) => + key === "portal.policies.defaultName" + ? `${options?.category as string} Policy` + : key, + openPolicy, + ); + + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + key: "portal-policy:security", + group: "portal-policies", + title: "Security Policy", + }); + + void results[0]?.onSelect(); + expect(openPolicy).toHaveBeenCalledWith("security"); + }); + + it("filters policy-backed records out of the pipelines group", () => { + const openPipeline = vi.fn(); + const results = rankPortalPipelineResults( + [ + makePipelineView("policy-security", "Security Policy"), + makePipelineView("custom-pipeline", "Nightly OCR"), + ], + "nightly", + new Set(["policy-security"]), + openPipeline, + ); + + expect(results.map((result) => result.key)).toEqual([ + "portal-pipeline:custom-pipeline", + ]); + }); + + it("full-text searches the bundled docs, not just their titles", () => { + const navigate = vi.fn(); + // "Tesseract" appears in the OCR doc body but in no doc title — a hit + // whose snippet contains it proves content search. + const results = rankDocsResults("Tesseract", navigate); + expect(results.length).toBeGreaterThan(0); + expect(results[0]?.subtitle).toMatch(/tesseract/i); + + void results[0]?.onSelect(); + expect(navigate).toHaveBeenCalledWith(expect.stringMatching(/\/docs#./)); + }); + + it("forwards portal settings row hits with their focus anchor", () => { + const { result } = renderHook( + () => usePortalSearchResults("smtp", true, { scopeIds: ["settings"] }), + { wrapper: queryWrapper() }, + ); + + const settingHit = result.current.flatResults[0]; + expect(settingHit?.key).toBe("setting:email:smtp-host"); + + void settingHit?.onSelect(); + expect(mockOpenSettings).toHaveBeenCalledWith("email", "smtp-host"); + expect(usersBackend.fetchUsers).not.toHaveBeenCalled(); + }); + + it("fetches only the requested entity scope", async () => { + vi.mocked(usersBackend.fetchUsers).mockResolvedValue( + makeUsersResponse([makeMember({ id: "member-2", name: "Alice" })]), + ); + + const { result } = renderHook( + () => + usePortalSearchResults("alice", true, { scopeIds: ["portal-users"] }), + { wrapper: queryWrapper() }, + ); + + await waitFor(() => + expect(usersBackend.fetchUsers).toHaveBeenCalledTimes(1), + ); + await waitFor(() => expect(result.current.loadingFiles).toBe(false)); + + expect(fetchPoliciesList).not.toHaveBeenCalled(); + expect(fetchPipelines).not.toHaveBeenCalled(); + expect(fetchSources).not.toHaveBeenCalled(); + expect(result.current.groups.map((group) => group.id)).toEqual([ + "portal-users", + ]); + }); + + it("reuses the in-flight query after close/reopen instead of sticking in loading", async () => { + const firstUsers = createDeferred(); + vi.mocked(usersBackend.fetchUsers).mockImplementationOnce( + () => firstUsers.promise, + ); + + const { result, rerender } = renderHook( + ({ query }) => + usePortalSearchResults(query, true, { scopeIds: ["portal-users"] }), + { + initialProps: { query: "alice" }, + wrapper: queryWrapper(), + }, + ); + + await waitFor(() => expect(result.current.loadingFiles).toBe(true)); + expect(usersBackend.fetchUsers).toHaveBeenCalledTimes(1); + + rerender({ query: "" }); + await waitFor(() => expect(result.current.loadingFiles).toBe(false)); + + rerender({ query: "alice" }); + await waitFor(() => expect(result.current.loadingFiles).toBe(true)); + expect(usersBackend.fetchUsers).toHaveBeenCalledTimes(1); + + firstUsers.resolve( + makeUsersResponse([ + makeMember({ id: "member-3", name: "Alice Reloaded" }), + ]), + ); + + await waitFor(() => expect(result.current.loadingFiles).toBe(false)); + expect(result.current.groups.map((group) => group.id)).toEqual([ + "portal-users", + ]); + }); +}); diff --git a/frontend/editor/src/portal/hooks/usePortalSearchResults.tsx b/frontend/editor/src/portal/hooks/usePortalSearchResults.tsx new file mode 100644 index 0000000000..b839d980d6 --- /dev/null +++ b/frontend/editor/src/portal/hooks/usePortalSearchResults.tsx @@ -0,0 +1,321 @@ +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { getToolUrlPath } from "@app/data/toolsTaxonomy"; +import { isPortalEntityScopeAccessible } from "@app/data/processorSearchIndex"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { PORTAL_HIDDEN_SECTION_KEYS } from "@portal/components/PortalSettingsHost"; +import { useToolRegistry } from "@app/contexts/ToolRegistryContext"; +import { + assembleSuperSearchGroups, + rankSettingsResults, + rankToolResults, + useSearchScopeFilter, +} from "@app/hooks/useSuperSearch"; +import { + PORTAL_ENTITY_SCOPE_DEFS, + PORTAL_DOCS_SCOPE_ID, + type SuperSearchGates, + type SuperSearchGroup, + type SuperSearchGroupId, + type SuperSearchQueryOptions, + type SuperSearchScope, + type UseSuperSearchResult, +} from "@app/types/superSearch"; +import type { ToolId } from "@app/types/toolId"; +import { assignLocation, openExternalUrl } from "@app/utils/safeNavigation"; +import { usersBackend } from "@app/portal/usersBackend"; +import { + assemblePolicies, + fetchPoliciesList, + fetchPolicyRuns, +} from "@portal/api/policies"; +import { fetchPipelines } from "@portal/api/pipelines"; +import { fetchSources } from "@portal/api/sources"; +import { EDITOR_IS_SAME_APP, EDITOR_URL } from "@portal/auth/editorUrl"; +import { useTier } from "@portal/contexts/TierContext"; +import { useUI } from "@portal/contexts/UIContext"; +import { qk } from "@portal/queries/keys"; +import { + buildProcessorEntityGroups, + defaultPortalEntityScopes, + isDocsSearchable, + isVisiblePortalScope, + withPortalEntityDependencies, + type ProcessorEntities, +} from "@portal/search/entitySearch"; + +const EDITOR_GROUP_ORDER: SuperSearchGroupId[] = ["tools"]; +const SETTINGS_GROUP_ORDER: SuperSearchGroupId[] = ["settings"]; +const PROCESSOR_SECTION_LABEL_KEY = "superSearch.group.processor"; +const PROCESSOR_SECTION_LABEL_FALLBACK = "Processor"; +const SETTINGS_SECTION_LABEL_KEY = "superSearch.group.settings"; +const SETTINGS_SECTION_LABEL_FALLBACK = "Settings"; +const EDITOR_SECTION_LABEL_KEY = "portal.nav.editor"; +const EDITOR_SECTION_LABEL_FALLBACK = "Editor"; + +/** + * Cross-origin editor URL for a tool. Only used when a separately-hosted + * editor is configured — the same-app case routes client-side instead: on + * bundled deploys the backend serves the frontend and 401s unauthenticated + * document GETs (the JWT lives in localStorage, so a full page load carries + * no credentials), which would bounce every tool hop to /login. + */ +function externalEditorHref(path: string): string { + return EDITOR_URL.replace(/\/$/, "") + path; +} + +/** + * The portal bar's filter chips — every lane the editor offers except Files + * (files only open in the editor) and Pages (the sidebar covers navigation). + * Ordered to match the dropdown's section priority. Lanes whose data source + * refuses this session (the users roster for non-admins on self-hosted) get + * no chip — an offered lane must be able to return results. + */ +export function usePortalSearchScopes(): SuperSearchScope[] { + const { t } = useTranslation(); + const { config } = useAppConfig(); + const isAdmin = config?.isAdmin ?? false; + + return useMemo( + () => [ + ...PORTAL_ENTITY_SCOPE_DEFS.filter( + (def) => + isVisiblePortalScope(def.id) && + isPortalEntityScopeAccessible(def.id, isAdmin), + ).map((def) => ({ + id: def.id, + label: t(def.labelKey, def.labelFallback), + aliases: [...def.aliases], + })), + ...(isDocsSearchable() + ? [ + { + id: PORTAL_DOCS_SCOPE_ID, + label: t("superSearch.group.docs", "Docs"), + aliases: ["doc", "docs", "documentation"], + }, + ] + : []), + { + id: "settings", + label: t("superSearch.group.settings", "Settings"), + aliases: ["setting", "settings"], + }, + { + id: "tools", + label: t("superSearch.group.tools", "Tools"), + aliases: ["tool", "tools"], + }, + ], + [t, isAdmin], + ); +} + +/** + * The portal's results provider for the shared super search bar: files stay + * editor-only, portal entity results are grouped under a Processor section, + * and the shared tools/settings lanes sit under an Editor section. Portal page + * routes themselves stay out of the portal search — once you're in the portal, + * the entities are the useful targets. + */ +export function usePortalSearchResults( + query: string, + active: boolean, + options?: SuperSearchQueryOptions, +): UseSuperSearchResult { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { openSettings } = useUI(); + const { allTools } = useToolRegistry(); + const { config } = useAppConfig(); + const { tier } = useTier(); + + const trimmed = query.trim(); + const { scopeEnabled } = useSearchScopeFilter(options); + const requestedEntityScopes = useMemo(() => { + if (!active || trimmed.length === 0) return new Set(); + const enabled = defaultPortalEntityScopes(config?.isAdmin ?? false).filter( + (scopeId) => scopeEnabled(scopeId), + ); + return new Set(withPortalEntityDependencies(enabled)); + }, [active, scopeEnabled, trimmed, config?.isAdmin]); + + // Entity data rides the portal's shared query layer — the same keys the + // views use, so searching warms the view (and vice versa) and the client's + // staleTime/retry policy replaces bespoke fetch discipline. `enabled` keeps + // each lane's fetch behind its scope chip and the active-query gate. + const usersQuery = useQuery({ + queryKey: qk.usersRoster(tier), + queryFn: () => usersBackend.fetchUsers(tier), + enabled: requestedEntityScopes.has("portal-users"), + }); + const policiesListQuery = useQuery({ + queryKey: qk.policiesList(), + queryFn: fetchPoliciesList, + enabled: requestedEntityScopes.has("portal-policies"), + }); + const policyRunsQuery = useQuery({ + queryKey: qk.policyRuns(), + queryFn: fetchPolicyRuns, + enabled: requestedEntityScopes.has("portal-policies"), + }); + const pipelinesQuery = useQuery({ + queryKey: qk.pipelines(), + queryFn: fetchPipelines, + enabled: requestedEntityScopes.has("portal-pipelines"), + }); + const sourcesQuery = useQuery({ + queryKey: qk.sources(), + queryFn: fetchSources, + enabled: requestedEntityScopes.has("portal-sources"), + }); + + // Loading only counts for lanes the current search actually requests — a + // fetch left in flight after its lane was deselected (or the bar closed) + // must not hold the dropdown's no-results gate open. + const loadingEntities = + (requestedEntityScopes.has("portal-users") && usersQuery.isLoading) || + (requestedEntityScopes.has("portal-policies") && + (policiesListQuery.isLoading || policyRunsQuery.isLoading)) || + (requestedEntityScopes.has("portal-pipelines") && + pipelinesQuery.isLoading) || + (requestedEntityScopes.has("portal-sources") && sourcesQuery.isLoading); + + const entities = useMemo( + () => ({ + users: usersQuery.data?.members ?? [], + policies: policiesListQuery.data + ? assemblePolicies(policiesListQuery.data, policyRunsQuery.data ?? []) + .catalogue + : [], + pipelines: pipelinesQuery.data?.pipelines ?? [], + sources: sourcesQuery.data?.sources ?? [], + }), + [ + usersQuery.data, + policiesListQuery.data, + policyRunsQuery.data, + pipelinesQuery.data, + sourcesQuery.data, + ], + ); + + const openTool = useCallback( + (id: ToolId) => { + // Link tools have no in-editor UI — navigating to a tool URL for one + // lands on a "tool not found" panel. Open their destination directly, + // matching how the editor's tool lists treat them. + const tool = allTools[id]; + if (tool?.link) { + openExternalUrl(tool.link); + return; + } + const path = getToolUrlPath(id); + if (EDITOR_IS_SAME_APP) { + // One SPA: swap route-sets through the router. The portal tree + // unmounts and the editor mounts fresh at the tool URL, so its + // URL-driven tool init runs exactly as it does on a cold load. + navigate(path); + } else { + assignLocation(externalEditorHref(path)); + } + }, + [allTools, navigate], + ); + + const openSettingsSection = useCallback( + (section: string, anchor?: string) => openSettings(section, anchor), + [openSettings], + ); + + const gates = useMemo( + () => + config + ? { + isAdmin: config.isAdmin ?? false, + loginEnabled: config.enableLogin ?? false, + showSettingsWhenNoLogin: config.showSettingsWhenNoLogin ?? true, + } + : null, + [config], + ); + + const entityGroups = useMemo( + () => + buildProcessorEntityGroups(entities, trimmed, t, navigate, { + scopeEnabled, + }), + [entities, trimmed, t, navigate, scopeEnabled], + ); + + const groups = useMemo(() => { + // Section order: Processor first, Settings second, Editor last. + const settingsGroups = assembleSuperSearchGroups( + { + settings: scopeEnabled("settings") + ? rankSettingsResults( + trimmed, + t, + gates, + openSettingsSection, + undefined, + PORTAL_HIDDEN_SECTION_KEYS, + ) + : [], + }, + t, + SETTINGS_GROUP_ORDER, + ).map((group) => ({ + ...group, + sectionLabel: t( + SETTINGS_SECTION_LABEL_KEY, + SETTINGS_SECTION_LABEL_FALLBACK, + ), + })); + + const editorGroups = assembleSuperSearchGroups( + { + tools: scopeEnabled("tools") + ? rankToolResults(allTools, trimmed, openTool) + : [], + }, + t, + EDITOR_GROUP_ORDER, + ).map((group) => ({ + ...group, + sectionLabel: t(EDITOR_SECTION_LABEL_KEY, EDITOR_SECTION_LABEL_FALLBACK), + })); + + return [ + ...entityGroups.map((group) => ({ + ...group, + sectionLabel: t( + PROCESSOR_SECTION_LABEL_KEY, + PROCESSOR_SECTION_LABEL_FALLBACK, + ), + })), + ...settingsGroups, + ...editorGroups, + ]; + }, [ + entityGroups, + gates, + openSettingsSection, + openTool, + scopeEnabled, + allTools, + t, + trimmed, + ]); + + const flatResults = useMemo( + () => groups.flatMap((group) => group.results), + [groups], + ); + + // loadingFiles doubles as "an async source is still loading" for the + // dropdown's no-results gate — here that's the entity fetch. + return { groups, flatResults, loadingFiles: loadingEntities }; +} diff --git a/frontend/editor/src/portal/mocks/handlers/index.ts b/frontend/editor/src/portal/mocks/handlers/index.ts index d5428b1853..13b22d6941 100644 --- a/frontend/editor/src/portal/mocks/handlers/index.ts +++ b/frontend/editor/src/portal/mocks/handlers/index.ts @@ -1,7 +1,6 @@ import { assistantHandlers } from "@portal/mocks/handlers/assistant"; import { authHandlers } from "@portal/mocks/handlers/auth"; import { notificationsHandlers } from "@portal/mocks/handlers/notifications"; -import { searchHandlers } from "@portal/mocks/handlers/search"; import { pipelinesHandlers } from "@portal/mocks/handlers/pipelines"; import { sourcesHandlers } from "@portal/mocks/handlers/sources"; import { infrastructureHandlers } from "@portal/mocks/handlers/infrastructure"; @@ -21,7 +20,6 @@ export const handlers = [ ...authHandlers, ...notificationsHandlers, ...assistantHandlers, - ...searchHandlers, ...pipelinesHandlers, ...sourcesHandlers, ...infrastructureHandlers, diff --git a/frontend/editor/src/portal/mocks/handlers/search.ts b/frontend/editor/src/portal/mocks/handlers/search.ts deleted file mode 100644 index b62e8bc910..0000000000 --- a/frontend/editor/src/portal/mocks/handlers/search.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { http, HttpResponse } from "msw"; -import { QUICK_ACTIONS } from "@portal/mocks/search"; - -export const searchHandlers = [ - http.get("/v1/search/quick-actions", () => { - return HttpResponse.json(QUICK_ACTIONS); - }), -]; diff --git a/frontend/editor/src/portal/mocks/search.ts b/frontend/editor/src/portal/mocks/search.ts deleted file mode 100644 index 86d332de70..0000000000 --- a/frontend/editor/src/portal/mocks/search.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Mock quick-action catalogue for the ⌘K search palette. The QuickAction type - * lives in api/search.ts (the backend contract); this module only builds fake - * data for Storybook and tests. - */ - -import type { QuickAction } from "@portal/api/search"; - -export const QUICK_ACTIONS: QuickAction[] = [ - { group: "Jump to", label: "Home", hint: "G H" }, - { group: "Jump to", label: "Pipelines", hint: "G P" }, - { group: "Jump to", label: "Sources", hint: "G S" }, - { group: "Jump to", label: "Documents", hint: "G D" }, - { group: "Create", label: "New pipeline", hint: "N P" }, - { group: "Create", label: "New API key", hint: "N K" }, - { group: "Theme", label: "Toggle dark / light", hint: "T" }, -]; diff --git a/frontend/editor/src/portal/search/entitySearch.tsx b/frontend/editor/src/portal/search/entitySearch.tsx new file mode 100644 index 0000000000..1d0a7cc489 --- /dev/null +++ b/frontend/editor/src/portal/search/entitySearch.tsx @@ -0,0 +1,393 @@ +import { + PROCESSOR_SEARCH_INDEX, + isPortalEntityScopeAccessible, +} from "@app/data/processorSearchIndex"; +import { + PORTAL_ENTITY_SCOPE_DEFS, + PORTAL_DOCS_SCOPE_ID, + type SuperSearchGroup, + type SuperSearchResult, +} from "@app/types/superSearch"; +import { rankByFuzzy } from "@app/utils/fuzzySearch"; +import { + assemblePolicies, + fetchPoliciesList, + fetchPolicyRuns, + type CatalogueEntry, +} from "@portal/api/policies"; +import { fetchPipelines, type PipelineView } from "@portal/api/pipelines"; +import { fetchSources, type SourceView } from "@portal/api/sources"; +import type { Member } from "@portal/api/users"; +// Flavor-resolved users backend: self-hosted reads the proprietary admin +// endpoints, SaaS the invitation-based team endpoints (the admin ones 403 +// there for the always-ROLE_USER sessions). +import { usersBackend } from "@app/portal/usersBackend"; +import { + DocsIcon, + PipelinesIcon, + PoliciesIcon, + SourcesIcon, + UsersIcon, +} from "@portal/components/icons"; +import type { Tier } from "@portal/contexts/TierContext"; +import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; +import { allDocs, loadDocsNav } from "@portal/docs/manifest/registry"; +import { searchDocs, toPlainText, type SearchDoc } from "@portal/docs/search"; + +/** + * The Processor's entity search: users, policies, pipelines and sources, + * fetched per scope and fuzzy-ranked client-side. Shared by both super search + * hosts — the portal bar imports it statically, the editor bar loads it on + * demand through the processorEntitySearch seam (a static import there would + * pull the portal into the main bundle). + */ + +export const PORTAL_ENTITY_SCOPE_IDS = PORTAL_ENTITY_SCOPE_DEFS.map( + (def) => def.id, +); + +export type PortalEntityScopeId = + (typeof PORTAL_ENTITY_SCOPE_DEFS)[number]["id"]; + +export type PortalEntityItems = + | Member[] + | CatalogueEntry[] + | PipelineView[] + | SourceView[]; + +export interface ProcessorEntities { + users: Member[]; + policies: CatalogueEntry[]; + pipelines: PipelineView[]; + sources: SourceView[]; +} + +/** + * How many results each entity ranker computes — the ceiling the dropdown's + * "show more" can reveal (the component shows a small initial slice). + */ +export const ENTITY_GROUP_LIMIT = 24; + +/** How long a fetched entity scope stays fresh before a search refetches it. */ +export const ENTITY_REFRESH_MS = 30_000; + +const PORTAL_VIEW_BY_SCOPE_ID = Object.fromEntries( + PORTAL_ENTITY_SCOPE_DEFS.map((def) => [def.id, def.viewId]), +) as Record; + +const VISIBLE_PORTAL_VIEW_IDS = new Set( + PROCESSOR_SEARCH_INDEX.map((entry) => entry.id), +); + +/** Whether the flavor's portal nav ships the view an entity scope targets. */ +export function isVisiblePortalScope(scopeId: PortalEntityScopeId): boolean { + return VISIBLE_PORTAL_VIEW_IDS.has(PORTAL_VIEW_BY_SCOPE_ID[scopeId]); +} + +/** Whether this build ships the in-app developer docs (so they're searchable). */ +export function isDocsSearchable(): boolean { + return VISIBLE_PORTAL_VIEW_IDS.has("docs"); +} + +// The docs manifest is static (bundled JSON), so the full-text index — the +// plaintext strip over every doc — is built once and reused across queries. +let docsSearchIndex: SearchDoc[] | null = null; +function getDocsSearchIndex(): SearchDoc[] { + if (!docsSearchIndex) { + const sectionLabels = new Map(loadDocsNav().map((s) => [s.id, s.label])); + docsSearchIndex = allDocs().map((doc) => ({ + id: doc.id, + title: doc.title, + sectionLabel: sectionLabels.get(doc.section) ?? "", + text: toPlainText(doc.markdown), + })); + } + return docsSearchIndex; +} + +export function rankDocsResults( + trimmed: string, + navigate: (path: string) => void, + limit = ENTITY_GROUP_LIMIT, +): SuperSearchResult[] { + if (!isDocsSearchable()) return []; + return searchDocs(getDocsSearchIndex(), trimmed, limit).map((result) => ({ + key: `portal-doc:${result.id}`, + group: PORTAL_DOCS_SCOPE_ID, + title: result.title, + // The matched-content snippet (full text is what makes docs worth + // searching), falling back to the doc's section when the hit is title-only. + subtitle: + result.snippet + .map((seg) => seg.text) + .join("") + .trim() || result.sectionLabel, + icon: , + score: result.score, + onSelect: () => navigate(`${toPortalPath(VIEW_PATHS.docs)}#${result.id}`), + })); +} + +export function withPortalEntityDependencies( + scopes: readonly PortalEntityScopeId[], +): readonly PortalEntityScopeId[] { + // Pipeline rows must exclude policy-backed records, so they depend on the + // policy catalogue even when the user only scoped into pipelines. + if ( + !scopes.includes("portal-pipelines") || + scopes.includes("portal-policies") + ) { + return scopes; + } + return [...scopes, "portal-policies"]; +} + +/** Every entity scope the flavor ships AND the session can actually query + * (see isPortalEntityScopeAccessible), dependencies included — the request + * set for an unscoped search. */ +export function defaultPortalEntityScopes( + isAdmin: boolean, +): readonly PortalEntityScopeId[] { + return withPortalEntityDependencies( + PORTAL_ENTITY_SCOPE_IDS.filter( + (scopeId) => + isVisiblePortalScope(scopeId) && + isPortalEntityScopeAccessible(scopeId, isAdmin), + ), + ); +} + +/** One entity scope's fetch, for the editor seam (which has no QueryClient — + * the portal bar reads the shared query layer instead). `tier` shapes only + * presentational fields on the users payload, never the lists — hosts without + * a TierContext pass "free". */ +export async function fetchPortalEntityScope( + scopeId: PortalEntityScopeId, + tier: Tier, +): Promise { + switch (scopeId) { + case "portal-users": + return (await usersBackend.fetchUsers(tier)).members; + case "portal-policies": { + const [list, runs] = await Promise.all([ + fetchPoliciesList(), + fetchPolicyRuns(), + ]); + return assemblePolicies(list, runs).catalogue; + } + case "portal-pipelines": + return (await fetchPipelines()).pipelines; + case "portal-sources": + return (await fetchSources()).sources; + } +} + +/** Assembles per-scope cache values into the typed entity sets. The casts are + * sound because fetchPortalEntityScope keys each item type to its scope. */ +export function toProcessorEntities( + values: Partial>, +): ProcessorEntities { + return { + users: (values["portal-users"] as Member[] | undefined) ?? [], + policies: (values["portal-policies"] as CatalogueEntry[] | undefined) ?? [], + pipelines: (values["portal-pipelines"] as PipelineView[] | undefined) ?? [], + sources: (values["portal-sources"] as SourceView[] | undefined) ?? [], + }; +} + +type Translate = (key: string, options?: Record) => string; + +function policyResultTitle(entry: CatalogueEntry, t: Translate) { + const category = t(entry.category.label); + return entry.policy + ? t("portal.policies.defaultName", { category }) + : category; +} + +export function rankPortalPolicyResults( + entries: CatalogueEntry[], + trimmed: string, + t: Translate, + openPolicy: (categoryId: string) => void, + limit = ENTITY_GROUP_LIMIT, +): SuperSearchResult[] { + return rankByFuzzy( + entries.filter((entry) => !entry.category.comingSoon), + trimmed, + [ + (entry) => policyResultTitle(entry, t), + (entry) => t(entry.category.label), + (entry) => t(entry.category.desc), + ], + ) + .slice(0, limit) + .map(({ item, score }) => ({ + key: `portal-policy:${item.category.id}`, + group: "portal-policies", + title: policyResultTitle(item, t), + subtitle: t(item.category.desc), + icon: , + score, + onSelect: () => openPolicy(item.category.id), + })); +} + +export function rankPortalPipelineResults( + entries: PipelineView[], + trimmed: string, + excludedIds: ReadonlySet, + openPipeline: (pipelineId: string) => void, + limit = ENTITY_GROUP_LIMIT, +): SuperSearchResult[] { + return rankByFuzzy( + entries.filter((entry) => !excludedIds.has(entry.id)), + trimmed, + [(entry) => entry.name, (entry) => entry.trigger], + ) + .slice(0, limit) + .map(({ item, score }) => ({ + key: `portal-pipeline:${item.id}`, + group: "portal-pipelines", + title: item.name, + subtitle: item.trigger, + icon: , + score, + onSelect: () => openPipeline(item.id), + })); +} + +export interface BuildEntityGroupsOptions { + /** Host scope filter; defaults to every scope enabled. */ + scopeEnabled?: (scopeId: string) => boolean; +} + +/** + * Ranks the entity sets into display groups. Selects navigate to the entity's + * portal route (deep links where the views support them) — the portal is a + * route-set of the same SPA, so this works from either app. + */ +export function buildProcessorEntityGroups( + entities: ProcessorEntities, + trimmed: string, + t: Translate, + navigate: (path: string) => void, + options: BuildEntityGroupsOptions = {}, +): SuperSearchGroup[] { + if (!trimmed) return []; + const scopeEnabled = options.scopeEnabled ?? (() => true); + const groups: SuperSearchGroup[] = []; + + const includeScope = (scopeId: PortalEntityScopeId) => + isVisiblePortalScope(scopeId) && scopeEnabled(scopeId); + + const users = includeScope("portal-users") + ? rankByFuzzy(entities.users, trimmed, [ + (member) => member.name, + (member) => member.email, + ]) + .slice(0, ENTITY_GROUP_LIMIT) + .map(({ item, score }) => ({ + key: `portal-user:${item.id}`, + group: "portal-users", + title: item.name, + subtitle: item.email, + icon: , + score, + onSelect: () => + navigate( + `${toPortalPath(VIEW_PATHS.users)}?member=${encodeURIComponent(item.id)}`, + ), + })) + : []; + if (users.length > 0) { + groups.push({ + id: "portal-users", + label: t("portal.nav.users"), + results: users, + }); + } + + const policies = includeScope("portal-policies") + ? rankPortalPolicyResults( + entities.policies, + trimmed, + t, + (categoryId) => + navigate( + `${toPortalPath(VIEW_PATHS.policies)}?category=${encodeURIComponent(categoryId)}`, + ), + ENTITY_GROUP_LIMIT, + ) + : []; + if (policies.length > 0) { + groups.push({ + id: "portal-policies", + label: t("portal.nav.policies"), + results: policies, + }); + } + + // Policy-backed pipelines already surface as policies; listing them twice + // under different names would read as duplicates. + const policyPipelineIds = new Set( + entities.policies.flatMap((entry) => + entry.policy?.state.backendId ? [entry.policy.state.backendId] : [], + ), + ); + const pipelines = includeScope("portal-pipelines") + ? rankPortalPipelineResults( + entities.pipelines, + trimmed, + policyPipelineIds, + (pipelineId) => + navigate(`${toPortalPath(VIEW_PATHS.pipelines)}/${pipelineId}`), + ENTITY_GROUP_LIMIT, + ) + : []; + if (pipelines.length > 0) { + groups.push({ + id: "portal-pipelines", + label: t("portal.nav.pipelines"), + results: pipelines, + }); + } + + const sources = includeScope("portal-sources") + ? rankByFuzzy(entities.sources, trimmed, [ + (source) => source.name, + (source) => source.type, + ]) + .slice(0, ENTITY_GROUP_LIMIT) + .map(({ item, score }) => ({ + key: `portal-source:${item.id}`, + group: "portal-sources", + title: item.name, + subtitle: item.type, + icon: , + score, + onSelect: () => + navigate(`${toPortalPath(VIEW_PATHS.sources)}/${item.id}`), + })) + : []; + if (sources.length > 0) { + groups.push({ + id: "portal-sources", + label: t("portal.nav.sources"), + results: sources, + }); + } + + const docs = + isDocsSearchable() && scopeEnabled(PORTAL_DOCS_SCOPE_ID) + ? rankDocsResults(trimmed, navigate, ENTITY_GROUP_LIMIT) + : []; + if (docs.length > 0) { + groups.push({ + id: PORTAL_DOCS_SCOPE_ID, + label: t("superSearch.group.docs"), + results: docs, + }); + } + + return groups; +} diff --git a/frontend/editor/src/portal/views/DeveloperDocs.css b/frontend/editor/src/portal/views/DeveloperDocs.css index 314cf17eef..6487a9afff 100644 --- a/frontend/editor/src/portal/views/DeveloperDocs.css +++ b/frontend/editor/src/portal/views/DeveloperDocs.css @@ -45,137 +45,6 @@ gap: 0.25rem; } -/* Search */ -.portal-docs__search { - margin-bottom: 0.75rem; - padding: 0 0.75rem; -} - -.portal-docs__search-box { - position: relative; -} - -.portal-docs__search-icon { - position: absolute; - left: 0.625rem; - top: 50%; - transform: translateY(-50%); - font-size: 0.9375rem; - color: var(--c-text-subtle); - pointer-events: none; -} - -.portal-docs__search-input { - width: 100%; - padding: 0.4rem 0.6rem 0.4rem 1.9rem; - font-size: 0.8125rem; - color: var(--c-text); - background: var(--color-bg-subtle); - border: 1px solid var(--c-border-subtle); - border-radius: var(--radius-md); - outline: none; -} - -.portal-docs__search-input:focus { - border-color: var(--c-primary); -} - -.portal-docs__nav-empty { - font-size: 0.8125rem; - color: var(--c-text-subtle); - padding: 0.5rem 0.75rem; - margin: 0; -} - -/* ── Search results ────────────────────────────────────────────────────── */ - -.portal-docs__results { - margin-top: 0.5rem; -} - -.portal-docs__results-count { - font-size: 0.6875rem; - font-weight: 500; - color: var(--c-text-subtle); - padding: 0 0.75rem 0.5rem; -} - -.portal-docs__results-list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; -} - -/* Hairline divider between results for clear, calm separation. */ -.portal-docs__results-list li + li { - border-top: 1px solid var(--c-border-subtle); -} - -.portal-docs__result { - height: auto; - padding: 0.5rem 0.75rem; - border-radius: 0; -} - -.portal-docs__result:hover, -.portal-docs__result.is-active { - background: var(--c-hover); -} - -.portal-docs__result-body { - display: flex; - flex-direction: column; - gap: 0.125rem; - width: 100%; - min-width: 0; - text-align: left; - white-space: normal; -} - -/* Title + section share one line; the title truncates before the section. */ -.portal-docs__result-head { - display: flex; - align-items: baseline; - gap: 0.4rem; - min-width: 0; -} - -.portal-docs__result-title { - font-size: 0.8125rem; - font-weight: 600; - color: var(--c-text); - line-height: 1.3; - min-width: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.portal-docs__result-section { - flex-shrink: 0; - font-size: 0.6875rem; - color: var(--c-text-subtle); -} - -.portal-docs__result-snippet { - font-size: 0.75rem; - line-height: 1.4; - color: var(--c-text-subtle); - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; -} - -/* Subtle match emphasis — coloured text, not a filled block. */ -.portal-docs__hl { - color: var(--c-accent-text); - font-weight: 600; - background: none; -} - .portal-docs__nav-group { display: flex; flex-direction: column; diff --git a/frontend/editor/src/portal/views/DeveloperDocs.test.tsx b/frontend/editor/src/portal/views/DeveloperDocs.test.tsx index d2071011d8..354c6359c6 100644 --- a/frontend/editor/src/portal/views/DeveloperDocs.test.tsx +++ b/frontend/editor/src/portal/views/DeveloperDocs.test.tsx @@ -25,7 +25,6 @@ const renderDocs = (ui: ReactElement) => describe("DeveloperDocs — markdown browser over the generated manifest", () => { it("keeps Overview static (open, no toggle) and other sections collapsed", () => { renderDocs(); - expect(screen.getByRole("searchbox")).toBeInTheDocument(); // Overview is static: its items show, and it has no toggle button. expect( screen.getByRole("button", { name: "Production Deployment Guide" }), @@ -50,23 +49,6 @@ describe("DeveloperDocs — markdown browser over the generated manifest", () => ).toBeInTheDocument(); }); - it("searches doc content (not just titles), shows a snippet, and navigates", async () => { - renderDocs(); - // "Tesseract" appears in the OCR doc body but in no doc title — a result - // whose snippet contains it proves full-text (content) search. - fireEvent.change(screen.getByRole("searchbox"), { - target: { value: "Tesseract" }, - }); - const hits = await screen.findAllByRole("button", { name: /Tesseract/i }); - expect(hits.length).toBeGreaterThan(0); - fireEvent.click(hits[0]); - await waitFor(() => - expect( - screen.queryByText(/locally hosted web application/i), - ).not.toBeInTheDocument(), - ); - }); - it("follows an internal doc: link inside the rendered markdown", async () => { renderDocs(); // The Getting Started body links to the Migration guide via the doc: scheme. diff --git a/frontend/editor/src/portal/views/DeveloperDocs.tsx b/frontend/editor/src/portal/views/DeveloperDocs.tsx index fb62b8566c..d32666e1dd 100644 --- a/frontend/editor/src/portal/views/DeveloperDocs.tsx +++ b/frontend/editor/src/portal/views/DeveloperDocs.tsx @@ -3,25 +3,22 @@ import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; import { Button, EmptyState } from "@app/ui"; import { DocsNav } from "@portal/components/docs/DocsNav"; -import { DocsSearch } from "@portal/components/docs/DocsSearch"; import { DocsSection } from "@portal/components/docs/DocsSection"; import { DocsToc } from "@portal/components/docs/DocsToc"; import { MarkdownDoc } from "@portal/components/docs/MarkdownDoc"; import { extractHeadings } from "@portal/docs/headings"; import { - allDocs, firstDocId, loadDoc, loadDocsNav, } from "@portal/docs/manifest/registry"; -import { searchDocs, toPlainText, type SearchDoc } from "@portal/docs/search"; import "@portal/views/DeveloperDocs.css"; /** * Developer Docs — a markdown browser over the docs manifest generated from the * Stirling docs repo (see scripts/sync-portal-docs.mts). The nav is auto-sorted - * from the repo's folders + frontmatter; content is the repo markdown, and the - * search box does full-text search across every doc. + * from the repo's folders + frontmatter; content is the repo markdown. Full-text + * search across docs lives in the global super search (Cmd/Ctrl+K). */ export function DeveloperDocs() { const { t } = useTranslation(); @@ -29,24 +26,10 @@ export function DeveloperDocs() { const navigate = useNavigate(); const contentRef = useRef(null); const [navOpen, setNavOpen] = useState(false); - const [query, setQuery] = useState(""); const nav = useMemo(() => loadDocsNav(), []); const fallback = useMemo(() => firstDocId(), []); - // Full-text index over every doc's plaintext body (built once). - const index = useMemo(() => { - const labels = new Map(nav.map((s) => [s.id, s.label])); - return allDocs().map((d) => ({ - id: d.id, - title: d.title, - sectionLabel: labels.get(d.section) ?? "", - text: toPlainText(d.markdown), - })); - }, [nav]); - const results = useMemo(() => searchDocs(index, query), [index, query]); - const searching = query.trim().length > 0; - // Deep-link support: the active doc id lives in the URL hash. const hashId = decodeURIComponent(hash.replace(/^#/, "")); const activeId = hashId && loadDoc(hashId) ? hashId : fallback; @@ -61,12 +44,11 @@ export function DeveloperDocs() { [doc], ); - // Navigating closes the mobile drawer, clears the search, and resets the pane. + // Navigating closes the mobile drawer and resets the pane. const onSelect = useCallback( (id: string) => { navigate({ hash: id }); setNavOpen(false); - setQuery(""); }, [navigate], ); @@ -100,19 +82,11 @@ export function DeveloperDocs() { {t("portal.docs.browse")} - {/* Layout columns, not landmarks: the search and the two
    } is no longer a row to a screen reader. That row control is then the - * keyboard path to the same action, so nothing is lost by leaving the row itself inert. - */ - rowsContainControls?: boolean; - /** Rendered in place of the body when there are no rows. */ - empty?: ReactNode; - className?: string; -} - -/** - * Minimal data table primitive. Columns own their own cell renderers, so the - * table stays presentational — callers pre-sort/filter and pass the rows they - * want shown. Rows become focusable buttons-in-disguise when `onRowClick` is - * set. - */ -export function Table({ - columns, - rows, - rowKey, - onRowClick, - isRowInteractive, - rowsContainControls = false, - empty, - className, -}: TableProps) { - const interactive = Boolean(onRowClick); - return ( -
    -
    @1xwyF0kT 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

    Db{N7mM`WF3*S{atXQ$q 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 literal 635305 zcmV)TK(W7xP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 z

    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�sO7Hg>DAZN`o2)iY5DqdzJM9x5OC3zP(e~T^sHUI-jL>6J6$yJAELfxcdu1$dXU`ZjC%xi5M zGzc|p3@AvYr1H)IaEludrWQ&}IREj_r*e>R|0cO}Ap%&z%9}N7OecK@rbvsQEvx6X zGACsC)h&=L&pg)>5Q*%w?ElBxZKdYZN~aX+M5^3(Ob$(CjWN);#@6Hpk*1N7Pfg;Z z<7t^KbjsArBrSC;yh&D;;VoWWjzbaJ*DP;5XAS9n$-C{7i)?Xp>z?&$MCZuuR%CYX z3&5zy^CP?jZFJk++|2rXW}W5^qh@C&hH^Af%WH_w1{mbb0^c(Q&=gZyvAa-* zSd0o2cW^WMasu?^FigE)uWCMK83Bd28r$w?hVw^bvT&&Phi&7iz zKmR~4hY=4yVH#I_mN(RT@#T-8TNbS2KrAi&U?b02Q2$?1!a*$lyBODwa>JAHednx& z?LVUpk{Y-+lI$Ki8wyJXUJQ~NlJ-T*M5}BVFl9`-risQ?Pc3Dnv0Ph8QmkXbtXJt^ zwbOlI%8So3YP<6%p^#I(7Lljnx0%-2Q}c<-KLGDiPgX|%OjLbFC!AccJkJDo?kexb z>3Jh*0S(|iT+UajKxL2j10+RHS=(?latm$e8ut5z4M=81i?}O$?SN|al9a!w)au9~ zIG#u8Rf{n{BkD2}c%UQQH-i_=3gyqc_x6W_fh892SiczJQf6F>C#{@&#&hKeut;?q zY?$h5D0V-S)7s&*!4&s@JcsWPA6$UA>UW8Ie&;5{ZTju6=vsNkVMv9SEI+sFSRreh zPzdMcyh<3b3xBC>4UDHxPhIVUeg9asdmp~5@4EqcEZKR8wR?#lj9&&)yLHuy3sQBa zh80^Zt%Tq&*|uGCf}_6tvs zR2cI`XQuI-E@fUrRDBNWP1UfMtuHXI|5l@W$st(@DDRZG)@uDsS5gWsNAcb8g|v=SW+0WN`E zZ%hr7+!B6St%DZc3ZA#cdQk35yZ0Rx4I5%6Y2klB+bby6!h`EG)t<|}Y6Lrq3v+3S z5-L=A31eV@6_LC-F=~=CP2V&$NXz8isu~@4)2J_9uN7>%`rcV%MiP-c^h18h-8s!m zzIAV)S6H5X$0sUKox!Nbpkb}4sU4w*4qH1p>OYfhW#WeotoSKQv#Jtb-nqvkOu{9R zYc^bHqe@I?chbL1l`1U{JvCXsu-Vp`nVz?K;Zz3aI4b$Sn4hNsox=$>fW(R?;Fq9E zVuMS>YRc&Yyo8;-#KZ8206g#1U0C8*Z~5Lx;%_TV)vbf-W_oXM-F2$&S{^xG{kc)X zDv&0>&Ub9a<%)A_=FUAY)ci8LN%ZqIkCM~LUZg~H|954HHU6}!CN&by{_eA3RE4mp zSdza2Xu0M((0&tvWXb_Wd=TI}LXcJ2Q_P5X@eP}ep4HCllF{vH>Y_IonYNB*dan~x z-oev5&CM_Z-4=c~NvXKk31bL1D_SUCar*FRE|d)lNX+Tkc<+v=*6`PKvwHbG)tMKy z0QQhuy1TTxb24!RpR!r5R|`Vg38Nqdwesbz5wD9!gfy5~jH0S%?wp_vP3SvrB>pUY z;Iou!bF2o=+46GJoCv7Vywp)=oLkyi_&wZZCG>?VhDnC7z~3W+x9;ir zBSXDFdTJd!G*wl)zue

    xK65@}V2$GD1eA60_%NZK@l+4FUp`m+0o~)pOoN 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 O0O<3VOkNwg#r+3R&!6-F literal 4260 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 Q3m1U1hNh=V4BVpr2TTp5$^ZZW literal 8794 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 literal 6246 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 diff --git a/frontend/editor/src-tauri/icons/32x32.png b/frontend/editor/src-tauri/icons/32x32.png index b77a386f81b3bd6b4646b8e84403632edcfefd2e..1844dfceb221370206f933cb78327537fa14893f 100644 GIT binary patch delta 1252 zcmZ{iSx{4F6vsbY5F%-HrdB)F5>Pe~xLF7o1PLXQ6xp#x5rifXDJlq|BFkI`2$()PDeohX9xl1%Ofvz|NS`cE3Xa z=neWF4Wyw37yuz2UqB@0;qke6ywJ|>Dv_82L8uf$P%Z?aDe9wU9+Akmuz+(3gggQP z?eTE9=7fYl`1~w0vpfJU0z;LsT5La@3m_yAK{M^GMVbZgIm77m(9&<*zD!S zMTJB%P+dJF7B@ski#VLOlarMpA)<(g*Uz3Qo0^`MmEEAzr#d@Fo141}3O+9_soL9L z$mJtVO|={jy5aOad&E&ugEckxnaqc2X?!ay$%P9e&CQ=be5eQx?hp!VSgbb+MG2i= zfBrl?(%9IVl$5n^U;Wv$lN}vb92_LEu~lJVLR;HHXXo;upleiB7J67L*WWt;Y@k0{ zMZypoqU}PkWW&Q~p!bWXhx-q)7@RdKws093+9%lo2cES#7F1V$cXC%`HHRjv6I59yygKm#?1100j&mpL{;QM%+?p=`Q?j& zH%CNWyoWQC;_47rohGETc=O`_>&N>D0!-bJujh+WJo=^ zHgzVP;vik>h^*I3&gdzZ_CI!+ae?{tLlGUC(dd^cT L^L7`zg{J)r#&}QU delta 1318 zcmV+>1=;%T39br|8Gi-<0047(dh`GQ1ocToK~#90wUWhVkk&Me$RIPrJno}8XYL&YW`>!;o`lSO@%{ei@tt$8=zjp@w}9E{_kZq^vRyaL z1`ALD2)tUe1fZ)@_9RU6`y+#czY?1voi#OIxmEQif+kE8|EIa6WHE7F|N6+?yFZH` ze7?_06y>+1XJY@aO6oUpHv zKw=P!&WSB8A%E&A>_x$aV0d(D3h(aO1K)uIh|kVKDDz9veM8sDb7-ZdX%WOEHWfip z#YPL0&4og!J#+{=Po09dq5?)Tnf92T(PJ^FE*Ca8HDS%Vbubc%`TKyNYA@KNqCih1 zpvU9*^7LtJ+P9BAwoE4k|MQWxT+%d;sweoE3xa` zIaJrxvDb>G&BKL2JQ6|0h7EYXxf!m~Qcex*`$Y?os)}$hi0W%F-p!Fo#$wPso{Vd%`G6T91R_&Y*tB;qc64;WU0%*`N{6ZI{2Ub< zrDbz-Gk>bLZsin^04Fj<2@p~;fENC+wG|(?xAS$XW`f%a`NvgOh7Vd=u)eMi@o<=< zLE=e7Q350tTRhGSd~xCg-fL`xWf+{xm%Vb1-`uhV?y@o@B9U}jmNRv^Vxz6qc=jw_ zuc_fk(DhtKg0+&8oL(>BAPYS5>>1wh`_Xja0)NBF^PF%3Qoi%_+%YxN#qi`LZ=X+( zA5V2Js`Qt-TSmB)IR$2mh`Qv85cuTSG1RrTrv8yyXAZbi$w6u=z;AfRZaDq{Lz845!yuTkVuQz)H(q@M=+iZ8h^!Lxe0D{BA3?=~! z;2sa6!5{{^yD>dF3iqm2sV%&cR&3gxk9z-;{(j6neG0|pO78#%PQ8C>WCVB4pXZ+7 zDlg9nFGzsX`C>COcyj9&=Y2swEemu+Eq{8fw-*mCUE)Zp)DyD6^AkV|+i7`x?HY!! zUPV=HEqlJW*i=o!gFpbIH*cn=<8rTQGB-Qr6)klC;zj&&@F3= zz8;sNd=xhfLrAIc%vY40TZk%taZfJ@Y*~g|QIv#X+>|Ep9bKf)CCfAoXPV^-=}O_m zqGMvqwxOZlwzLil>h($ diff --git a/frontend/editor/src-tauri/icons/64x64.png b/frontend/editor/src-tauri/icons/64x64.png index efb2a136f330acf01d3d035251bbf834e24dfe08..d758a26bf3a22e891351385b7d4b8b652a4e1afa 100644 GIT binary patch delta 1897 zcmZ`)c{J2(AO8(g$bE6S_eS>?DSMbjh6zKL=#^d4t?Zf+E`?XNeq(9uOo~t$%Oxgc z9~s#hm1W8pp&EP0*cma#{k`|E_n-Hi=bZ2N^L&=)oacPc^Nh%FCCGB1FaQA0Xwgeh zf#XfAOaP!dU7YWJ2*L`U7WP&E5UvaW#5e%hg|>+E01$!zfJHX|z-9x$(ZHuob_M_- zWN3TI(F_uRIGq3K(+|*SG7|X!g(9O+X-MP&0HkSaLy!sp=}2TM0+EJ5r2f-@Y5yo7 z0HV>5G*w$0qBC`LpdMO}pdb_~4}*bZ>1Z^>CaS8Eb##(7G#+3ujggV*&>3gXCa9=n zU@(gxKQ4@nBq%GVXlX$q8T$J7Po6Atc3z&GJUBR*U@+2kbXrqV3NK$yQd5hQm#0NW z?t)ueg8luew{J6XxbDo%@z<{@Ha1D>>WOM;_9qhihtBRaHqE8UjB5 zKp-gd^kmo74ZVEHq*8}#YqM~;c@}GbZ;uucF;ZXup}hQkc{%CSsgfHvHkX!?)z!(m zy6vf{Y>29;Xo`+bR8h$>HD%=Hjx>M`?_a#=FDm-W*_l~Z_MS=|ZEAupryP%uKYjX% zg~j~vaEhj8V^q|~#>N+T{Lb3ibYCAyQ87bLuj}DMdTcDGySuNTV7RVMAP{gTCi)5s zA7Zf+Jw5sM_WOMP#^NHgwDdz|<$XoPnxLSa_4U@Iq~tSaW|_>5g@u)=DR7I+T^t={ z)zr-N_m94Mm1k>9_44BH?tba$s0s+!US1xmtbFd`^5W*r)#>SX6iTjz#kaAs2fDfg zB_(@2-lx{qWe(^2=H}=2_NJJak2KmN1A|U7IaN!mCo8L&M5+r79jdBgRacj~yAKo> zKee;#f8#5Wp@l zg^Bz7ME?0*!I5FRGQq+L;f)qZ1@o{pH$`KHBV8aM5`59hOk`2?Uq6Uz{-O{S1C6uD zMN<<;=g#7h1y>n!S&`Ltaf8A&_|0kS8sAWdT;rvoIujbmjG3M9iL2|?`guzC{H?+Z zKZX;&HHapOI90rM40mO-zwowbTZi1!tKLX>xhPXOywNaSN}qw~Nyk{H^6T~CR+=R) z^}bFo|9B-dtPm%4K7_!k=Wv=7Vsg{yjv!o@JOoul_8d+S?dNB0p1(uio) zLBn0=OR7e~cgBr;KGz=oi(y6V8XKeBttk?9JE{O4TmB@n`jaf6(bd!x5l&ZyE&F^B zzjGZ0L~}n;EVhePVOl$VwM4c3VES+{o|z{@Zb5ezb<|I^Po3Sbeqs zOG`1#BXFHM%e@wj@d1!Ed&hdg3DN7%VSn(JLIz?bFiUOMtO5F9K@xq&JH9rxc#}x1^*QQc0onN~pH; z_;Yn(5iDN@^x_KIQVnNId6_+OV7u@;Fz{pm1b>+gm%Lj5gEey@ACV1SDsrb zfGhL{=bYp*4`ZQ5X^=2?SJ;;Ie#OivNiEu^F>*oiN4T&p-X5pdFesNzDk)1ysD^og z{DrUNjqPWE*nky@R4wM^(8?dXsQ$^rkEr##uIyZu*bL|!FsU)?Z;WfpX`iZho){9l zY{vlc4E4DRA{@N*lf`YC5XE{^$$@qkoNYd|BO}1tNu`45G#lQgdQj%r6?oE>8}>VU z%atjF6B&G*`9b}DXr^4$FIwYE`v7Xp%iT9 z9PH&D?CpW|yyFd30EIy5Y9jPBk!VLG3X4Qwb#yck2rTp$g&)}euLJ8J;N^2O?Ef2P SRLqwl18~vI+O+b#Tg=}|yPZ}5 literal 2519 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 XjP(dVh=+RR00000NkvXXu0mjftwAt= diff --git a/frontend/editor/src-tauri/icons/Square142x142Logo.png b/frontend/editor/src-tauri/icons/Square142x142Logo.png index 8c9659228ddf1e9f63e0278ef6e08be4fdf0c2fe..27e766be49cefcf43c37acb94c50999cbd514475 100644 GIT binary patch literal 3140 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 literal 5006 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 CY7902 literal 5218 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

    !7&o`7zv!y~Mh& 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 U_;vQmGME5?8=74z(??+b51BGl`~Uy| literal 10678 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 diff --git a/frontend/editor/src-tauri/icons/Square44x44Logo.png b/frontend/editor/src-tauri/icons/Square44x44Logo.png index 51f826b8912cbf57dc2db6f76954e1443dea2ea7..a7e08945ed1a70096ba94777139826cb68b5b4ed 100644 GIT binary patch literal 1493 zcmZ`&c{J2(82(W=Qf{b_?vb^z`^{pTlypW$_VqH3bIct>27eE+=9M{;tomX=bI0suhT28(fl zvfkm5JOV{+Tm=$JM*}SFEdk&K<-`I07YM5murBrh5Oom%ViN&i54wu|3IKnC0I=)} z0O%(Gpl~~{+1VTb#E&}JxmiJk2M-=R)YGFP5L6^G9S*Mm05S+>A(0d~94g3gI28n; zIun6_AVh$eP=t0i3I)|qjf|ic1=1oADLOhNEv*tSuXIRgXh=3N$Uvde3=EPrG=|vh zF9QRQ&CPQxEhz{@hLI5o2Fo-v8*gkBiA21&Zz(9$BN&WsZQWN|TIBAYqOJYt;>BDn z_Up)qP$1y-^ptpdeqb;P937L?)ysT*cGlMqc6U>Cby)!cqjhz|FJ3%zc22x-p*b!t zO<%t;CT4)i>}N7ZYHNFoi$%i2MKv~lm6bJCUq4t`Ily8~G&hs9wdq*wyTZbex;mPL zg0CFMp`2`g3(vaB%RcyL+smVW_IAAtq*bb5nS5@MCdtki{Bgv%7L~Hda>p z8H^NN-Qnu$jpgNR3yZJA!vcX|xTa=%ZEdopWumF6z{#n?&+q%(Tya8b=42To)Oc3XRXmXsEiHfyd|B+U{&@yr$8X#>NU=T$0q( zUZ$lr#m2I4-TK(kk*cG^&B&l#zWh&862sT`k)~#*sc9e)c4)5C5iSm{fVcu_OnAio zv&(_-?4pn~BNX7Ru782rVt?3LWAvIohBF}`5n*F*C9y0ieNtLc{;e_$vQw1}#?sC7 z9c^USb?E9DiSKVzokCqy0&fH|=)eax7m`Ylo=Vp^pr3>5D5B}`Vp4e|shOg?vkjt> ziO@n)Z%?&F1PA%-o}jI-FZxV z+1C5yu(QbtAbTYEuGmMdDGPg6B{R8RasT|N5j*Uf=@h3%alHTJ;PphWxZuFb*?BH4 z1h>HnnGyFE!~cC_M*e4gWCqw75t?%~Q^w!i=h8AQU+4J#Zjz78Ab@w~^OxMYvU2jd z%X?R)#JFr>T!*Tr-7B?*mUCNxM)=|kqrzKGe!D3de-xZqODC%ZQ#MC`{VnnCVEnP< zE;%X)Jtgd9@Wihm3M8|tx3!RB>oR&gg0q@@(;Vz}uT{B3{CSh&@#|etVe19M*d2vnrRgpE^WoMB zk*2sp8&6RyQ?b(i6gd)gT+f~x-e`iHN}Z+4wTNPC9W+lU$0hTvDG9RElc(V$ICS=> zC1fbyj7v@2<1XND%8l|0-)|Ta6KTJHt2&b1=)r35n=q!!%6JTSnF#FmFkUa^TJu?W zPo>W{SAFtt5nN9#36d#Af1RCo3|d7_5d+qJ{es%=fBRL%_QY)4cJCWy?^FqOZvi1x zxm}*KvRE~{RC$h;)tR@TkQ4vCo9Jz9_I{lX(rPHNW0*;CH4N6EkXXrC(C;O(=GqI$ z8VdE7&BY)$YerakM)=_)h-ktcB9s6Gh%nX%P4(eOH#h;`8hXjC)l>>%-%{TsE()PyW delta 1758 zcmV<41|j*?3*!xt8Gi-<0016@S^)q62C7L!K~#90#hG1b6jv0-|M$+!Zn8-vZBkQO ztc7cr~UpDQ`ik3xQDR;O-kY`fNsV7SPrL=7G}EYLBMXYeKve zwk()R;Z>R@1Pm#O<9^6AcWiHOKS!+W!SDS3N}sMb1x*uS%Q7K^6tB*^Y>IxAvJ5FT zucl!%5~=#Ct$(dqd|z7nh9yL&CPY!#vMeElh76dH63VjD{+v?EG^CWKQhidXg)#!{ z@M_wd8G{vqVZ&o%DUavh_*ilCnpDM8XB-*%^F0;#}kgHn+OI1$e%qM%eHJmPGKReaM)?6 z(%sIr1Uvrh84BjjL&bpuD4IV%8f;FABYgu11p=7m_oHO>YGmi&#e7t8*JdH{Hz$>>$N#^;9s{6 zzQRI;Mn{uMz%^hy6XX^Zq3XyH4lY_|Ckfa_X>D$KIZ9Tog5mQaGB%bLET)=!3F`Ud z$9UWC#}`daQCQPV3Tv4r2V+@H4F@5mxpZOE_HaD7`unkX!v-z}9-oh^6BP>Q4+;MB zdVg^-7FJbpabVd@0X8jQJGiKN5$K1TH}n5_@bavlKKP(7`x}tbRBL zPv~LtM!YRdC%WxyAOs%YzKwrkp4AM)5rF2*L@znR8rj(#*lnjzWAN@>WaZ^?RZJf^P2=uN2rjwzu3f`OUmxdfM?F$E2XM#s z~8l;8Kd=Xcev>J&NH+kKoD8of2%?9%c<{8h;*kbf9_b zR^B0#clk-pORUkfs2c|E{PGL_Y-wTbrW#z+0%q^UN^|G=^Z2Q@mSHGo(w@xm#tegl z>5o`&dC9A>Ytykwf#X|sO~cQ1b@=u8ab(Sy5#6dgc*?F^!Qo;2-q?sI-QApDrwf`A zunkK)-Ind!@%N=m{A7kaZh!BN|2N(=SZsFdYix`L7wOf7;D1VBLLBt_yEP$- z&1fQx=4BzG`!-FRv|}S3lQ+kQhq=sV2-MPq5T;T+QYm%LkW#3LnLoD7{BjZi`WQ7k z8H5F~#Td^?9jFhQ=D&3CK?ficp;NVZv!5AQlh}xnSml@P?E?U|c{NSY5fcsIBd7Gf z)qPN~W6jvH7sc5&Vpo-PSV!kyfpB=O5aNEeu4{S*4rq|35d*Oh6R{B^u}V4xrDIV# zX&ep)mxL|rTL{sWiL;M1jJ7Xb#6nEOMvTPzAE>RiWLD(l0ssI207*qoM6N<$f^ttp AT>t<8 diff --git a/frontend/editor/src-tauri/icons/Square71x71Logo.png b/frontend/editor/src-tauri/icons/Square71x71Logo.png index d715318c5d9324fad9033f3e7abad6842b993a5b..1d64d7ad39f886c6ec3622f7ec7edc01e2a1789f 100644 GIT binary patch literal 2305 zcmZ`*c|25m8$Xd$k{7o^HDg{Qkm-Nsrw^<+(aajq@)DSn53qbtfA5M{CTF45%K(a%D(<2b@ljj=Tdca z=7)#(_V#Gn+C)XgBor$8%$dCF*Vh*pH`#>P#_$+U|XNy^I6($b8Oke!VUE{C)I z^Jk8gRc~=|Z)s_=rY1N`91PZ(o6BXhOFcXWD=P#2np0ASYijyROZ#8Eh&g-qxufIuj~{d_w*ASI z!RqS1va%>CsrvBnJUhFdqN2g7DlV6spr|-hU7dnPe{N~13kw@zFk)n6o>*HijEuZb zO&w-1VxiCyH#ZKOon>P3Ix;d1gDG%w+FD&r(b6jS_8zLLYNSwxYHE6li=$;^Dgy%8 zySr}_5@vdOqNJsB?d(>*evOxxZ%I#QvseQa6}wwoy=7&cd3kdK1KFmg>q|?^pFZ`M zmk-z0GAWceS=ll#uVMn>b8G9)+S+(SLr-C0&gIJ+OG~YEdZwY__lb!{GMT8LFx}bN zU0BF?_;8ZREW+axfq{mYn4R_YhUn-Ibb9r@d%3o@{S_63I9!^p z?pq?U!q>Op=1r=mW_wOfsk?hKjTS2_`;J5+D=R;DaGj=kr9?X3HQ-s42ZQJD@e7$?m^W*||$)ldjo}F)Cb4y%s$L6>k?%)wm z2i~31gTlo#Jn$?yS3pn1eAb>NqgPmT2A5YSyidn71?8C|)wVcD-CE4D{4cb74Lt0R zk3;-jhiFBkwA$p0ciJxZQ%(v=Nj(54TATGUy!jzw2?>-e9pyMFt?Mm|0c(~0ewAxK zZ@~#OaY+^$XFpxB!scH%DqyyQTt~xgFzuRIZLS@tE*|6o%}G050_vz441muz*F}q? zqXR7Iyx&gD7PEZmC`AtKhPF_LQ-#$DUL{$pc}BFrw@9NyS5N(Ie|w6WbxGVL{9EP6 zc3-=L1Ll~a+I6?;4t;bIzP^Sa|cj29J_kCFuk40%^xttn3z zLLo7=6OJ!F@EgjP1c3`dPLqYyMo83b)zChCr8A;@+naUZ$4mLz(|Mr?K8~(>I!@E9;e|uivHcaU8C7Ucqk#8O`99=@-I!Z)}#B`A75M^ zmCOqEyfD+P&4N5~CCusCg^ONu9=Py`)w5Q4#L}$!l>P!_gm3Eu>^uej0I^LS7*$-< zEf3AnXJSil2dSuFAx`dzB!d7}?1|jQ!3J$rxQzdHudSFuZ#e(WtEGo9ZP$b}WzF7= zjyP+yP7)CXJ2e#sMH!95>D$3nmx!kl<)r(6iCE+?^N;3gOgf0rvTd%m-r)C9+o`qu z9@RTO+2!n@w$k98Qto1n#;yM@CDmEJ)s9)Uv4>=@#^NKO67Eknaz$*}bcMP|QSSW? z*u?_pEttVEb5H-(>C=)aUG(Cfkwyb*lnwg6L~uP!`AhL+w+;r%u()z}mAq-hoIcH= z&ai1GTO;4>?w)>;Csv@c?&HGzC@Cbtz2$LUOj?vX$ug)iQ~Q{GvYOS(t4PPj3aNjW zvUbpKN8^MM4T_xGGu&nkK8A$L$UzhW3r0o6(1=g4t{S27c>!_o` zRV@U@>Iq2VDP=_L3qkg?jEV-E72T3YcdCAyUzLmiD&yhNPRW$jK3&GB3gJf#2lw9iM; zv?BL|cdyI$3D*lTUUO&(!V{sm*)7acGh$?PoaMd{b(W>V`x2S^0U|bR@$@P54l|kK z{AZ|F+V4zu&C-fU!nLd9FRp%Vm;@F9*;=R&gWjm_ZxlCbW8JftjNDprkErU>fvGe- zVtx(iz91DQU?#q$K|00bC;XxM)9oK`)8|z9&>?P|oCY2|m*C=Ux@4T$_uW{ji_?8S zsLK^EVD?s~lWXVyBh}Vu7oWMlSpLRstVUO5bCJvc8=p(TdZr)Xf-uOI2;Xr;vek&`e*qc!N=Ft(=F)#6Q*It%OC+T MF|sf$*LRNi2eyMlI{*Lx literal 2797 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 diff --git a/frontend/editor/src-tauri/icons/Square89x89Logo.png b/frontend/editor/src-tauri/icons/Square89x89Logo.png index 8a1fa5068ac0ba5530c29ba57fb9d3b33a894e8c..7017222db08536851835b8bc27a08c736ca57cea 100644 GIT binary patch literal 2563 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 KYrN?7+kXK38QnYp literal 3313 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 delta 1932 zcmV;72Xpv`4v!Cz8Gi-<00168h_L_w2UbZ$K~#90&01+}R8@go*$jbyxy7f-E+QkzPktjP=A0B`~qyQsCZfjmMWzl zRlrzR@y7!HuPGq}Bya&j>{phx_4THvgZ+IM$v5k!PcIo|+aE|FULmh7rH}wIY)lFP zLkRNAc9r@xVi;@RJ#wUjF{}gR*oul^k5c=?nl>$JS+0uDfE)&qxX~0Dgb*c$A-Zh) zcfGgwi7^u<^nb{A0XOQp9}|cU%d#MZ6hdgj5CoZ^2^N6ph{a+>x<0MXG&jCbQ{#xu zlP1j#==$DX$8iz>3T{G5np&FQT))cEg^;%E&Xr2amm!EMLt{eI5bN%SYguu2rZq?m z`oA<8nxp{XRr>q5<+>cQ{JTBS8L4ULzH$Zk&Yq3iCVx(Z)!j|G;vYT7fwJwJz>v?D z>(ZOYq!eOIp-U_|Lk>sV+AwYDQarzPD}tq^a2>}pDnd$FJv|5%7b84s6kOZB5x5}V z34?SwSqoRJhM*LTMlte^JMhTzC{j}ts4AtbOo?X%g3{=rXP)6Pc5iRSAo2nv6{)>aiHsT? zMSpMd;>FA*D$$C-`;a>SKzvB;HIB?ED#F8+mAG@p3??;wD$Kn}nhtU;@KsU{}5*|t*G8~0bejORf+BE?8MZ?ix~tjf+z|(g6O$=6{q&? zL;HmbaT(&_oT7=-H0Xf<{@lGA*IHWmeJVg6k~HpgF0vtkf^CL4Dy=3@$0W~zP_IOj+gl|HrcVW=7SKpeCiZCLo=)yhA#*s6hhbK%c$SJ9g#C< z5G*c^ukUjKl1nO4iN4U#z(BNM@cOe9gLv&hGXBcx)2RF63&gs*c;mqjL_vTgL696j z`|GcWUb+N56!MM&jUf=B5YGPc3xAI9+Qr{N=|u`49m^|_B%g@XXAU0ZtsHj7CzYsr zQOY&#*ug+3wFpQiDV-M}$#fwQSI?ehK2Tcu^1*SShr_Uc`{lub@obc-rG|nt8#;Stbbt?OD-=TVri4S`HsZfKn;(ELYRB zC3Vxc-{OZgYq(1XOG?n&(SP9yf@YogQ&(M$NOLo9gXRN3#z2y!rWDw>aU)k5lJwm8 zd?G>lPtFqv0%PVGMA|^eQ9ZHD-LYZ?H@eWsksP6vS}Z|ont4j2<2!e9TKV;rf&t-L z9|+*$i4)jWRfQ|f%?Ov3ai8#Pa;C|9w#UBv4$TJ+K%=Hd8w~{j;eQPzF{4yvPVU)* zAJ?x(Pb3mQpz#4gvrZC)m1{%8e@(7HAd*1XUksvLDP`)v`U-p3u4UQ40?r2n$!$`s zjWsp=N@H>@t!bFjNocDv}XdGR7U!)F&>q^9lvV37ODk!{_6EaAHE$*QAA581A}rPweurBn>L*w9_VQBcY*(sj)RK7V}|TB zQev7Wvq33h=rqS!QzPTM|k`+fJlNf-uPb1Ji}^+Jf(G`dkRjITL4K)2(% zTfR$tZQ1k#? SCQSeU002ovP6b4+LSTX@#HMcm diff --git a/frontend/editor/src-tauri/icons/android-chrome-192x192.png b/frontend/editor/src-tauri/icons/android-chrome-192x192.png index 217812412012b85eff2056a3ab7bb7b584b840f4..55c165df4b65eaf144485b54c093a2eec23c508b 100644 GIT binary patch literal 5599 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 literal 6246 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 diff --git a/frontend/editor/src-tauri/icons/android-chrome-512x512.png b/frontend/editor/src-tauri/icons/android-chrome-512x512.png index 8abdd8ca3a5ce9b38c8dacf8da23b33579d948f1..1b043a72ecf61239602c8d7ef580c661256191ad 100644 GIT binary patch literal 17810 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 diff --git a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png index ae8588d81cee0db2fc01015a9aac33d8c839e53d..bb04f26bb74391b479ad5698c259a5159e57e628 100644 GIT binary patch delta 1651 zcmZ{jZ9J6u8pm(R8mTR*t(J3|SdU{E_dUimyJAE}6J?CW+S*1%btr3SY3UY22E!;L zPkA^V@-R(fj2#tLQg#_(^0a4CYc_=@DS4PNGtPa^oAc_txvtOecU|BAo9p-IZB=dN z?t%3I0Pyl`c0}67NU|Rp0GjiRH4Fnpqfb$2egJUU8URvQ0H8*!loHGH8gob`nD%o~+WmM{5eLZV0yjPN(tXf)9^!L{@7$eQiPj23v z`t7&YganyDu&&ch4-ZR{k|yrmlkxeBb8~~YZ>OWtLk$fB)z#Pc?_ZpqEu~OqMn{+C z=YFJ$pc1v7bs=4{Y z++3|j^IoAq?)Q+x854`AhlXyDNE(&uL0VddrDan@#I>)!;-5WRNuy<0Sk#1sNUN$| z%VhZkLRWTnUwL^6g_4cM_Lh_&=PrR=TnZp)q_;Z=36EJE%|8&>Ky7%X(`~OY>DS&? z9nv)EX4R$I{!PW7>-j6VNag^&aH@|NvC^=13=xKLzJA_@bEqwwjY+)4RV0J@X1-oz zI;EF4rV8F3u)}b{$KJ8N{#|MpaEcfh4*tQ+r0>9xHyTg_rL|jNV+<&YY8HWE;*O|b z`HI`xo7B*djAGvxN6nwEm92H=J?hvB7y(q74651m!D@hVG;D*#11mH{4gDug12-Hl z2rZt{URRp89M3G&Aq>*SsKmpmv&HO$Ch$@rA1X~>}9#7wHV3s$QQl_#zhd2lCxUZ$ZPmV;)N z*RTh!6`{8zr{#Bui9^hxzb_DbHr`4^fr_Ci%ks*DM})<*(-|FDx*o%1K$=zkNYD8y zx_3evV6xrxvi_B_fp*)g#XetF#iOhs6-5%1^dWyD*y+ceTp$CyXCm(4Iy zr&?bpz+}Z6zLAM-jIMiu7}g!z+~?b4q?DZ@K6I1ue&1%?zI^pc!DsyXJ?3+N(%VS$ zoYZE)KW;_+z<9jG+A)POYfap}?UVRr*U(u@#m)=F0Johep-J7v7qTa$T@^8pSB`MmwwKvd44)RMn2jbYjyHnxX~pPyH- zxEBu9)rsrVP3iVDh! zr)AZH^DefZ*>&E{dgz|3e@DokRNDeT6krwS9TIn%5qE}kD)tQ001yWfus9;t8KOIb vBxf86kH_F}Bpi-+d7Xy*xc>ps7f+vyNc{i6D;uAA1OR-!{k;U9CsY3oV$%{~ delta 1938 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 literal 5332 zcmYiqbyQUEa;c?Z7Xj&5I%RQzrNo7$R&q%Nq@Un@kX*WJX^<{4h($tL@FxON zQX)!7!&`sv{qeqY?wvF9eRC(ixie=b)<92_mWquE007WxYa!7DSn~Iw021E4`^@YB z0AsE;5@GC@x0@f5Z2T#GaNJ)d;6>7hTVjT0QXtvTZ&I&_V+ZhLQLb75&ZkB!=qm>o z;aVmgtxgvWtyy7XW@vp?D^|ElJvvn5iD65n0cO9?*X9Haf?z_Ip z1oSJ)zY|7sINz2<&b6TM@^5FyjfT5BqA>{vHxyR1$4(7NF+>{{Z)E*`{mJ(6N%Xq` znEf>4ym(MhscC~GYEcJ#C3&iD@(CUdf9Z9H3>2dYqm5Mr0%Yxf=3;}yIKi+0kQ65Q zQ3AQT5kLNPRbd6GHvP^{M+v?`m;ZG8 zIFMeTFs_1?b4*?nxiAC6IJ5C{#1zewV#Y_~2V{L8m342+@@%!J+_vlgg|Ss~<4Gke zOI!ZXNrE{nFOEU~8VL0QVl-0fZxnrd`sRJpuI7duENJT4>?jrH@Sd{Q`MhB%*C%j@ zJ&(rb{I_7W`lAN)ejNpo9mzdfwPv`-8sHjwi`il_CI1O=ie@R>>}=EKL~+kK7JKm= zIqisdo6mj06pGq~2*Qv(qf*WpK~=3CCzFN2zYtz9woi|rJh`8QIOMq)pLg$mGXq-^ znLL7i%y6gx7{4e)YlJYs+edV#b>L%(9S*t=JDkSJk_0wYt$Nq8jp=(axR)-CZuit+ z-DfH&Q~`X$w8}P18}Eyk*x?HQJ%V$ot9cjs0;k5(?q-3goU!69(H;dU_JbO+W2WtQ z%hvc)EXsDS)ux|)ZIeL4x`LTeC>|RaCZC~?MNJ%R6fdYV(M?v8N%BDU@l2@$&flZi z0UY^Q!E`$XlmGqE6eJ@fk>gZr_xnd#=5#i475z-gS^@4*a{B7Ak<>!&ljM zbvH1NBwgsNO|32$?5y4@F2hCjE3q<$!wsLF<=N@xA``$*wv4>bXDmsI=*hk@nV96F ze1cVL+{8fXO$EOsp)|@r;kmgxJ9}UBO%wrROw1{C#)%yAUFOq*6I1yQ2w!T$h}gxm z@XabwD4t?4@Q7;Xe!I3*v}C_LA0E?s*tg3g$qlQd09C|EOPU}`v3y{QewIeTtR0FH zD>S;C=%RFh#bmk?U9S|oEjwAaKof~#l)5>P*Y?x?zmmm0yj*V39XD4}TU0vu}Va(nBtXIAm`yTtP zkW;|q5+km`k%-q-K9JeHwmLi{@W|xkY_~mv2?C0RwrA1-{bHuB_YrO9CB#s(xxx~4 z-H43aPBt??{*l^Q&n&Kd&5@cZsZDO1xC_Rav#)VFH&kl32F1y(@6+^(O03g=adVUM z|7}MEsn|^M`kmtnn)2UH(HgrGE)Icia5`VJ;m#7%Jz-4kGIDBVwKTW}^GP~hFAg@J zWQ{6{H+ALe9n^A3tR%8zX!)kut4BY%lhQE zm0y6vQ@mUxyP>gRidnqGoC&3Y!3uwxl*16iGW`@*b5^76gZZ?N7EF%1y?FcTytvlG zTC1&vaZQN{Bmaxs|o>l@Xq7J(cZdrmNP#MV= zVyKTFD2mf`!X0y@?9RNw*cL-+<&O;#*tIV7owO`GJZ3ijrtPTy3rILaDh2A%J@k{D zR{@NjX5$64Jsv$wUvhGguOJbOnEU1;r*jnr;o##1@;ru_DhH*%zS@rncJE=*xZKnv z)@ZC1ri|n#v(t}A#R&@U&EI@;-AkPr!4L~>)LzB~MYvl>29* z#w~JzCPZbo_G8pgLY@q@`|C?zs7!nj5(h))Fk;N?t-gG*xlp5juX4+r$HyS`wKrwJ zg7=MtEO{eySJshH1ytDQ{Gzft-4j!z7e`_pZg?8`2nO#*K0>xO-HF{O*eD%@DKT@3y5!A z$0+`Erbm5X7=9>!+vdWr(OAg+F<;ui71fZBi9>aseDDL~(uw?5V}>a042xuCZ5hUK+!6K+~%1?8?Om8J7 z>|!uAtG=FKU4B<*N2ID_8JP9Ed+9xGU-S?w*4fi^?2($M)fO>Opam`MHb4dD9bv=5 zQ~yieRT^ejnzX-oN75^l@PUnR+>CI%m=tQ1)L4t4JS*}PWS3UmqL-CHXzwkSVA8)R z&X1frgT`& z8QL#w?7?RL?lSQHglZs{{%R0LHYul!|M_O>E(jeS@Oh@{O-3GDf$W1jmH^y$f4dqgHqWgM$ zzC=yoAil9zsdb-KYQ(cJ5~P|M0SlbswL{82V*y|=_d}n znaD+^%n=qA$bID+bwaQjt!U2)(%8EL?&8x!MUnVY|+B0OW0sV#+Ifm%R(j%4e z*n*U{LHHaB;kPv-a^iaK+gOu{1$oXXOF_WxJu>>ykYm^c46D;?MSZgi^dRH&5lm-a zeEC#gC2CydOlwUuN?({YQfa-09Y@(cS080R-yVN-6_n?+h;t;C^45ObBq?&d+CRwM zm#r2<8tSdn_AXM9jzvJgZYOP_p$91ByGz_$#_v&B`1TZ1=zRistR-m*6 z6d)~f1xW)__B}iNF5s}*`y@A2t}T_uH_W3(4M)5Tran^)`u=ztky--Ts^u}5CBDnZ zUU;=*U3}F#_pZyE64IguJR)8WfWQwzEEt5X!z%!1_zz7BT26Z}QjUkhjxXn;244F`j< z94eB&khJ%9VxR`ZqU3c8_o~x;TP!m^O?3G;%E z4Gfxm#28bU#BB_w(h0ehP*dtI*6OY7%8@4#O+W#{1@yrGY}Y*fIjukcd?KCTqMFF$ zI40o*M7O@duo^zhA(3Q&iRtOp2XLiz58H~ z(xAzs5ReKqQwT|h8X6#&^k{eH;*C>l5!z;G9^cZu!XOD3GZf2>4Gp)>Mw1%k$I?xb zRAHHEP7zRdXtFzAE2@}a37R`zG0|Lv_k=<<+z`iq^hql$i%3;;UZKgS29sPKnOqT` zI*fwS?wubL4z&EZ`EQxw963$m08c|tW17eX$Yovs=4Vapu4RKpG5I9P#10fU%=+v( zakXIP40p?G&#SY5cIWq+1jVRiFpI(W>o=j2qN#0pOTr@HAr}-nZ5>x8b4iOZa7G;d z98qPuz2>C7U_Y8=&RXoC@SwibL2(bLiAVu|Pfz8uXnVJGycZL9b-7T$5Nn1?bYGwf zv<_LQD@&;*WZ9`+=48LXXL1)?`oG?KeaB$QX~olMR0|&Wl$TIFs4m@RZTfbY{F6G3 zjMcU|ylCgwVyZxiP60=-2%mk0S5l+!gJh}V0*NSMq(}UXee$I|6>d6@FQfuJ0b%J{ zAIN#df|8!`K8hsky-iNvdwscbvn25qtX2E9aSAlEVz2{5C5fq$*rVvxUktvvc)EUz z_r-E}X!19#%S~y?`vg`-El^LGK;f@dlbUY}0{4){;`{Dxw9?vmu2t0!6psy5RhBGX zv-rL~WWXNniakt|*b{v6S0bsju*>6cAC=3hSCZ?OXPs57LqOB-|1z;f8uy=lw@->S z!Avdol;_4>PS{&gu!d2iN(`NqL%8A^y7d$&2pO6{MEoKbA-?(*b<<{Kf8EYzK%v$Z zbXG~wdtZWZF=wg2;jRJOBAA~2xUYyF zCS#bQwSSz{4OGQIQ>PI`gg_urPa=%vn;Y9qk$G6|!-r|wh^@b5mAhIs^d8DrC+kK1 zO?PFhSKJu_%5DmPa#tL=J=(Yands`B)b|r zX7lNbl4*_nyn|b?FEdIXEgg^g1R|4>>9=R>FMe}hQ|L)onWFZ2{%x5Q?juRz(U?-B z`5$T%>LG%4YaD*(ZXJFh(k(-wu*yD6+&kF zLfsW?-_Hh}qwM#Op>OBi@KATY#X6=&Qi3LIm9qrKXB{M3LusD?LbN`&kzfJ}#`Q(S zRc8ndoW$i*MT`vK?6p<*pLu{qoDZk`hNncZ@ae;ThMB+$-Fq{GD-mtBmfqsCasOeb3y}d^9UJTF$Y9~SH-B*VaIB1aP6BnI2=x+ zp{C|Zp-+R(CuAvC9=%0^%#JY?cl}HWnhr^modaaHm#ExP;loGMaR!i?<$qt2k&!tY z?NV^s`jC=jc{5gN?$Lc_NKPjlQJ#$P>YkBGa2)i1iU__O;j-WI7}})J5Ymx)Ea%ad zX<||*E|(ym$(VprX59E%Ev4H$)bz06?UT6(E(^}3OfZrzG=8&ndwb|{l*Yg5?d&RAr(qrq} z2ga#i7#ME%(?MH`ii;hrtvJ(J924ZbEX{D6*(IA%fqyJ978#+9Q!w2#0Mk2R9|60F z{U{ix5bHeR2T1Lq2CnA%xSWjcklP<7YNJ%BrK2a+hWiTBt*$kp^eER6VdDf7<}vzG ze^&haviNxtG~P0*qT31mDC|~4eL*}RUblZ|rA>FRvQ0;VGTJP!yOol($kA)jnP~L- z*!+ij!0#II;tKa$g?>adms90@8bQ;rgzG7A#RQd3?=$#RYLSI1bG396D4vK2cV-e4+rkfZy5vjh{6F^l?hdW>uP@$(>eMUr({>YCN>x zCGS8a6t1Ky^D>V*(Dg=^@+nBbrHl_fn4h)%;T=C~DLcCl3U?#0=6M4m16#KV~T?>3fbPLGIGP ym-buVN3;i;CVg$|%3<4mTvwDzSPYLQu1UJM*>xuBmtqLhN`SVy9+6A0_bEEkvS5X6HZ#4jTd@<1>jf;i63Y&$zH9-o836u7yy zCnx7RIpt!pTvyjDTia`HZu66qa}yI;Ha6Mz_QhUaH6bB6j*j^t$U>uQLPHm(r`Oll z|LE(>$K%};)C zExd4HthH5k`}TNSTQ&wWDirE88l6seH#K##y?unwA8Bfu`1$7|50BZ=QSI7Vj)Mcw z!{c#L(S$@Y(%hV}cW-T2*hi(3ZEs&rr4BVTu=c=vBq=GXadKm&s@+S2m)#*OJ; ze{D-llnDguI^E33h$K0A^6p(3pT9IeKXmI>1{yuw*f>~Ib8X+grMbB>3T1X|Ya9-v16a!zn_#yL~(IztE=+P&Zh#ws7S=xyLX_nQlnO{EG&$PL<=uo@b>TTD=zNH z%!HrR*N?ZhiZ5Qwc5qNFEy+7Nv|8=5Qu(B=uFTKx?bOsG9Wuy~M{ zr`2fQDHO>59&kA0V)4xI@O2VNqf*^ZPtUZnYL1M&`qfwbGiR!3v`kCO+R#vGb@eNm ztbjo1&dKSos3@gSal+iFb*w6|0T zHO;y?b(wZxQ_1Ig0SYd1bAVn1)z_O?Wmq?k2*Y?ke;>nnl*wk3U0?k6r7ZyHoBMf_ z>6AX+xGH2@;C91BUk9g#hPP?mz)50I1o%5Mi@qI0-e^D#lGd5P#u!i(-68_R#huY1 z@>Tb>*J)v)nI(SDk61ieD_`r%f7rPNFaoGD8C1LJz4ajFNcaZJ`_^cP8uk-S12-Nj z3@e$|UQ=4M9kw~f;`X1KFIUDE{3D?vq6Ks0rm<%i&8eN79{WoHW{FI$uJRO<{0|51 znaMDyqF5J_^Wh=^`|N=*7A4rnbFm1Q{Ptd)71OCUBBhR9b$&A{xSK$aVU3Z#%cDaXioU9L1&77*N&bT?zXHiVnkn;mi4lpv&Eh_FPC(gYnOUacD=avtBj3%_%ZWGbwrZqdPO%U3E}3JZ zPqw{EgvpB6d?Qo4SY6K|F}x?PrQfgDNGUr_eBdtQ{kGM(W97=_!q4~(yDjGbptqA2 zIB6|{f8LD#p7CgzwS5|6-j=j$>qqg6?%^|5iXG>Pf$lp{Leu&S&t;EEJ1b)!%73VL zjE;nbdbSZMIY*vltZJ(5gfHL|0cSXVmYR2$XW=Cu=!c=)Dt2cfrpKedrtj(J1ZH@1 zeeTobeLV(mD!xe>S293(*-@+{+&265qEGOU@>$2#!RXu}sTF^52E(*JWb6QyJUgdg z$qiEKL3gt8_i>SeM3d<<$tG_yyDG|YALJX#n7Y48`7~2=fr2 delta 1938 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 Y8%4qK-3P9<{{R3007*qoM6N<$f{|0AgWo zY7hDw|Exm-;NIL^b^toT026By0AOSs=3s@u_>7Oay)^(tsQ^Gs0s!oRP|N}V+=cTPy?AZ(iB3VTx-`xBgn|*L_Q0e0X)|05DRO;cev$n?La;yCP3970^ z_VxvqmI=T7(h?iX-QO?w@*02ktk}UJMNMshMmyl~QqPGA9|FE?s85+*? z^kjeqXlS%0CJt3qQSIy!6&1fvOpuL@dvbCnnwo|xD~GD9f3B}{Ih?`r^5L4A*7$gW znp%aA&saml$kV6EP$-YX;qiD)_wSE4HV#);r>d)G>gjb6iDQk8znhve^YU2b<)aM^ zXVSaehYu&3o3r%vUXe)EI9!~p zY+q4PnWtx=we@QpDc;^I_ychkd%yIWg5xw$DC8f-c} z$Jls*&Ca*5__?-rz~xTAe7Ui(u=ME@yRPniOUwMgz;JEt`{&P@6bff|m$SF`v7>_? z60*I#y!!cbAC;PCZoV)&`nj*~U1{n5&dy|e`(Rnw*5cyQ)YMlNYlg{exN~QBV`H+d zjm=<`W3dHSuCOXA$rmqb|Jfoeq9+c*IHeDVPm1A79dwM!~ZTn<;zd5O#6`EfXo zJpQxGT!B^-P90BXrDk|9*XWWGDC6_KVrGg?uP=821Ca;|YqZGsLy#j7;l&;$QveXw zv@kVs#7(t-*u8%OeM)4tAx?MxtN*8jlq&azh?Tj1|~p@*cG5;^StGhg$nmdL6_XOJ4p7Q^Gr_Anzs0YMm7H$GfE~Wc+S;rLXLbo4@fz zv)^;h2dN`()+RoBBRP><_N!6N#j+}<{dZkKV zj9*l~dqgZ%%Kf-hnS6X`ul;#-G^YQRJN34a%jLs91Ko)EhDpY$2D0(B*{I@z<2|k# zxk{}ku<)iN_gQg`N>%K~x9F<*3~?+R^DJt>?dc#|1lc8|JX$V{g`c?Q?DS*Z@-Ymj z?kVj@KNYh6blMgQ#$&SLcCZ9GMV3h1!ct8|gHnYrVfxzxH*#t^=%%8nXqxaWT=mA} zvbZwgsFXC(S5t;42k|5v7d8n%Wz&;}isBi>oOigh-i1R&abdA<3q5iqGhUG6S%L0L+4d&ljrX6+^T1-|eeI;3cHXS3urQzl!Bd`dw^ZelGLE!|_El=WoD z*(RcUU+kSdo*{J!Wk`2X$~G1KoIpyzMR;u}4LpV|vxCYkCP&i?ixYaUL9RtBm7tH^ z%I6034i%*`zRlb_8KHOqX44!BcS7O318FA59K<2lDcLwyw?Std&bHg&hqBNRj#-dL zUx-}~AC83Bw-cs@V;bdDm&CN7QLWu&EfO0!F%OE4D3om3BzReLM2=upHoosX5wXPg ziM3zTXq>5`uf#_%uP(DCH;cQ`c-Cmf^?cDE$A4TM&z{Rn*7aa$^cTxLTuc_PNm&i6 z;nh>H4JR#T(DDP4Tx6g^LezPBgfZT||;&nFBSM9_y4Tet1-c-FV3 z;PEV47`vV-@|Z5Z*Da~wI+ZBr#GlbA=@^S=Y3u%76joub*>z1|-WY~%qCVj)iFkjV- zhX{TISY7iWRRLUo{TJTY=`pQ|Z=Hsk$(1{Q#4*ly`1VWJPGf@OV~#t=Z!b)XnwS{%0COVDr9^osZ5iFr>k| zu4gspP5T6qhgT^(Q=)#>o)Rk5@LcU!aWDAu-h<%lj()RrW7Ze^>uOOixZhy(YmB%@ z8~b+a9Lz3w9j*Sv#NaEB;8sKxxNG$>bn$YG6ZiUgKA(JE%Wl(q$SUSl*nDWuL9be^ zia&L81=`kez+Cg9xh~yl#1i6<_$N{CAlP?Ru zgvx96r9h~@FlSnC3UL%Cf65-ih*j_~D~^y`AHMy-dc4cX!|dpmTq!%!%5j<6=3<>}Xw2f))C9k-@0TgxAKmY&$ literal 3902 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 M07*qoM6N<$f_TPbpa1{> diff --git a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png index c8fea55b19abfe79719839003f86e6b3d7d35b43..fba443fccd440100ddaf659126b264090357e3d4 100644 GIT binary patch literal 1576 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 kprDF=9*_PPxEgfH-{ literal 3437 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 P00000NkvXXu0mjf9Q&%e diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png index 2e982171a88dfdf1eb89212ed0043312e787b323..251b991a03325ccf66f783a1a8ada4885285ff52 100644 GIT binary patch literal 6129 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 literal 3437 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 P00000NkvXXu0mjf9Q&%e diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png index f43d6907cf20550af6adcf90e0cc7e4a703f5e87..3459a810d46d08fcc3dfcc5f55192dab41576c2d 100644 GIT binary patch literal 4542 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 D?COX# diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png index 9afc95d61f9e5f3937d3940b24178e1c1ecb25e0..e6c0d49b64e95ba53b7c029542247ae1a89bbfd8 100644 GIT binary patch literal 9638 zcmZ{KcT|&2^L7FeB1Hm%6r}`EKzi@eqBH?%A}B4PsVLILLX#4ZB1H*ZX$mR|C@Mvi z`XC`R=_-N{I*0)=Qu6H`p68tRkMDQHlVtDRduL{6uDNz+6V6x~v$F`Wz+fbI%^&9fC+1EjDgXhe-9fUXTV_Mjiwj_yX#}i`Imj|X0tmuO1k3B zsHpNw@9hpVA0bb@%iFx&bDeR1s`8MOh}}io{R!bd{fMK}T~|+F#-CyCS?teRyFen2 z;0_z{mV9}s8=34EQ0Kh zNyO3V^X&z~JzLuon>0xlmRQ3_XC%4*KOg-L#YHk#eSQAq6iMDqq}|;7yOH`z={Lp< zAM9`cwJh+x_D5>D22KK3aYO2(W*dK38^0Jbntb{lVwT%>(A>Y>@|cERDd<*NF>Yh{ z?6T)l!pg>?aQ513m(L@;KVP3Z>B!csi%BbzS#W9E<;;P9RKXP-Lc5p*yjTA?@|HAm zpL_Om*~QEul}nt_PB9Nl;6~u}rlus;iKbU&*12WYiGlt6_33WDI-S&S&_HE!7-3bb zVy;DM*KdD2gDJYmlw`G#t5$q!n9Aj9b{J=!qK}h>Co~3zMrbx%CWa;* zJA5pNR4Chqdgty#Vrh{ZE|wzMJ})~dY-EfyIFpc~SXN;xYk1}KpcZb9oer~2_zeHVdl04MI8r<%3iD&B4 z*b>WHOPt6PHU-Plr>9#7EDrBgEMjnIkZ3$oh^ zXk-QU&GpM~CJhU}%x~&>bz1r#V;fgxSBMeuAwBf@=J-m8+tm7y>hVugr_k>!l^?{T zi3fq7efCup4$kxCo?1*!tR7)GWj<*UkW{s!am)y%GSM;1hue}}W76Vic~Y^EABjd7 zu)^AaBt;b!@^iXv+qx}ex1aG>PAw+lln@3Kx%{efHH%`Xjg^Zf?M)rd@QY&{`U5PxP)l)VH84kZe|(SCAwc-Xpn(z)Bu8Yo4-S5cA{;hEdAtk5K1F?gR`GO} zJ27@E&Sf=S1IK$l`6W}ykUq+zJo8B4zRtE&FG4%>zFNr~xz9iT#l6A$R4CHm-bP7< zt>7cnN&ayi3Fjiyk(1gxyB%Cfx6`I|m>u||MOf=e?+`dwg4Na```6~ggKA|J&zG;= zM9j*BYEGqy$tOOD@N@4>^G2+RMPVErPxGHit4B+qK(gp$m?-_{?r9-k5%)r(L+CGbw-yr zp$C9aBlhesr@v8fPYDfqzzS}A1ch+*@Ys3myI8GOjQ3!ke4jyekn9u+N`&s9&r z&tZ(2BYh$~Ei(T2QuXZyIbodD2iYbn${)Q^_kE~GTM5NrGV=?{mOgVw&Z&r0ESMbC zZ^(=(XXrd3Pt27t&+1Pbc76P!(nU3;b6|jp8{V48Cgo^xU!0y_X7t8eco6?Ekxgaz zN?wjz2&=2xz7H7not%KT{@M*lP(Eed=HkKI!82y|?sFKC1;aBVKOQwM4)u*PGP6;p(QZhQDc1e6KY7(j zn#<>Elfvt+oOjH64sYGcm0)(cm3Hj!-xY0n1PYG%2k;d%xd~ (k@Htbb{v|HEOw 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 PzmIe@?`u@Ro(BIPE@kg3 diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png index f43d6907cf20550af6adcf90e0cc7e4a703f5e87..3459a810d46d08fcc3dfcc5f55192dab41576c2d 100644 GIT binary patch literal 4542 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 D?COX# diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png index 496f9c774837cbf7750c8d5c637cc1b17a3678ad..02f73102aa0f3d6e85d8d38946094b4ca4353a59 100644 GIT binary patch literal 5810 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 diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png index 2005b9e93ccbda6fb22968ae1c910b2eea66ffd6..e51cd3794698c226681a562752a87ae5d1372393 100644 GIT binary patch literal 15183 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 S+%)_EB`>RdHB%b<;C}&R#T+OA literal 17272 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 ZHyCbLxO5@qy{_o>%8D8aWpWm;{~tN-GZ_E? diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png index 496f9c774837cbf7750c8d5c637cc1b17a3678ad..02f73102aa0f3d6e85d8d38946094b4ca4353a59 100644 GIT binary patch literal 5810 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 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index ac71f6e49d369922faca7df8945cf67110b77136..a42c0f75f0f84fd4b6e71fbb3b0adb6b54d5ebfd 100644 GIT binary patch delta 1094 zcmV-M1iAZx3D5|T8Gi!+006rnNM8T|00d`2O+f$vv5yP zfP?@5`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u0dY`FR7H+4F^@AdjxaEfGBSxM zD2_5RjV~{VCMLv`l&fZDj4dsYG&GAWEYYm2|NsBBcXyRNJ%5ZYE|4`fh$knLIy&Xh z(Ek1Xt7T=1Dk_yeKF64t)32|RI5>nLAfs4V@8aV8`udzpOV6mN*R{3GqN0*HIfWu3 zvU79z^78%s{D~jLM##(WxCg9>DSkpMMaS}H=t2bw|soBZf+jRN2~w< z00Cl4M??;9y)aJz000McNliru>Ie-C3?)~(V|M@m0o+MMK~y-)&D8l*(?Ar!@!f7V zg`{~%kbjsORHPt?4QLv*tyU`3Y849}s9XgF#rwYhdNeRb`mhu{H4JLA?oEhsw%SpmiYB6+`f3p^&Aw63>Qj% zKrdgJxLP(U6%=+essI7Jc743;xh@L38KuBM4Q|X8>MjaphI9hn^eQe2ql^g7FEmgX zXGHK;6NO1e2z?yVBqN%5JA{opDadk3hGfuk<*DDT--{t1TpUe1AFB7;(s@hG&U1EOb~5|^2MHAW^{TzM7iPRx*#5U1!xFrCJC@^_Ok0e7&B2rj~pyke37 z7I(3X5axcif7yDlM)vQOKUhY*kG9smDd1l_)BCGLGAz2uI)sx$ej1P5`3JVWWj1yS zQ||x(03~!qSaf7zbY(hYa%Ew3WdJfTGBzzRI4v|42!RQZ8Gix*000A=FFF7K1V~9lK~#90)tFCc6n7NI=l$k4Gdueyf7orL zHA_RKXem+CTte1E@lY_-OQaXAClLk3n`ci^@la4f#G@XJrC@Ro6){>bAt@w_m^7O} zElqagg4vxvJHJ2Wx6`Jxvwt$P2@QShVOie3&+onW{k^xd@PEU=fV3Be_Jk_^@4;1^`KtD&;cBax)L{+FdXp#3-$l%bClUdj|&v9@PW{ggB+laoqFm4j9QYtyCDL zC$3-5T)NcoSj4tvMfqZ6M2*E5rQXzWn^cUg4D8T0iR5OPd2mP&nf z`gDBXzTcid_kShb4j5rv%I7~F8v6Fut*&&MI*zO;|7KGqRLbS#fdhkQ&qfl7^2SDe zo5O)cOn~yn#^F<^zPWZyOC%Vj$RjHN;G9=XvoDkR^5jX*xno&ghjze1N`w&G9=>?- z>#J8KN#cwlSr&EPa$t;D*5T37zD&k3P2qqWBXB?}l7D4l+nnGAy-P@J$T?!n+5&s*fA}Ua7;4@RceuOLn*Zg>e#j@ zhN|*lLw_5$C#41lZ8DQXfKdP``7B_TUh^jb2$k~r{R0EvkB^I4SSO{X={93?VN#vd zE)xJ^+d-tX71aIOS~goM6cENfhARu)sPUd`04O1PSJ%jm8?vHoOXZvc03+m&xw-t} zBGxp&17ld%9mANPm>`A$7<;QyTSm@7mL0=D9e*9IrQ}kHWv#q=Ra#$HbbV`ZdZ|XC zFgrf}etB7o$9+lH^^bEvs8!G00wQDvpd}J3FJI12OaMaSSasBJV7I&=No(2cp9>3skWWqS zE`P#`GXL~xZhBf)6!FuiwiuzoZh1Llzt7ARmX>fN;=8`0sga0d7{5Mw@@{TUjYfIP z`;7xps|w#-TbsUn_v8NlZKRaTX^BL`@`eMulWQ%T{qfwnNM~o?@UYkfp6Te9QUI#7 z4VZJcZ3~l=Q)6RhvDn$$+n$s{fZd@3vVZ)`{rfNO+`+1<=(_lc+Ln}pf?aCGFn+#y zbLQd0SSkeo6~ky-N`b(P5V8LYg^4RyR$sr4_w+DAyep40rbMHmq_jPlGp2WUufKWo z(}fGA_4QaPC8%BpO4VreeJ&SDO51=r=Zdb+K7Rb-_HBD}Q;WxaRH#a6>DjY|$#BU~ zQmQxb{UhK$dLUVrRn@B$0B|g;=itH4bb4`W3ahG+QuzJfywsE`?o{%P2+pyl6<1gD zZ{LaySbbuUc3{Z=K{aBtpf28hw-c(=#$)OZ6P4Nq@0RTu_%8r-r?z$juipRw002ov JPDHLkV1j$^HH`oO diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x.png index ac71f6e49d369922faca7df8945cf67110b77136..5045b9a17fad670b6d30e49fc5321f4b4fd3ab42 100644 GIT binary patch delta 1094 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 JPDHLkV1j$^HH`oO diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@3x.png index 562e04004b1b85f7bd2c054b6f734a00abea3ec5..2417dadd8af5d4319dbd329d12c9d69f328214d7 100644 GIT binary patch delta 1482 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 BKF|OF diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index aba79035c5c6e7ae5ab16377f62d1bd808eef138..97cabce4ec87be78d4ef80e11cca2b80fd995c67 100644 GIT binary patch delta 1459 zcmV;k1x)(e4Y~`E8Gi!+007oyx*7lg00d`2O+f$vv5yP zfP?@5`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u0nt!QR7H+4F^@AdjxsWjG&G4R zDU2;GkTf)fAt9`2Xr)_Qi6|(KGBS=ZFpV%UgdZQde}Dh~|9`D&YKJ8yiz_RTH8qen zHjFMVh$kn^qob;1WQZmvnny>&k&%==J(4&$*}A&@`}?bAW}i?{(WP2=#+aDbx3`NdEdT!g$C{eu(9qw+#DpLqqgYt);^N-I z!qBLw)2^)P6%QBjI1DdWq_ z&!(o=wzio>MBBZ+iz+J6s;abhcAid7s9|Bii;JF5PtT~RoJ&i_mX?)1K9M&!yoQFb zZ*QkwUVplQftyK5rd?gdm6hn!)RsR#(5tJ&l$4@XRhL0Qlsr6-(Qupq0004EOGiWy zm9h;i00007bV*G`2kHn73=J$ded>|`00QPoL_t(Y$L-eZU(#R{2k<>Z2JJCD$%LGh z7z=fxnL{!Mm{Y-PrI@9XrIlSQQ;RNImUhwZ_kZhO+mH1zj9PG%XWYd=5*uT#WU9ysVE znV)kVsscAuYm~<9VUKXcOF+4r+B$HHI>p5fd%drr(VWugXmyj?Q>VylJ=QGwTg)k< z1AoUsbvm^;T2Hh|a?qTr0Z9-FgHA23_LCiwLO?mbvn$zBqYP%dBT-2qpql~pfKGdH zpS?e*N>hsIfEth{Rcfzc1_lLLF{jAOo&s&!O?ujSW+>Uxlrq*~B^6MrVo*BLrpcWt z=juGG-c2yjQCU_9D0lAs7=$2;j!(qRDSt*6CY!Qll19v_8qh^UK>t9Q;p~9ODaT&Q z4v3g?wyEiyH8<-rjGEDsD_8T$Fw6`}85+N~mD1O@1eB6DqLN>iipS^4JtYud0ijqv zc~dgVXk=cDaetaR*QWoYGTOAgPfPaSJ zZq-6CEd{ylUc!qg0vdz+4U1^XWs(U>x)>Y9%tLS{`*$HdYy&inM{P!ZgZpZGI- zN-HBzS(N`|$fhF9JTt5*3oDD|s}_oY_P|*Enjs*6^tr=}D03PP!NLolVhG6hlE>Q~ zNCv9@yt1wL3z;Wq<8=|*;cp^Mdw-y5GB8{l)#=*co42coBA{-ltF^sLENN60hTd}y zM48i=2wk0gIo7EZjDFZYAR`rH(7-wwN~e-f{^$dM&+<8r%itz2bi%A{FD*Yie571Ubkx+=EK8Dh%FK!-Cnt%Q*bLxBH z#Cr2acP}RVN<%D_ei`xdua-(}c!DZ642PY+|0EDqP|>sA0;OsT{*c|RocarnpWkrS zfk;>Y001R)MObuXVRU6WV{&C-bY%cCFfukRFgPtSGgL7$Ix#RhG&CzPFgh?Wm3seD z0000bbVXQnWMOn=I&E)cX)I=W05UK#HZ3qXEip4xF)}(aFgi3eD=;uRFfimNNA3Us N002ovPDHLkV1h;ZwUht= delta 1750 zcmV;{1}XWv3)>Bl8Gix*003^;-G2Z82BJwsK~#90?V4L?6jvC>&uwNevm2Wx5vxL4 zrD{txM%!u>G!&KADy>mLB#I9{1oTBv5D`=`;zKBkctOMm$wT5pHdwq+@?b5EK}_4F>c*x?c1`v&+1Z&nr<|RX(vmrQ$wUeC|Cl|Q^E+q0|9|&==j_5C%FDsb5dt$x z2+SxUFr$RPj1qb~(?Em(gfjydVw_S{s_RoHo@QJK5yombym0;c50)*{RTT<}`KOHQ z9pFNQAcXPZ;m`N(-G2G9d)6#!nt~062pO7&Bniv19r1MGB8*L4*W&T7PoLg&@+1Jj zOr-={3lT~s5`VMi&tF+xO7)xI)51zfG8*+37H+MpTeNW_H4H=uciZHooGX<` z%wDo&#qQkzAye1w7Un!IB18>`=PqBq`OKM;#fzz7*n*}}8Ks7U#lmy@{MSYu;Kaq*5TS4t}?7^M(nTTloAgsFIZ zetG$4+kdu6Uav7eE)D=EasO*9(?mj6Myal83o9!>-Ll2hbt9R~o^?vNd~N{LNaP=5 ziCIxPLm^bQb7$$QRaznetzIG*<%w&V+R(I$nwrYPhbN3>&k6uAl1bU;`*Q#O!qQSL z9?umte_U=XO=FCGQ(L>Vsw%UwAoqJ=X=&-2HGl8To0rO$H6L87Vkt#MRhKT!Ei1D! zYc~J5XyeA%;Gmw1u_l>4x6y>ehll5tmsdA5%q=TprkN!xNJopf!eyguC$kbzA{<_} zefy>pCtQ9%GfhFlvU>`cnn+H@T2oU~dE|(-RXMXdq0G1lV?CJ!0Ay*bcZrJ#(PA*z*s=T>!KtP`-9r# zxDX*N7F)1xT}4d|-_(4K#nP;izP?a zE{|ks1uKMqbaoDPccb(fLAl_XE)eB7#QPo*qmE3ZmOy4FWcJ2 z1_rRpC5p)O2a?x2+}+*L*qBNrTm=O@SK7$otYon!$HrbhdZdQK#O)R=m=6X(_J4T# zTU(z50$7sxczaf-anr10p-`y3-O#iQtm%^+mZZ+6rvBDe$>V|PKZ<8?2sa(90K$=; zo`GNxVvHq8#A=$v>(%4&$2V>acX!Jk51WV!FzvWJ&83@)*Bu=rJw4d%{yQco7o$}6 z`9?#b&YL$!Lm^pFvUdn^gw;qU`+r+o<3mHl;}Olx_#$LQ8G80C*x0DYf(I`m{Rv#}5&lJxui`_FFQMuhOS<-(f(l^)MbY1WR0hQPstj4@>8X}j^1 zpX3n6Pnw##nwyEsHIdhIR9@wZQeFcv{<*I1&aq?GC9Wt~O_R9YhNiV$y?^?mt&O+# zjAe5P;y^H10ieZVKb<<&)!ZyAikOv}rrYn2^z{WB8q|>y+3U??Q}e{N`rYB@&+i;N zHV_Q@X3y4QF>%Fm`Te0_uqzPIRW*YVoy1KSQbfp$hYx=|d^j;OqLh^IuN%ZwfRN|+ z?)9{^aAOIL=p^n$up;zZeSdvRZ7r5%cR>NQzcgl)Vp*oT-Wdq+VDR=8GX zWv1CuTl?txb)~r2ib%nD;&yA%XnTD8)E-*BuGg z9B?5*U4cMLZLOY6y3-eowycz5jFBXD-@R)Yi+|bVG;0RjUoT$#<@|Zcd`48iJh zjgO8#y?JvW800OBL&nMqmv>Bx5^8Vneel5L^YMFJLHsdBWLXV|AK$nU>F*c6gsBVK z{#t~wjAfl4atuQ$Do-+XU5!MfDOZ7v5*^$#&TSPAH5>*Iz6Dm`Kzq&@KV(l^uuj9_ sC$HWrKmGq}k?9lyGfL=P1LUT^0KWLNr!OIbj{pDw07*qoM6N<$f^HFDRsaA1 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x.png index aba79035c5c6e7ae5ab16377f62d1bd808eef138..7942561423eef79ec40069aabbdf8e415e5f4efe 100644 GIT binary patch delta 1459 zcmV;k1x)(e4Y~`E8Gi!+007oyx*7lg00d`2O+f$vv5yP zfP?@5`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u0nt!QR7H+4F^@AdjxsWjG&G4R zDU2;GkTf)fAt9`2Xr)_Qi6|(KGBS=ZFpV%UgdZQde}Dh~|9`D&YKJ8yiz_RTH8qen zHjFMVh$kn^qob;1WQZmvnny>&k&%==J(4&$*}A&@`}?bAW}i?{(WP2=#+aDbx3`NdEdT!g$C{eu(9qw+#DpLqqgYt);^N-I z!qBLw)2^)P6%QBjI1DdWq_ z&!(o=wzio>MBBZ+iz+J6s;abhcAid7s9|Bii;JF5PtT~RoJ&i_mX?)1K9M&!yoQFb zZ*QkwUVplQftyK5rd?gdm6hn!)RsR#(5tJ&l$4@XRhL0Qlsr6-(Qupq0004EOGiWy zm9h;i00007bV*G`2kHn73=9!=4wu3J00QPoL_t(Y$L-eZU(#R{2k<>Z2JJCD$%LGh z7z=fxnL{!Mm{Y-PrI@9XrIlSQQ;RNImUhwZ_kZhO+mH1zj9PG%XWYd=5*uT#WU9ysVE znV)kVsscAuYm~<9VUKXcOF+4r+B$HHI>p5fd%drr(VWugXmyj?Q>VylJ=QGwTg)k< z1AoUsbvm^;T2Hh|a?qTr0Z9-FgHA23_LCiwLO?mbvn$zBqYP%dBT-2qpql~pfKGdH zpS?e*N>hsIfEth{Rcfzc1_lLLF{jAOo&s&!O?ujSW+>Uxlrq*~B^6MrVo*BLrpcWt z=juGG-c2yjQCU_9D0lAs7=$2;j!(qRDSt*6CY!Qll19v_8qh^UK>t9Q;p~9ODaT&Q z4v3g?wyEiyH8<-rjGEDsD_8T$Fw6`}85+N~mD1O@1eB6DqLN>iipS^4JtYud0ijqv zc~dgVXk=cDaetaR*QWoYGTOAgPfPaSJ zZq-6CEd{ylUc!qg0vdz+4U1^XWs(U>x)>Y9%tLS{`*$HdYy&inM{P!ZgZpZGI- zN-HBzS(N`|$fhF9JTt5*3oDD|s}_oY_P|*Enjs*6^tr=}D03PP!NLolVhG6hlE>Q~ zNCv9@yt1wL3z;Wq<8=|*;cp^Mdw-y5GB8{l)#=*co42coBA{-ltF^sLENN60hTd}y zM48i=2wk0gIo7EZjDFZYAR`rH(7-wwN~e-f{^$dM&+<8r%itz2bi%A{FD*Yie571Ubkx+=EK8Dh%FK!-Cnt%Q*bLxBH z#Cr2acP}RVN<%D_ei`xdua-(}c!DZ642PY+|0EDqP|>sA0;OsT{*c|RocarnpWkrS zfk;>Y001R)MObuXVRU6WV{&C-bY%cCFfukRFgPtSGgL7$Ix#RhG&CzPFgh?Wm3seD z0000bbVXQnWMOn=I&E)cX)I=W05UK#HZ3qXEip4xF)}(aFgi3eD=;uRFfimNNA3Us N002ovPDHLkV1mluwIu)m delta 1750 zcmV;{1}XWv3)>Bl8Gix*003^;-G2Z82BJwsK~#90?V4L?6jvC>&uwNevm2Wx5vxL4 zrD{txM%!u>G!&KADy>mLB#I9{1oTBv5D`=`;zKBkctOMm$wT5pHdwq+@?b5EK}_4F>c*x?c1`v&+1Z&nr<|RX(vmrQ$wUeC|Cl|Q^E+q0|9|&==j_5C%FDsb5dt$x z2+SxUFr$RPj1qb~(?Em(gfjydVw_S{s_RoHo@QJK5yombym0;c50)*{RTT<}`KOHQ z9pFNQAcXPZ;m`N(-G2G9d)6#!nt~062pO7&Bniv19r1MGB8*L4*W&T7PoLg&@+1Jj zOr-={3lT~s5`VMi&tF+xO7)xI)51zfG8*+37H+MpTeNW_H4H=uciZHooGX<` z%wDo&#qQkzAye1w7Un!IB18>`=PqBq`OKM;#fzz7*n*}}8Ks7U#lmy@{MSYu;Kaq*5TS4t}?7^M(nTTloAgsFIZ zetG$4+kdu6Uav7eE)D=EasO*9(?mj6Myal83o9!>-Ll2hbt9R~o^?vNd~N{LNaP=5 ziCIxPLm^bQb7$$QRaznetzIG*<%w&V+R(I$nwrYPhbN3>&k6uAl1bU;`*Q#O!qQSL z9?umte_U=XO=FCGQ(L>Vsw%UwAoqJ=X=&-2HGl8To0rO$H6L87Vkt#MRhKT!Ei1D! zYc~J5XyeA%;Gmw1u_l>4x6y>ehll5tmsdA5%q=TprkN!xNJopf!eyguC$kbzA{<_} zefy>pCtQ9%GfhFlvU>`cnn+H@T2oU~dE|(-RXMXdq0G1lV?CJ!0Ay*bcZrJ#(PA*z*s=T>!KtP`-9r# zxDX*N7F)1xT}4d|-_(4K#nP;izP?a zE{|ks1uKMqbaoDPccb(fLAl_XE)eB7#QPo*qmE3ZmOy4FWcJ2 z1_rRpC5p)O2a?x2+}+*L*qBNrTm=O@SK7$otYon!$HrbhdZdQK#O)R=m=6X(_J4T# zTU(z50$7sxczaf-anr10p-`y3-O#iQtm%^+mZZ+6rvBDe$>V|PKZ<8?2sa(90K$=; zo`GNxVvHq8#A=$v>(%4&$2V>acX!Jk51WV!FzvWJ&83@)*Bu=rJw4d%{yQco7o$}6 z`9?#b&YL$!Lm^pFvUdn^gw;qU`+r+o<3mHl;}Olx_#$LQ8G80C*x0DYf(I`m{Rv#}5&lJxui`_FFQMuhOS<-(f(l^)MbY1WR0hQPstj4@>8X}j^1 zpX3n6Pnw##nwyEsHIdhIR9@wZQeFcv{<*I1&aq?GC9Wt~O_R9YhNiV$y?^?mt&O+# zjAe5P;y^H10ieZVKb<<&)!ZyAikOv}rrYn2^z{WB8q|>y+3U??Q}e{N`rYB@&+i;N zHV_Q@X3y4QF>%Fm`Te0_uqzPIRW*YVoy1KSQbfp$hYx=|d^j;OqLh^IuN%ZwfRN|+ z?)9{^aAOIL=p^n$up;zZeSdvRZ7r5%cR>NQzcgl)Vp*oT-Wdq+VDR=8GX zWv1CuTl?txb)~r2ib%nD;&yA%XnTD8)E-*BuGg z9B?5*U4cMLZLOY6y3-eowycz5jFBXD-@R)Yi+|bVG;0RjUoT$#<@|Zcd`48iJh zjgO8#y?JvW800OBL&nMqmv>Bx5^8Vneel5L^YMFJLHsdBWLXV|AK$nU>F*c6gsBVK z{#t~wjAfl4atuQ$Do-+XU5!MfDOZ7v5*^$#&TSPAH5>*Iz6Dm`Kzq&@KV(l^uuj9_ sC$HWrKmGq}k?9lyGfL=P1LUT^0KWLNr!OIbj{pDw07*qoM6N<$f^HFDRsaA1 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@3x.png index 017432e9116806c62e133bc603167d252ae8b22c..58c87d5ea758b07898909977db36417873bf843c 100644 GIT binary patch delta 1862 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 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@1x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@1x.png index ac71f6e49d369922faca7df8945cf67110b77136..882b9b5d2ff40f1119bf4c88c5f72dc691ede8fc 100644 GIT binary patch delta 1094 zcmV-M1iAZx3D5|T8Gi!+006rnNM8T|00d`2O+f$vv5yP zfP?@5`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u0dY`FR7H+4F^@AdjxaEfGBSxM zD2_5RjV~{VCMLv`l&fZDj4dsYG&GAWEYYm2|NsBBcXyRNJ%5ZYE|4`fh$knLIy&Xh z(Ek1Xt7T=1Dk_yeKF64t)32|RI5>nLAfs4V@8aV8`udzpOV6mN*R{3GqN0*HIfWu3 zvU79z^78%s{D~jLM##(WxCg9>DSkpMMaS}H=t2bw|soBZf+jRN2~w< z00Cl4M??;9y)aJz000McNliru>Ie-C3eRb`mhu{H4JLA?oEhsw%SpmiYB6+`f3p^&Aw63>Qj% zKrdgJxLP(U6%=+essI7Jc743;xh@L38KuBM4Q|X8>MjaphI9hn^eQe2ql^g7FEmgX zXGHK;6NO1e2z?yVBqN%5JA{opDadk3hGfuk<*DDT--{t1TpUe1AFB7;(s@hG&U1EOb~5|^2MHAW^{TzM7iPRx*#5U1!xFrCJC@^_Ok0e7&B2rj~pyke37 z7I(3X5axcif7yDlM)vQOKUhY*kG9smDd1l_)BCGLGAz2uI)sx$ej1P5`3JVWWj1yS zQ||x(03~!qSaf7zbY(hYa%Ew3WdJfTGBzzRI4v|42!RQZ8Gix*000A=FFF7K1V~9lK~#90)tFCc6n7NI=l$k4Gdueyf7orL zHA_RKXem+CTte1E@lY_-OQaXAClLk3n`ci^@la4f#G@XJrC@Ro6){>bAt@w_m^7O} zElqagg4vxvJHJ2Wx6`Jxvwt$P2@QShVOie3&+onW{k^xd@PEU=fV3Be_Jk_^@4;1^`KtD&;cBax)L{+FdXp#3-$l%bClUdj|&v9@PW{ggB+laoqFm4j9QYtyCDL zC$3-5T)NcoSj4tvMfqZ6M2*E5rQXzWn^cUg4D8T0iR5OPd2mP&nf z`gDBXzTcid_kShb4j5rv%I7~F8v6Fut*&&MI*zO;|7KGqRLbS#fdhkQ&qfl7^2SDe zo5O)cOn~yn#^F<^zPWZyOC%Vj$RjHN;G9=XvoDkR^5jX*xno&ghjze1N`w&G9=>?- z>#J8KN#cwlSr&EPa$t;D*5T37zD&k3P2qqWBXB?}l7D4l+nnGAy-P@J$T?!n+5&s*fA}Ua7;4@RceuOLn*Zg>e#j@ zhN|*lLw_5$C#41lZ8DQXfKdP``7B_TUh^jb2$k~r{R0EvkB^I4SSO{X={93?VN#vd zE)xJ^+d-tX71aIOS~goM6cENfhARu)sPUd`04O1PSJ%jm8?vHoOXZvc03+m&xw-t} zBGxp&17ld%9mANPm>`A$7<;QyTSm@7mL0=D9e*9IrQ}kHWv#q=Ra#$HbbV`ZdZ|XC zFgrf}etB7o$9+lH^^bEvs8!G00wQDvpd}J3FJI12OaMaSSasBJV7I&=No(2cp9>3skWWqS zE`P#`GXL~xZhBf)6!FuiwiuzoZh1Llzt7ARmX>fN;=8`0sga0d7{5Mw@@{TUjYfIP z`;7xps|w#-TbsUn_v8NlZKRaTX^BL`@`eMulWQ%T{qfwnNM~o?@UYkfp6Te9QUI#7 z4VZJcZ3~l=Q)6RhvDn$$+n$s{fZd@3vVZ)`{rfNO+`+1<=(_lc+Ln}pf?aCGFn+#y zbLQd0SSkeo6~ky-N`b(P5V8LYg^4RyR$sr4_w+DAyep40rbMHmq_jPlGp2WUufKWo z(}fGA_4QaPC8%BpO4VreeJ&SDO51=r=Zdb+K7Rb-_HBD}Q;WxaRH#a6>DjY|$#BU~ zQmQxb{UhK$dLUVrRn@B$0B|g;=itH4bb4`W3ahG+QuzJfywsE`?o{%P2+pyl6<1gD zZ{LaySbbuUc3{Z=K{aBtpf28hw-c(=#$)OZ6P4Nq@0RTu_%8r-r?z$juipRw002ov JPDHLkV1j$^HH`oO diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index a99090af6023932d2b953102b38d97f7005e95ae..ac1f2800cd71cc3e0c285ef0c40ee40d786b4f59 100644 GIT binary patch delta 1802 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 sjtPMr69PFV1aeFWrc*RR910 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x.png index a99090af6023932d2b953102b38d97f7005e95ae..cb2137c6b138df4801f40525190d3270cc12c04b 100644 GIT binary patch delta 1802 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 sjtPMr69PFV1aeFWrc*RR910 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@3x.png index 73e5967abb4f9b87baeba2def9a715d7e793eb71..ffdac97f0123d8a86e20f0a46b7650e59b6a0922 100644 GIT binary patch delta 1897 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 literal 33019 zcmeGFc|6qp_dkw5ktm8vsAMUn5Yj@(xQLR_LKHF;l_;{WGt+`XSqe!KLY9z{Ek?-_ zAt_5_$)0@(!z|zPbiI}sulF^-+wJ$~=laKWYo4AS+d1cc?&p3UbLXhu!4=EbFGmn$ zh4!HX#}Q-+{O=NklNEl9;@^Hn5W;)y1AiH~J|6mEZNq0oqmxw)zx^1tcX8OcReDXm z>0YsQ?{^&0Dn0P%Sp5O9>x2~>=nuAVcCV_ij5j=VD>E#W_dU<`EcNRgD$@topS5Oh zB5b_jQ>CVL=);F2c1kTDji*BdFJ%0|Jj?uX5-xgU+3 zEa=aSRp`VJ?B@UXi|9K1zs31~CkH(T|9`cP%+2B-&kA~4BP2f5=2C9)uJql!)_>Wl z9uu~pM0CN8H;HeL8%Q#?DcNAF^42C|9h*39J$n#-T2=r-_-Ta?x*g>sYJVFlyQH~N zoeJd6VMr5UA&9iOI zuW8>&v&yXN7nET`)KoXY=c9F6_ofeec!Xd1KJn?z6&b@lj%KkmF43A*@Zt0_^|0Rt zhZ|mBd~?O5(9*xSLr#93SY;1c1S!9Wx(eK{&-mTHj(_WYV{4w_{32#RJtSwGL+z|6 zd^Rd8uDLGHALJfux9!f4jdiEvvfH9- zof7Z}yN?U}u3Ut#=dIh5zo{4M66n*#A}u&#@W-QM_M@)TE8yee5zqMJ{TBA$?rqe+ z+R`U?L}-TNXb`%D`MY?izQw;DY`6Y3AYzvLreQy z62>&(qsH6Mw$&CEW|BzX@?G?w?5XtZ_3sxURG}|>gFQ2_{eZNf>1!PIm6Owj?r`{I z2f7E|`iG8hjnb32zt-dpg*KEuauU}x%YoH_`KVpLZ*gkFdV58o6xC_$S*Jar=E&ZI z@H>3++Q&kIZ`+mzWO+KlbJJ(&muyGwZ&0Aw(mU}1N9d+g%UZ0vIem+R^|n@hQCThk&mFyV zOM2i@m|t0R)0?5(p=Pt>3QKxXfxtc9dq=iL9ihq62k1)isR4pVi(rWfH#TiQEXjXA zs#<_HjtRGII(HJcLgNs{i=$;WAvGXPHZ=`*Zs>Gj>U%8+W=DyX3TG}`UU?BD-b!Yb zP8E9fE0Wt67D(XjbZnPng0GLF>_rZAt#S7)_C+0zzyHxt*;ys`T3oT*&JW1RmDGX$ ziSsEZ_a8rHMSFZWq9cM!?Fzco<>MOkjtnKsibpdu2aVEp+s>}rTN}+)PS&7*d^Tvm zBRN@8N>*Ph?q^xsvv(`e1@hdo{jiGQ<6Mi6I<%s$l;r3iP=Jb2eZ~2a3IoDn;`Ym+vj)laEZ(h|Fu+DmFI4ak5 zCm2k<6fy1R&o-Oy1~ZN4lMdXdkfqJ-iU7EmKV5CiK+6PA%l)8k3pj+;kK_03DXB}e z9#af8rzoTI%MSP?RomKX8M$}=t1FQ@hAYf^2j#Kxq)0z4ku!M{cakL&lP|v%JArMnp5_E)qBq-qq-jIunY0btI|{osX`VfjBcZjFD>p^c(Q7?*X)ff z>#Do>Gi3Hu1Kge}xd88P-(DG|9@2{IiGhDDso4zgx9+NT$`h^sGI9VSL-KrItIsgwJ*)1ln?dPiwOPf43;xbF^Q8r(W*I=1Ct&|BT zFQGar-=jGb#{(Z3-9`=iaO*@Yu8B3PPx%TmfeqL+_P)SCU}fb zD}_ApE7R$zHk+)Ff)ijM{(d5G7Ip0bBCk5F1@F9jm$=t9E%n7lw_blPJfaiYcF1DV zZ>@f+$;Co)iL$vXf)2yg2xs1yWnJsD!Y54`&RNFEh1TVb`TNOfLtH^!!V6Wqr#57J zha16`h3@U)%jVhFr%2ieO%=((qi=o)IDk3(3Ox1~I&3kAl;gLJ<#H}jXD$x>0>4m) z!CG}IzIOGT=?s*8k#x8HS^+wQ|1V|2I$Ku5DEQG)9NW76a4M0Q`quMDp;yV4 zEk0=ubr?H+^|1TF9nDo4?wpu?t;acJCl<#&9D0-t~8q_U+z=7 z7|tRU+GenOX7o$EzJcLBLqq=(V?{ybxsmW@rY0WGO8|RpChaiMk|c8PEZWb1kGH3? z@X_887vk*iM|&VR8{u}A&J9OtLga3q!EXqE0;y)!S1IEVsN!LoK4+y<)1DNZ)0AJo zhlhHCTq1~?m{uoqCf0cq9(G044~6jYcwaCwFc@hYnVhY1DdeNsvkYfBTI^eU=SY;Y zOH=9Ht3-@uw?OA@x)vKEoJwkLm})jMf06oPZO8{@b3#dlt-O#7V879y{vmZ%!ZW>% zJf*9^IoM&uPw6Gj!%*Bi3Gpt4U;4{VzHon*`GvAC^jrt_F$zJZocb_AClimHI$=)l zs|fA6f+?PsyzU`1qrcTM-1?ZI;VHL$q2@>UP@kp@2;=6v2%Rt$qQk|03fTb1l2V7? z6cZLE^CBri{-U0|P)~^I+_OcQ9am@{ii$Reh5}mqp(=tNzWzhUC52@pU~W?BIez{p zf;sCjThXY5z0)+dC*VVS`-w;eqx$-LZ^ui#F@*RETXA`h5_$i7 zUopa!@4VxC#=Fl>93!4A^^!y@U60u0%hh%I1CK87UjP)h<}OSv!{AT2woEIGTu(6H z3X%ShqM_3@-Kz93>7BMS#MxN{djnkx;(_B2E}5I3awGrxkuOp6$g*&}1U$n&I0kyS z=?+cVg57TQsRp|%N-_2vFzV5|PcIIOoqN|F`+FezManKI;t_h${uQ<)>#^yHG7X4nq%3L3KdU{0sRK1Un(8{u8 z9#vywGu>{1@}HsRx+Vr@`}1Sfb;JWYFkQ_WX=3m!&HLH5kB2@eoG<-e;Bisr%qLws z;de!PO3t)GTrW~{lif6 zGh|IwQLy$O5zK`!Nn$yQ(d-(j%-NUQEf|XPmRsW5V@EGJYijcQn?I;~d%y@9i%0k3&tcnxD=PN<{(Ut%5WMPZNAc}z1H(T>mBYMT(AhE3A$vI9IY}pgzJDH&!jMoE07MakAIhhBVC$ zdj<1`+oNQQPIoK3umWFNh@^J?iJ9J3sW&!cYO`(I3rgk`4}}Ea z$xiY3oezV4e%^+Lc?t1rmfialBMO%O^y#6@)DfuZ0k=6)*0SHOh0@8q_46Q0bH$HHG*kSSK zl-LaPt!fLh@&5%8H_*4a!g`c@Y#u-SI@_fwLAS=Hx#zf{M#H{*ZsR0MLTs&XxQth9 zaz#?|Y3b7U*U^+zl(_6v{fX^nbx)e!Tp@?;*c@u^BiJqsFnHY3TsLwmzm4i%wf+^U zQi_nHi zJ1%BjXLTr7SP=3l^3cHSP)hpA`ugSC;K_^5bt|k}86Fxg>pG_Sv41xY)nSn14Z7Z* zn?Bkrg&vl;pW-~|YTv2ld-mLI7U_cVP(93>%ikcJ^wcEradEr>cSd*%5qf3HiGsVh- z_!M!U+c#HS034HA^#%sMAS~>jc>K??5O5m>B)^m##kF_jsBA(ktOV1G&Ymka?^K-r z+EW#pm9=@q^I8WFHi@+1&Zjdkf8KE#_@qDt7&yI($bt}5p3i3$urElF^{*w8lY3OC zuhXn9ob}ERW!qYhw)K^_;w?|<~$VS=MiHoHg>2dQktw|2MWgzgz^+L<4F z?yO;GnpOXS8=D=9YS7b>7W5D4mk&M2mS=h&I_zc4Bw%*>@aRL=#pw-;ed_=&?Yd~C zt2;K)e_Exw6oX}l4d42u{O#(YA+tADb-SA1{Kno26=JPrHPJso#^Mu_Q}co z{x9tif0(1(S>QX89;8`;CBE^j3=6K1)!?=Z1Pn~hOi!qq{V{?oxN!FrxjWQcbOSn? z91VKya~;Fz=0dM`-?Ms3G3j71lEgL+s)ejwazf?Xng&~tFw>b1}-MmT*-;SmySkGmPP#)QzZ#EUQ8WPYDL$@=IlDE|LqQ*%XJ<6tXcQ$hj zUz;DiuA}J&VW-4DGaND>*|pHUDgDA^^b*_wYb_{sp{zk}^}jg|Wm2^OFdSijFdUxa z{w{@tW2j4bxq!G^)WrUNGMVC@sB7ZYuGU2A?pdycx|-UZi@P3`eTaOrVwohB6u(e&&&i^-0O~mYpJk zXsJbTPtVDC-j`Ur+L_-q_1_{w%^}!*sknA&9ynw@?+4yD&Yb}qE>3W81Z+q5g;hH& zD@M$>_~oygJp3n(fa=V2?KZ08H#DkAd;WR#vh&kCrKG7Ke;6>u{k)&p-1{f()K5+X8vVO(;THJ5E`yy(!>a3=mO4) zS)9_3chvQ`(EO&`O@fsnwhM3O8Vu{ zoGojdU*9s_3qJcWOq0795b=IU3@mnE7M_7bKLBN1Ea=Qdu(|ZacA_HZv3l%T^&+Lunxfilb1mUD%g)U%RRM zw1J`F$uoL@{Wrpan%=?`J`{BUAJTr2n}I}>y0u6Q+vg1{jd{luJ`C~zPdWPPKLldT zz)Ij%Qw?!-k0uWoetVo4UB9PF`jcG;&)-k32{j*iZI!|!w9TZjk=Yut)a2f_xv{w5yA_cJ}hT^oyOe2CxVfC1zy|y{^tqvRgMi+CYB`gjo{h@}skY!022| zHx3Tas?wkv;I31^c(JdT3t*z@fL^lf%3pNa#Z?@eFIx)^`hg`tty zuuK`38Vxpbn_Y->;)x_j`P|E8_aqdqItp&}5iI(mZ2l)x|I&`|UJVk6K3`h#S*MBz zkRk=9nOe0@?y`x+2n3BUu&!-#U|sAw#~--LDLQ-%>2(>Fj=}&i#yZXDi+}y{1<=rm zo=@67#ik)#e1fb9L4Ehzs%Sw$*Jqjbx?Tm%z2^Zt?bxV&xTF&Txr|5mTBB>%Bv+rE z;aJYYf~dvpR4!yFGXXbo_rQg8i_+R04xLLx$1n6iiV}eEa_0U6ZrXp5$)GJMIT_#$gZ`$zzGdBA5B1kxQ_&?I}WvVTRctoP{;a z)nSU+AmGuh>0qJ;2C=!l4V~a7?PEXRwSU^8^Q0~Jl_Skll1TX0j|o1iqXTaGNW?CQ zbt@gTWe8|$5_H}to4kgNEzsa(T2i{ptS7{P`1sA*Nv>}p3AIdr`%_XzPjk}|I)Ymb z*0qv3>uT>shhf@1!(mG=C3E3s{8Dsseg zJ0+Zhq2|m7HTgJSq7O^JweIZnk?i!s)2B?N#P7h#@(*pZP&9>NXnTpkmc%-hiJF%p z@GNZ45vB@YEOL41BKCa7E2ip2WF{2SYD1-}nsGj3W^4-!U`Qzly^6L$q2-?oV61bw zN!&%|3Sd0gL3Hs}GF1RWN(EsUmrnk30qpN5`wSKqz)t>;0vJ-t4{v4*X0`yvgKdFf zw@5JJL&gFaOTaETejJKS7Qoo#s$pL;w|>mrySvLy@ub)2yZ65?1Ipx2r(FIX3#tFw zwWl|o<)(|Z2@|fNpYk`qm4wg=TgmlauKiztS9+YBTyjoY2wCF~DM2gK6bG`80Lb<|vjieR2*8$I zI&%mS$pyNx{o-P(j=-gMJn9SSd6q6gnheM2cxc-KImb!d{SG_GRc{EGKX|MMTuT`a z=|a&NU5~jaCUgLD4qFC@7?8d!r!0nUaojnV-_Q`U5y-BCDq+)U{!CGbYBGSH1otu^ zM6}+%u)xeCS_b?0a81D3h4p2E2XBwzDGit5c{HLLS~+o^3S3YDJ9+L^TB5Jdljq5g z#wY6X5+c0U$xv8=csM~7b$5Z4b>E>KRTBn|psCxPgr~RB6E*$OI`bKp<%k<>JDR01 z3Yl`kga5(j#_0Ki^=7@+Sh^^0uRk)N)xdU=k0%sUhAURJt}Fc%421#W-r22(5i`)~%NdN^xw; zH`M-OAmi?~utIbLds;rs;QDnZdRY-<->o(nKTLZ;wgLo!_7X&t&Zr&&gaY1$ta~Vg z;h2G$qn~*nonQ0;!vI!P4QKR+hkiwps$4^jK?z7D7n`8M%$%bX_C zn(~yOE;RvS)>6qnOLQ}WW4ovs1NLD%d~v< z_ZU%HPB3y2dLN1=a2iY78OqSGPr#xD2rr_VO5uh>&->~bXZt$_)hh;gdN^($TE*p- zGYB>|XQNfCE|6Wwt!E5Tw*2s!C@V6m29z1c&0XQrZn)m+FXVAh;=n5*iyCfVpxEJ8 zR9QhBvY6`iU!TzzaC>0OHvmZK|CtO)5i>u9LpUS>laC@6#$W+2TgwWX4!0MH3;1t< zL%wi03YKYCJge)s$UIjW)<&dG{-}`k0P_>&f%E3UP(6ZQg7SEgEWp1%b_vUQ z^aukas>1-s#JvP*Z(D>PgWm$AJ>9^7bRgIW!%iLH)DHO>^BD@xzo@f6a70j8zDH~7 z-laB5ta~pClU8l|-Y^xZ8%gWr!Q0JJf%Sn`uHEoBLs>hlF3CVC!ZhoA8Ax-Lxa-Py zz+pqYA!G3UvAFCRiXG|*!HZG$3=lyLA?#Qr5TN`ER#`oSyZ8|IO4Tek2g@r%<;v!e z^+L#mosL`p-T@9QV?o|5bO91G8ZfH>Db~60yI2?)^aU>KBBSgXUbvP8qVHm|XBf67 zq9(>Ddq#lwSRS@W5I`tP=Xg7Yq6MrR{7TUOvVOsLt3>(u7PbCS5EJs%FM@ml|DhwF zU??)cArogsNYRUNp0=>AlqIiZmUYRF2L76x7gevN+>5@gFjqQD3B@PYo~ z=8w_=r1XUm06Db@5sjAoL+5dd9Eu`Hco`YC7!AOTH8F_5vM{#Y}!Gg52CW5C?cwGh7kr3UTQ$xT}&5&*MV(*dCpnp`m(c&djPrm#>*ui zf)>_g|AvBdFJ7{U5Y`d~8UR|L(#S<%09tV?Qd|TEAZWT@b_m~42NLNb>L_I ztd`EffL3q;TbHmP=Ycq1%mv6@Q285k!(luinSEZq}27ZkOJ%N)q99pn@aJE~Y%AA46=4EO9aj0VoA(fjN}{V1-Twb5+I@S54I@<)sMg>7~|9YN3fhMLE& zvxrqLB`%)}s^zqWexh+18&*X9mXZ=q5r3v!p11@-JCV?ky+8} znX}SH_0NAlnsI@FB`Vtb>BK$tzDM=HyTgN9&y;k_>(lI}In-J=q5Q&P?KEBq^bbX9 zF4orat8@ z5#Cs00ri5Ys6|A-h}t_*U=tt)4h=|w6t=))3aS{NTneFSqOqq@e`{3X@1bn!(2)JH z+Hw&+NT3L{e<4NsawBinEI_A4dUbgBOV0-en=aX={~QyH_yl@3L{N7LLUCCXv2Kv# zQ96A59QNsWrEJy#_3e@%2RQ1=iA)PHR1b?(wS@PG^!>_P$7%7!3b+b@JIg^INY_06 zz(V|IP<3bx!6_nf|)5PBcS^I-pAk(Z$E^PzZlBFrnnTkfB&u? z6}23a5pO^|&h+5J$AVQs6M2Fz6iVMgYB9O1!3yMzG z(LcAhPnqL4@+^g1ny`NrGN?H}i|R4=DduTAg6{WA1r_Q++qPj<7n%LLKTPlxz2QXm zDlaA+L$IcttA6!OcTn8KD$%j7V=0nh1lSpd-3@X9CfC)>J@N+~?@>Ar;H8m=KuT{A zWDt(Q0%Q+V%5E+w@T;`EM}z1xco#X^{^?%&S@ESD(cjWEJqe$G ze18;hU|5fiU_F)~CI<7#RDv$IKHbIL9k%?-$7i7G0oiAs`|S$$d+^df3xcVX3RI4X z2qtRKSBlLvH`^{{1?d*|Zo)Tqi)~jG6qfMsWmP-7I)v{Z?PgVTgc|=qS%+QHuQpk; zsZSm66 z(QM51_)B9G4UNL@pLI=}0`qYOmv)i299qxkI93`(+Zz**DJz|swpq-s2Rl+$$rq7nG2eVNpN;Aa47pzDmj--@gl~L%U93@a?726&7=}$;`n;c-mS%F1?q@q zKrptl!mDk(td`dRT^)*rd~7E<;l;*>BG5bC!;?5}L^dcz}r7CeUNvy&ECY zl!UqE^O2^LI7`i3+|L8ENhf-0WCSGegQz;Z+^x6{KQTEp?9lr~L*oct?Ts%j%a!uJ zqga8BWjp~dNqhsDDAJ`OroM3_<}pUrA_9cAKPE=TKFrRt`L6UQaGsb7d(c86xHhJ&8e@Z_b+O5fM7KmPJzWWNYz6Fh65M{5301UiTZ zVb#x>Yl6KpKL9n7>U?-4= zeN{Tz>;D~sK(_-2Dlg4dCCNDTMU0qh=h9GfW1visBNp98oY!W5eIuIZZW7l%&bJbp z_ymH|^1TX=UcA`;ImQ?dszzgUV~=zxoA1AJC8k;r%qQp@?11SRTYXWx+$3%PvZ${Y zf>0eY_FMuT`f;ea4uBUf2#7Xo(EVat6uoACjdfn=&VGLTi%OC zO8k`)_k+#S0uszFUH+x&Qf+2x1Kk*w^m3^AB66*D_~)-)CBCQZbh1i|Br zmpi}Kc1*gjqh3R6kc4Y5{%smtPB|Z1jcR^bzc_{02>j~1WNmZArx+wgHk3{rx|W?r z^Q;5<=@_k}j14_~rRQfv)(cT+1L>XbL|<_QUFkMD`Cvdo?{&12C2i3Dw^jSK#A5i5k<#sUz<*K zjaxcK$@0eCfcFWe%r}sOh1Nx1SSu_{lh+TzbS@n#Id?SyA0UaoGgXpkW{z9eKV)bs z<1jIIno&z1cY-r?^(udeh4AC^BO-C=Sr(+uk2{%3lrvYa4y!n5IV*XKA0wAM2f7#0 zs`&$=J@TaiM)prR*s#{{TWsw(2hZf0d=Rh$%eVYJWJvJ%&}X8?ZQA5yKV&zRD=-CsTrsAV`Tv&ix^71jsak5Aaz)W6$^w+@5WA`v@!z!9xO z|AA;XKqQ(P7WizN`fdM%_Z*}44@|f&8Sy-8iW(hHhTC>+HZwB?w(Ccfs+7&u_$y(0 zff8%|v-a#>V%q{OG^mOZX}QRZeV|zu(yrP@{$*ti5k(SYRH^lW#T_Jf57z6Iy1J)T zhM(=dc@4E9i~ZEZ*xHerDF2B`)Yl5n81Z$N_SYE&iYQ1CH8mgf?4+S*c`$+QD%aTb zB}8~Vsyp2!zpkmf;?j5Tgaq)vEhQyzluaiC@2H`#-1%qZKp%~NzHQyg>^*yZl$2V| z$j1Gv>k3l6Y<~fdbn|eFCNUJ9`9rrm*)!(ey*H($7K*6Fm14G{HyVJVeb&o*ASzP9 zGQ0T`8>$uCDfHk64)0<;5PH5${!wWRzKa!|v+y6_3=M-o7si@muEkQnZXdrG?qBe) z$1Vbpscp{AuZhy(Spj{*Fztq)V_d#GZQM(s90grdbFL7har@}OGM?TVu2ZN8Pl4*8 zGDf2n#c0G-S8s}zjT`X1KBtW=(l@}ZD9%~SIgyoJsn=VMA?;x~DHsu;V|a7p#w#mU zBpH9Inh0zG)*PW6SOCe;W~rKnZ(rXlELyLzh+U|sZGuf4S|Kvy!dctairdAT1C5r>EU6+hQ|=rd?lL&a z7*;>ft%GO8Q!E=CQc=CuJ*vf*ev>v_b6cy7z*?$n$U1`e?@l{K!>p}s3Ht}1Dy5B&i>0*xnIAED@XM#KR0twRTKuHyDlOi z=ow^Xz2WAU?|=ocoZ2Cxee4#mK%h{+l1F|27-uV4orXSYrXah;>S@Zr1<@ewM8MkR z=g56|KgXV(d-=x4r~U2*Xk9|ZhuE@;bl#tn-%{{Id(A^3&$hvx4XHAsW}x1Ije_lZ zNy9-O*TE%F2I~BWpb?F}FTEeL_AU-TZ2BxayotSh@-s1Zn}S|+p*saS&UZ-0?haJZ zgvH;WOCQ5#yo%p4hwK6PEp8NV(P#6I&|xf3-t6_|1#6TJ)887$w{}Z$2ORaI2Ys6d zEGG^Yf~OMWP_qewbfRGLMJ2F5xw}D=c_T+AZrkSZ1 zFI=v+KKOBU7tbg_uWM9Ap{qn~%c7DbzUkdiK-0h!2#*M$=Z>#T~pQ~7S z^qKpY$qK1@?&6iR1*xIn(zDHLZfvgNaEqieaJYSF|Z2Pd>Ldx2q~B1MF+dIHpseOIsO zYHe~!#3M}xI9UY+mnZu+D@Un^KsOWtjxQ7<8B&!TcSo~hhC9~p$B$x1vaI1g5-DP; zC?DF@*+q@P`|?7f4*oYk)ck@#(N8qAof*OE*OUZega`X~%2GO_WE&A!P15ZA%sx;e zg2mVvv(op3p+~wImyl|zW?YXHQE0|@Ke%?$z(o4Q=c53JYJsunJlne(KvIs%leDlf z9bMhbI;%9%1bid9OQgWykI?k=enPr z|I9KXL)&6E9Ujj>Lr+&3Q)ns!q*qASyFW8|uy70c!^e)v3thFg;dXM&DLA8rCxHF9 z9fjt)690mRbe+t>AaPW|$Np?UCR+AuZEbzyLvCMaVQ!q}@EO}Mhvom-Fh*;@(u3mu zDPFWkqo=9tpW+28-3JD_PwOA!MG+CKTy3S{{CJ@^QN~KT8T>KFU{52==W~!>NeguI zqL(M2&*m3WdbUJJ5pe^%he%79K zcUq!oEEXro1uZQtZao2mj?1;-z66RqtoV`V9sP7YeSLis6Qu~~ecMH4<2Qoilboy> z*UKY3Sm0b0pA4PoYnQZu!~)q$Vh&7U(WZ>8gG|~6hcwKs=);jeGxp-@Es%0wb5k+f zu_OG^Ba6g3)4w3EL-Sj?G-j|AOB4Fi>8ocBe^E1Ps8SQXuLq^>pb}E8Q%R?G$vFtc z?=~nv7CnVFXanS)pM`7C=ML3XI8;sR00Gyx!9^*qyx?jDiGL7XqA3ssA=`SQ)^2~| zK7XKcQWe63Zhz;_f5KANQD{!s8BObahrOty<9_LOzsTH?&#Vlsea)q-`=hry*WVw` za?YR#TJ%Q*oBJ_eo0{tCdwAh85SYqGiwCi>7K$zPje@`W>^uqrXmhptBEge5y7Sn% zk>3L$(#qnCDZo!Z#!-sfyogB);oM$>W`K8)rGRKqESb&p~sOKiiT! z@fq-M&`=D1hcMA@dbCQd=xsl!+2!VT(ft;ulOx5B_BO~8L~G7<#IiLbBRt@Teho^K z$ntsB0-OQpeQ^PL^FznxHfgLC3gHb`I0LP)``Njj3UwGc^r8egrO>uCSz^G%q@+yv zz8Ul|&&`FLclxx9f&!PeJWut@DV~6x@LH9=|2uTBb`oIDQ`W9C#5(w|J`AoT(x8I^ zxa3>hfM4?E+CK744)F0%$Ay+fc9zZ?=;LmEZDI%T9kI^N^@$4)xv?OMKXRZGF2Zr9KD%u)JG)X-6Z6DkDfTQxZ9T)G0_Poe(;57CAEi_5 z`$SMd0#fe&&!~!{6y1STdAL4H`_A?2)w0B|tCyh}_6zX+GQ#`5I8CP3b*=(x>Ig5a zfpP2G2hXmjrA1y>+ZxpqB`ca=kzRJ+uLLNL8wyu}>e&ivM?~5X=gpg)eug?5Dy1GG zuG#S!gAI-um~R>n?O=!*6V*oGNuIwt5d*MXLFj?myexc9ok#FELWjo3S%=ixC3P7xE4`(LJ;ORmWSIXd@aU%on8Cqx;YIlFpoBv-7l?Pqz>L&Qx8UK`mi~7Rbun&15=Mkxn7RWpT z%~n@urd)qgsn<1pfmhd5HP>n&vtBoa=!>;24mB^9yUNb$H8hl2w~rLKhWe5~!CEXZ z3siZB3=W7eWfxuOU1TQubT~P~_4O9w_AWCVLAu>j_6`oc5xX+$wxg~jNEY5`W5ujl zkZ;1=@A=X;s!wERLq;_W*DIMJ{#@JcUvn$CxqrU^hUS`)SoU_*7m$l5I3#R7vsO{A z0Y`4~5v@)`!|Y;>mWRjMK;I}G(Uk5^k}GQPd-iP)3A^ z7mwC@t^;KaH4t3eKQ%;#L_7oJMufNWD*ZZ{p1nO)PsV%e{#_l(AuN`16kPyw zBKe?bfAz|;68aQfx8$7W{jESmoO&V37&$VwSjA$%bD!(W3wmatg8NCIA<~F>N=-vO zEv(qkv-ZiCA)o1`mC-rd^57;3LKKRE-Q=m2_R@kjGz={el8uk_ak`59XS66qO4vE_ zhq?S51Fw%#>Y@#nU#zr`T{X|OpZMLdIono1KQc@^5!Ks}4FD^ds@Ojp7(A6&wi&%z zYCp`n#d3IzCs5W%l{(QiI|5CMSEKdy<1NCiV=?Q4Btp&qB(S)!P_9?gGs11mqq(=93@<`ggT2p(R=MEv=vI%BFST*Mz9wzP_wk-XUp?+KyHh4T zTY@*IJl&RzJj)<=`V~pP~^pDuE2LgeqBlDovA5~l^d5kIX}AVX4c!xhF)?|bPKW= zGfb94FQ6jOoAKTlW6vnOF`p1dn!w;5!Yz^aJ4S02TKjmnot6y5b5>(s)) zhDx(ZDckV-p@^54cVL?VtQ+(cqJ^~Bn}edS^VOf7#43|qDK+`dUUi?J zhCK){rPmnoK(gPnl{i1!mli>_MF`gB?h*P+YzDbX0kE{cjt+FIr^<*F^Q!-m@+Y~f zKq*!jBpyYLM!0m-eKRuy!OhwJiMr6rnTZzQ=fO7YZgl8PFTFd)x+@Z+pLo)}uE5m| zzvZDc0){g`?Jq*FNJF`LjWD8;UHd0rkqW)Ux=s|oIkC^HFame;cb8AsXT1Dsa-GMB znUcFfl#){y4>^{_Kh!+Dp?M9b>D9|$eqKu~EB3}9TA-gT>eF9nV0Y66GhTc8=vQt3 zcg1mz3ji?znnGY7ND;GTOciIoTffA9HaT4Z39N;M{9*|YDsMsrt13p~;|^ldQ`V^S z!L>kGFLp}?MgTeQ3vd1^NlY9Vpp$A3`Ze#^4ibc-U&|TmD{O-3g{C~>aRXoMx~E>f zFli=E$6op(o+gQ|HQe0^xJRn&p5^Yl8uU43anVgZx>uXmRGCX#0mwHlYinbq zQWR4K|VlD311M;iN5;(5cz)RVgbKe)Q{9*3(8_X9vtb_ZbZP#Sr7Eotg*tf8Y zEp0E^eS@C)M|;}GzBzd<-=Bg5#`f3p3JbpfmR+p&_PA|hD^i4w%!#f@j~PgG9#+AR zK5ZTmP?sx2>WzZt@ygI{1cq+mRfm@sQk^N?0Jz9~tNvbn)@$dNp?3xSx9T!?y{p_X z7!HtjwxIE7GO=Y>R{ENBV&5d2g~@-pf=5K{%_?jUIbq$xq=+;f`C-dKZf^p55`o0G$+!$vo1WSQo7%NT4-kMH$Io5S=qhIrtC#QShs;8M4{QS~O z#uyJ)9iFDF)@bmSPW(BB8TtNOSL5dCNbHhQ9r-={2GDjMfb zqE5l(7U0Dvcr1>QfB(0T4;m|bpg2&je?bKE1poo1i+ODGoPw(e4|f3t9H33KdW7E- zHcZspNY*Ia47U$8{87R0AA>6k2y^W~vUT)!9*hvaH)^>49)0t&}~7$IuX1q z9nIFKQE(7pW(CaobRT^%>{dG&YM$NJ_T#&IcjL=|1hkQ+5RCiH6WVOy>~P>np~pb8 z?YEGSBT83}OeC_Bp3e_ofWFY2gFqGBsvVY3ocRT2d!9<&Tt^4Bi)i%91xS%)PVrrQ zo@=F;wg86Qb>Pa2pUwhgDrD~8);{>SO<4ae5o6bP^!ckgn@{6Mm(C>}Hb&$)UE z*5p4$|K0feTCPijvSD)jB^gZ%#ozOZxY9tg^+@>%Xbl?Wp>l~8TG^=Np7;If9GMuF zJIayq_QnF1`fR7j+*Jkam5T%~40=y6$706Cw}f>r7XiSC8?OSF`18y0y)>4T$}+iI zynhI2B!zhZI^6)9S)NVu=&q=3Fb`}gG*h{<9#|hkG&lN>S_a5!#M>LPj27lXm)c+8 z@gU7`Dkm9ntb9x!)oAK>bXP%(IcW=vIiOpI)~DxiQzrF@z9{1I- zcj+!-U+Q21%P-D|P&#}&N8Z-``2H(Ob$^`IoI*%xB7IxM-MYkdVa(N&IDbD7p7F`S za)>gG4WB-R-U0RJlru{16XSVGVh2{CH$xg7WhnA+U^$?ZXGDsrz?H$l3O6Rp=gF)v z!eN^8vZ(&Dep_{aCfkcbd~Y5Zxq79*e$ZH}P7!2fC!r6haFMvRb>;+%n*lj=ZTHNb zHA9Jd5tw{axIzofS%Qof1}=Ooh98B>^JO#$$mx-;2Bsqydbv2kGnukJ4|ln@8p`u| zRGiq7PA-V#x%2pK#0CIuSCuaPquq4h23xcw9--2tvh2g%E%U3QK(>vDpm!AzC=Zzk zTxeY(2ox=v$A*Yz#^a=v@V(0_*Z*;HuO>x?4khg-++w%>sFM1X6+>qcstM|{=$@(wjY}sA=X&M4 ze%}ppE!cG4pFe{)Lx5M>FO2p`joQMl%WO0e6gE%uoaz5K)z;wS0~K}@1q0ZdS1Dy_ z@8yN&oMeiU1v!O{quO&iX3!Ddg9>aI)30d=pQ1)9sc`oareLZS0NJ;s69;K_=mjPU zi$KFsE*Od4k%iFkf8|~XT8NMvW7@uuDRrnVLD^X(#JdBPnQhIkY<6FNRirh|Cq{us;h}j4|`*{5i~c!K9(fOdX!7_=N;Ozlq%U z&^9TfdxbRjuF0XFR&_u@fo^HOIESUE5l~B`Y&s$j&>w1XqqYB)-6ner>&b%hLi5F-h&=);QO0EantOUWzMl?4mFR-sCo|$S zqLf0epQQj@~H@*~WjQ)c+bRB&X15-AZO?B??Po9(j^8Rkeir`UPM@q+KINQA1-*$sUINfN(p-ay&&gf zF>8YkGVmHmmn<|eMx70@$em3|S8dMQQ{RS>r|P3Bh5m)yAwq(TksZ$*cPLx|815k4 zm~6xZb-0ZWx*d@%<|3mbGY)36-55qHEN=_%^}X-Gc8c3Og}29qlqSlMwUQj_vDmX@IT;f_Qus$WilzrIut^ zj!no@y9BziTqC6?^2du8CGqhFw7e@#}FU$a@)6sR0dqS>YE zfApw&uXfgGtV0NAp(#1ngb8K?q2GdZpr9YKZl#D<=Ur%U*?(UmQsR2f%O&s)8NdH4 zpP0LH8KFTZcCI25)`d`@G21?sX<{Bh6;?qSzBF&%w%vQ5w7k@~mS1(E5}Lw|&-}GO zvJJc;GQDmIaztej-VmrE8Br1LMZyBbneg8*m1IY8Bbsx=5?4L40 zZlG0@BTSMT2zr;=8YamNv{)gxlcD%kPF%JqHWZbDaTHBpri*#X+T0TJv%V>Nxog!; zyCBA=Rf`jQ_~vOX;En-N6KH02>7J_h<~nHZv4fx6T}KD4f`Z1%IamIXb4*gwY|B2E zYcD};8V)zpWFAC{av%%XxPQjYEQ&YwHWaP~(A>n&FQmf`D#g#^ilH6>UlBRHfd$o7 zF_nFdBG961vd%2rM~Vnm@SRp70Q~Yb!+pRbHOy-Z;y_dsp>No8W@L6a9|Hs;i1iJ1 z4$JqyerL4OIBj8lnFZm2R@j$kABvwEru|B(d2{vd@2S8ebk84z-wT@{;N~(h1RHRV zc?$ytRnbGxd;eqQtLV~Z1Uq3nqwiCEu+23NDyKDFAA7V#uS&DP9j#u4P?QDSryc^A z4g;Pg)M~+4^y#SRX7EM!jg1?j;3+hVme2pw5K-0kY}FHJUM9fPDHO>HbNwfjw`nq~ zmahfU70oi#79-{nLHIhOfNEw-UkKXfzysGv zog~pHO~{M-yL_m*q%HxieWRMAKSkiINC2e9!vl@)N2l$(>-6;t>4k9b*`)hiY7A7) zyz(+4cyYQ^z6LU%=a>b3^`$Cofp6_lp#b0Z>1&}Qf}R$$1z6xbPaFQjWVD?+0- z-q1Mor9@wK-!YSvoFAGv46wx|f41UPm8$SqLTer9u;>LO>%N(!C}n7UgD*+(<_(ZV zk#*q^ED#9EU3Erp=4C>;dxBLbhG<30~Lw_O0VY zU$2vs`t42V&Ec|(2se;z$P&U0%Qqh0K^Ei%;*CX(jQu)t^kKk}gz^#nl5=IW8u}}0 z4PNHa?mH#9>i6-$^ad*6!XguJQotB?p!e-dhw- zOD*Z;MX%0DSmlSk*e@JpvH8RGlF_y@vt{)|?JCj{mmjjiu~D z)XoBlnz4P6rla&~DCd9zpeT)jg+TB3@&INnBcekWmzWIwd;E%?#{ODWM!0=IfX{t} z%!Zwd)pZdYs3;YkVc-&wNuVg;Chz%LR-_SlShbE_4DKJ5JE318#2_L(F^#H|8JjwtG!P<1p-%5BX-vSkMIKBhw}5d0ngFL7bl<0|2O~ta^P+y z;39zX^$e`Q<;HZnZ+w>r9M=WrIEDj8AM5pFv;NleGFS>S z#OSprW_+{&okKQ3w~iTjC^m=Umk=)tnXcPs?*Z4l-2{&0pRjl8m>AFM^96Y0nZkb$ z@GY{y#W3*PhX+qm-$G~$Y85lfW{an^LB{Ts5>9^np diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@2x.png index 73e5967abb4f9b87baeba2def9a715d7e793eb71..d1bbbcdd09fed5d8d9bf27917b555212089888f0 100644 GIT binary patch delta 1897 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 EACWeSasU7T diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@1x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@1x.png index 57f300b1dd4f735bc8ba7ee653086ac078e00bdd..5e18a2da142403d06a20b49b9ad045615850e346 100644 GIT binary patch delta 1740 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 P00000NkvXXu0mjfZj4ob diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@2x.png index f8c9f0aeddca19b77bb8955234f547871f9da756..618fb3b29d6875adddaf786a8c1347e59908cd0e 100644 GIT binary patch literal 2539 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 diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index 7d815970aefa357932ed6f99dd8e3c2546d91c45..e796d944b5451fa2a927ebce781d81effea6f36c 100644 GIT binary patch literal 2718 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 CG%i#C literal 3864 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 KuhqSA|9=3ZhKW%C literal 5097 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 N%}q}es!iN({|f^G=biun literal 4422 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 diff --git a/frontend/editor/src-tauri/icons/mstile-310x150.png b/frontend/editor/src-tauri/icons/mstile-310x150.png index 9419850f143ad755016985a4fdaae968441ef01a..9ef3b02fe7e1a59e41de9bf2b07693153d565ee3 100644 GIT binary patch literal 3166 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 diff --git a/frontend/editor/src-tauri/icons/mstile-310x310.png b/frontend/editor/src-tauri/icons/mstile-310x310.png index 7cda72eb7e05bf322d644b7c5f008ca07090e0e3..044a032aafd131ee8f4e1173a38256d4488fefa4 100644 GIT binary patch literal 10701 zcmeHtc|4Ts`@ibb>Bx~%nMydPNNE;ZWbagtqX}inHYl=Hm~3NLCxo$_$V{>o2T5@l z%h*eXm~a@8Z8RAB(u`$nGrspTobx&V{J#JF{`*#5nrEKpe(vkKuj~DOU)Oy{T{F}Z z-g|7XfPjFo{;!vf1q5~?p`Y)+16Q71c{Bum{m1!dgP#QiisFSfZtVu2e{}fO*g!xa z;FN&CgD?SsHE`*{gn)pLynw*eO#uP*X95B+Y-;6oP4LBc_GrD!0-Mm^7d5$w0s^N; z^)LTy>en|rc=z|TrQk2DN3%cCYA63_mUcTud+P zGWB2j=)X%-p-5PbqmZ}n2QKF;ks zyVvS_EpG1orH{Kr>9rwPAE=!5YFiC*3Q^LhOE*lU7V_HXH8(Hr05kl*KmV%-*rN&W zlg{c?crsgz+IUh0wzV53yRwa`jmDD;HLjHfviiRS(iW4`rC0BJJpaWg;p(!q{t**| ziDAOT8L@anZlGpgO3t(j;Yx^upEM)R|37 zyTEl1B5iSqlRa5eqwh&p)u&f$Q0-rsMpu#GYv7+;hy7Kvwg}}|9vt7hY0Ej)oDyv5 zOHR$*brAsGI_;%XA_RHaM)1GnXebn?bb*T1Ud52Q2WKS5-dVqbQ%U~GZ z>gbz1w6lLPZW=LhI((q`%o?7sMj8?e0@rw{E7FHq)kYo%8w$BfT7 zA$r#o6~|_bZk|#hUg;o8>ä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 E1#6^sKL7v# diff --git a/frontend/editor/src-tauri/icons/mstile-70x70.png b/frontend/editor/src-tauri/icons/mstile-70x70.png index df178bf964606c80cf62953f967eff9899f9756e..23f118d9f1bd0f720905630237e2803238f48e75 100644 GIT binary patch literal 3328 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 NObpEoo}YJ$`VZoj0agG2 literal 3419 zcma)9`8U*!_kYirFoWzd3}Y)>WqnGFT@8vf6w1Dihs_HzsPxZfDmc6$GKgWStx0XbD~gx!-s4Y*OA?Uqg_9 zx1W$K{<@oYdD6ZH`6QXV)eY?#dX|Qy11bvTcCW2@OpC!W8D7FxuonUyezHF9pQr)# zjG3tmzH_z>_f_uYK0vbBLq6xV7;S9}VS_s}!E0?iMaBOo1f^>|T%5qQ^T2CCBie3) z8pW*{k~hz-Aq5QnOL`)T`Rrksa8>q#=N0oExbKSR;6zny9bd=n+h6*7zA(HRS2~eU z7p^qf`3j>vJ2By)9*teSLW@p;n0QS=E_n%;$4fKFP3&(ABD6b z-ov##2*%`vc!%8H`Iv7LV^5hq!pXoq=ejiO(d49mP*?arKlY{{MN7*+@*;}17xbJ5 zMHpRsGhI(~r~3bDTy@>6E-CrjLBYln=)aZ6IJB`3bC0d-fD;SygAva)d@5V7ipZ2U z(98UFO%Q{Y5%W@5Y)dQidIYk1*+hJV`TPCy@-+?oskmV5o-m+mHTVvTKqIaq#yzRu z!XOSVu7NUo-ltiwDE@_U{5ATnd{7)J$SElLxugVTB&xi1BJSYfdzdDoSYi1JX?8|ocHhD(;IlbR450<9beG56AS3*pVlI3_D=Ar)KpG26+i#zLTNh+T=Zzr(0rqOOGy-Vn?g`$$Sn_O?8XVb? z={Y4LXb042O^BL3@tDvyfgG9lZ|6D?j}IP*T$nY31DBdGb{CYU*xM9<7K%RC~==)zYOJtZ#lFdr0gkj-F=}fn9TrKVa*?-<# zkoGW1-+gMUNr>XXzo~aS*<##@W4c~RX#3<(bPh15z@n(sCG5<;^PkjVR#wrbpK0+h zf(jY7baX6mXuqP|&vux8rWq(ra}cd|uuV|B#v)^>7`IpA-DjR%Argg4gB1}}`LD(?f456ZcX z`rZ);N)Xq#_fC#YfB9wX?fpbUe2tOtrpzjCv!*Raj;EdfW;?$WNWEquO}Z3ixWJ^k zuVgFVR*f=JE$Eb%CP1}((wQ1;u4^mtX@H+sKU<0AGxN}pi28N9)MJRFEMwDmh%kER zw%7m$}Wz-YnHaO$f5SyyLgdvAR0E zd9WKlgW4|PhjkY|H6MmUuy@9VdVh$D#?yatBn}Gze0aXnl??ge`rD#*q!?YRUO&#F z58E4kKdc!ENRz(hu?BtA2{-!gZHqh@y@YDm=mu(SbdMWR&I^G)#yXLvI(9w^Hzan~ zbQhQlcHTMpaH~ghIE0@1b@;t-i(N!%NX?U;9%~mWkJ@n5n4#P?709_`>GsVjmb(Ac zB#f^$r<@3MGw2BosMCuW;VNkTBt+w>^A)d+=!S(aJoELl0>KPWZxmGuTFr$z``8o@ zU;0St`um(S>EahMQJAD4FPv^fhCIG8pw@RtI{f#bUd0pg>&h2HP({io4Cm`CF)1)5 zMfuCN5^H!fNcupOj;_rTgM!14IIUabuOAJl+GCpja&fHqy6pq#MHDOI6SEk1TynL`jG3Mg|`Uh$CgK6z~G8H0nurOrn)SwI#`Q zDFMMuAmNmC0`_6X6KHegQpx?sSR}L)8|6G3M0mz;ZLo2?K-h59Hu| z|F^mPiK*N0OaGmFJr2En1IFiKrnLuJ1zHF{n0;Xd5&yc`2EVa^AIrW7M3&pZazgHV z-ygwGm!na(0OEcQqmfX|%z!k$8f9$p@j7Y# z)cGY;)+=0=gnp#S=7CMuSNxd;#lK|Y_@2;3&hu&?l)y(@V~J!-(e7=VD}gT)0`9&{ zh@rd-J%wvG1NLf(n|bmlV>~;@%i}!naVt<$ z1`kRXd-HF)j$!cQyyJi`GLRwTmF^b;=m40|dxM9Az`;gUPEHupl=y-#`52k-|G=ek aCbDC;@yezEU&GPU1Xx_OHmf#qi~S#Kl8q+- diff --git a/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png b/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png index 3cee859e7fbb07d04cc88b4760556bc2b43a30ca..ce40d97faface38dd7f2f0ef54e4e71ce58a8af8 100644 GIT binary patch literal 364411 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ԉE@3M{no{{w55zlRN(DzN51o{=zPA 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

    (`#I8a? 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

    +Xp1;JZ(I4ktFYU(dNV;H^0xAUA;jV$ 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 literal 415584 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®%7VEW>NnX2k8xO)_+=RCGc$JRx+ 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

    {<==YRdXXm6VbaS7AM9t{I|X#D!W3jh0DMg6F?; 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 Wf30KUo$+75M_onhajBB!oBsiyO47yv diff --git a/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg b/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg index a19eaabc9c..4556168c5f 100644 --- a/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg +++ b/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/brand/classic-logo/logo192.png b/frontend/editor/src/core/assets/brand/classic-logo/logo192.png index 08101ad33ccd9412c402d3191457cc98abcdb0c0..6c8d372c56ab886719e45f75d4af8fea837f504a 100644 GIT binary patch literal 21703 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 literal 24013 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

      6T_87YxF4_5}&h=9E)TAq*C# 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 Ef=e;v!~g&Q diff --git a/frontend/editor/src/core/assets/brand/classic-logo/logo512.png b/frontend/editor/src/core/assets/brand/classic-logo/logo512.png index 1f7fe384fd98ca7ada6d9d8ddb4cff46ae26ebe7..b71e8b11aafd573bada3bcef6df358f68278a16f 100644 GIT binary patch literal 80195 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�w|iqoNuPLvqNcH#Kgt@J@3sk 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

      (i=VwrMIPqlRmr& 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 literal 215942 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 z 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 diff --git a/frontend/editor/src/core/assets/brand/modern-logo/logo512.png b/frontend/editor/src/core/assets/brand/modern-logo/logo512.png index b481550734d0cc9f0edcc223808a620a6cb08bc0..90934019e88a1ca460351a7e7501431fead127c0 100644 GIT binary patch literal 4596 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 V12%BJ*o^|fb(8zXn;UpP{2RbX@h$)W literal 8151 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 diff --git a/frontend/editor/src/core/assets/login/authentik.svg b/frontend/editor/src/core/assets/login/authentik.svg index 26dc0189ef..4ed18d49b4 100644 --- a/frontend/editor/src/core/assets/login/authentik.svg +++ b/frontend/editor/src/core/assets/login/authentik.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/login/github.svg b/frontend/editor/src/core/assets/login/github.svg index 1174b67928..41c82d53fe 100644 --- a/frontend/editor/src/core/assets/login/github.svg +++ b/frontend/editor/src/core/assets/login/github.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/login/microsoft.svg b/frontend/editor/src/core/assets/login/microsoft.svg index fc1130cbb2..691300857f 100644 --- a/frontend/editor/src/core/assets/login/microsoft.svg +++ b/frontend/editor/src/core/assets/login/microsoft.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/login/oidc.svg b/frontend/editor/src/core/assets/login/oidc.svg index 440b54487c..6c697d709a 100644 --- a/frontend/editor/src/core/assets/login/oidc.svg +++ b/frontend/editor/src/core/assets/login/oidc.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file From 9ef20dcab80b85041912f045e17a6aea1d08f969 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:25:01 +0000 Subject: [PATCH 114/154] Fix WebKit PDF-engine and storage failures, and catch them in cross-browser CI (#7366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes Follow-up to #7314, which fixed the IndexedDB blob rejection itself. This one fixes the remaining WebKit engine gaps, fixes the ways that class of failure surfaced to the user, and adds the cross-browser signal that would have caught them on the PR instead of six weeks later. ## Why this exists Two total WebKit outages sat on `main` for weeks: 1. pdf.js reads its text stream with `for await (… of readableStream)`, and WebKit has no `ReadableStream[Symbol.asyncIterator]`. **All** pdf.js text extraction threw `TypeError: undefined is not a function` — Compare, read-aloud and the PDF text editor were dead on Safari. 2. IndexedDB in WebKit rejects Blob/File values with `UnknownError: Error preparing Blob/File data to be stored in object store`, so nothing persisted and every reload came back empty. Neither was caught, because the existing specs never did the work. The Compare specs filled both slots and asserted the button was enabled; none of them clicked it. The persistence specs asserted a *filename* reappeared after a reload, which only needs the metadata record, not the bytes. Every failure here **looked like success** — empty panes, blank thumbnails, a `src` that was set but empty. That shapes the tests more than the fixes. ## WebKit engine gaps - **`ReadableStream[Symbol.asyncIterator]`**, installed at the entry point before any PDF work starts. The lock discipline is the subtle part: releasing is idempotent, is *not* done after a successful read, and *is* done in the read's error steps — `for await` never calls `return()` when `next()` rejects, so nothing else would ever unlock an errored stream. - **`requestIdleCallback`**, installed once instead of guarded at each call site. This one wasn't broken, it was mistimed: the local fallbacks fired at 200ms and 1000ms, landing the pdfium WASM compile on top of the app's first renders. The shim honours the caller's full timeout, so `{timeout: 2000}` means 2000ms. - **`convertToBlob()` does not fail on a format it can't encode.** Per spec it silently serialises to PNG, so asking for WebP and getting PNG back looks like success. Canvas output now probes what the engine really produced (once per realm) and uses the best lossy format it honours. PNG of a rendered page is several times the size of the equivalent WebP or JPEG, held as object URLs for every page on screen, on the engine with the tightest renderer memory budget. ## WebKit storage failures These read as generic transaction hygiene. They aren't — a refused blob write **aborts its transaction**, which is the mechanism that turned a WebKit rejection into a hang. - **Blob refusal is remembered from any write**, not just the initial `add`. WebKit reports it when it can't write the blob's *backing file*, which is per-operation — an engine that accepted the add can still refuse the rewrite, and every read-modify-write rewrites the record with its body attached. - **Aborted transactions no longer hang.** Read-modify-write moves to a single `updateRecord` helper that owns its transaction, guards it once, and resolves on **commit** rather than on the put's `onsuccess`. The previous shape — two promises over one shared transaction, with an `await` between the get and the put — put the abort guard on the read, leaving the write with no handler at all. `persistVersionedOutputs` awaits that, and `.catch` can't rescue a promise that never settles, so tool outputs could silently stop persisting. - **Stored blobs are no longer re-wrapped on read.** Since #7175 the record holds the `File` itself; wrapping it in `new Blob([record.data])` can cost WebKit the backing handle, giving you an object that looks valid and reads as empty. - **The file sidebar reaches a resting state** when the library can't be read, instead of spinning forever on a rejection nobody observes. It carries on with the in-memory workbench files: an unreadable library should cost the user their history, not the file they're working on. - **Thumbnail failures are logged.** Three `catch {}` blocks returned `""`, and an empty thumbnail is indistinguishable from "this file has no preview" — which is how outage #1 hid as a cosmetic nicety. ## CI `main` now runs the whole stubbed suite once per engine (#7304), so the new `@engine-capability` specs get chromium, firefox and webkit for free. They assert the primitives actually work — a **counted** comparison, a raster thumbnail data URL with real payload, and a page rendered from a file restored by a reload — rather than that the UI rendered. Deliberately small: anything added there is paid for three times per PR, so add depth, not breadth. Run them alone with `task e2e:cross-browser -- --grep @engine-capability`. The cross-browser projects now share the stubbed project's viewport. At the device presets' default 1280x720 a layout difference would fail these specs on Firefox/WebKit only, which reads as an engine outage. `vite.config.ts` gains a `worker.plugins` entry so `@app/*` resolves inside worker bundles. Worker bundles are a separate Rollup pass and don't inherit `plugins`, so the alias worked in the app and failed in a worker — previously worked around with a relative import plus a lint exemption, which silently bypasses the layer cascade. ## Verification - `task frontend:check` green: typecheck, oxlint, theme lint, stylelint, prettier, 215 test files / 1841 tests. - The `@engine-capability` suite passes on Chromium and WebKit locally. - **Negative control:** with the `ReadableStream` shim removed, the WebKit comparison spec fails at the Deletions/Additions assertion — the exact reported Safari symptom. Restored, and it passes. Both the fix and the test that guards it are load-bearing. - The worker alias change verified both ways: the build inlines the encoding probe into the worker chunk, and removing `worker.plugins` fails with `Rollup failed to resolve import "@app/utils/canvasImageEncoding"`. - The abort regression test aborts the transaction mid-write and asserts `markFileAsProcessed` settles. Before the fix it never settles and the test times out. ## Split out of this PR Two things in earlier revisions of this branch were engine-agnostic — found via the same symptom, not the same cause — and now have their own PRs: - **#7416** — blocked IndexedDB upgrades hanging the file library (multi-tab lifecycle, the concurrent-open race, `onversionchange`). - **#7417** — the thumbnail TTL rewriting the whole library on every listing. `FileSidebar`'s try/catch appears in both this PR and #7416, identically: a WebKit rejection and a blocked-open rejection both have to stop stranding the spinner. Whichever merges second is a no-op for that file. ## Known gaps - The blob-refused **rewrite** recovery in `updateRecord` isn't unit-tested. `fake-indexeddb` never returns Blob values from a read, so the branch that converts to a copy can't be reached there. Noted in the test file. - For the same reason, `fileFromRecord`'s "hand the stored File back untouched" path is only covered on a real engine, by the reload spec. - Nothing asserts that `src/index.tsx` imports the shims. The unit suite installs the same module via `setupTests.ts` (jsdom has the same gaps WebKit does), so a future regression where the entry point drops the import would still be green under vitest. - `FileSidebar`'s resting-state fix loses its E2E coverage until #7416 lands — forcing WebKit's blob refusal from a spec isn't practical, which is why that spec blocks the database instead. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] My changes generate no new warnings ### Documentation - [x] 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) --- frontend/.storybook/main.ts | 32 +- frontend/editor/playwright.config.ts | 15 +- .../public/locales/en-US/translation.toml | 4 + .../fileEditor/FileEditorThumbnail.tsx | 7 +- .../src/core/components/layout/Workbench.tsx | 18 +- .../core/components/shared/FileSidebar.tsx | 105 ++- .../components/shared/FileSidebarFileItem.css | 10 + .../components/shared/FileSidebarFileItem.tsx | 20 + .../shared/PolicyEnforcingOverlay.tsx | 3 + .../editor/src/core/contexts/FileContext.tsx | 16 + .../src/core/contexts/FilesPageContext.tsx | 5 +- .../core/contexts/file/FileReducer.test.ts | 37 + .../src/core/contexts/file/FileReducer.ts | 33 +- .../src/core/contexts/file/fileActions.ts | 118 +-- .../contexts/file/hydrationPublish.test.ts | 77 ++ .../editor/src/core/hooks/useFileManager.ts | 12 + .../services/fileStorage.blobFallback.test.ts | 421 ++++++++++- .../editor/src/core/services/fileStorage.ts | 692 +++++++++++++----- .../services/indexedDBManager.blocked.test.ts | 79 ++ .../src/core/services/indexedDBManager.ts | 129 +++- .../src/core/services/pdfiumInit.test.ts | 75 ++ .../editor/src/core/services/pdfiumService.ts | 83 +-- frontend/editor/src/core/setupTests.ts | 4 + .../tests/convert/ConvertIntegration.test.tsx | 2 + .../ConvertSmartDetectionIntegration.test.tsx | 2 + .../src/core/tests/stubbed/compare.spec.ts | 3 + .../tests/stubbed/engine-capabilities.spec.ts | 138 ++++ .../src/core/tools/formFill/FormFill.tsx | 8 +- frontend/editor/src/core/types/fileContext.ts | 6 + .../core/utils/canvasImageEncoding.test.ts | 91 +++ .../src/core/utils/canvasImageEncoding.ts | 45 ++ frontend/editor/src/core/utils/engineShims.ts | 4 + .../patchReadableStreamAsyncIterator.test.ts | 93 +++ .../utils/patchReadableStreamAsyncIterator.ts | 58 ++ .../utils/patchRequestIdleCallback.test.ts | 92 +++ .../core/utils/patchRequestIdleCallback.ts | 37 + .../editor/src/core/utils/thumbnailUtils.ts | 17 +- .../src/core/workers/pixelCompareWorker.ts | 7 +- frontend/editor/src/index.tsx | 12 +- frontend/editor/src/portal/setupTests.ts | 3 + .../policies/policyRunSettles.test.ts | 51 ++ .../components/policies/usePolicyAutoRun.ts | 24 +- .../src/proprietary/utils/scheduleIdle.ts | 13 +- frontend/editor/src/saas/setupTests.ts | 3 + frontend/editor/vite.config.ts | 5 + 45 files changed, 2305 insertions(+), 404 deletions(-) create mode 100644 frontend/editor/src/core/contexts/file/hydrationPublish.test.ts create mode 100644 frontend/editor/src/core/services/indexedDBManager.blocked.test.ts create mode 100644 frontend/editor/src/core/services/pdfiumInit.test.ts create mode 100644 frontend/editor/src/core/tests/stubbed/engine-capabilities.spec.ts create mode 100644 frontend/editor/src/core/utils/canvasImageEncoding.test.ts create mode 100644 frontend/editor/src/core/utils/canvasImageEncoding.ts create mode 100644 frontend/editor/src/core/utils/engineShims.ts create mode 100644 frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.test.ts create mode 100644 frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.ts create mode 100644 frontend/editor/src/core/utils/patchRequestIdleCallback.test.ts create mode 100644 frontend/editor/src/core/utils/patchRequestIdleCallback.ts create mode 100644 frontend/editor/src/proprietary/components/policies/policyRunSettles.test.ts diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index c0436a3a82..e86a8636e3 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -10,6 +10,18 @@ import tsconfigPaths from "vite-tsconfig-paths"; * the portal layer at editor/src/portal/). MDX docs pages live in * editor/src/portal/docs/. */ +/** + * Editor stories import via `@app/*` (proprietary→core fallback), `@core/*` and + * `@proprietary/*`. Resolve them exactly the way the editor's own build does - + * through vite-tsconfig-paths against the proprietary vite tsconfig - so the + * shared Storybook can host editor components without duplicating the alias map + * here. Built per pass: the main bundle and the worker bundle each need their own. + */ +const editorPathAliases = () => + tsconfigPaths({ + projects: [resolve(__dirname, "../editor/tsconfig.proprietary.vite.json")], + }); + const config: StorybookConfig = { stories: [ "../editor/src/portal/**/*.mdx", @@ -47,19 +59,15 @@ const config: StorybookConfig = { // than a relative path. "@public": resolve(__dirname, "../editor/public"), }; - // Editor stories import via @app/* (proprietary→core fallback), @core/* and - // @proprietary/*. Resolve them exactly the way the editor's own build does — - // through vite-tsconfig-paths against the proprietary vite tsconfig — so the - // shared Storybook can host editor components without duplicating the alias - // map here. config.plugins = config.plugins ?? []; - config.plugins.push( - tsconfigPaths({ - projects: [ - resolve(__dirname, "../editor/tsconfig.proprietary.vite.json"), - ], - }), - ); + config.plugins.push(editorPathAliases()); + // Worker bundles are a separate Rollup pass and do NOT inherit `plugins`, so + // without this a worker importing @app/* fails to resolve while the same + // import works everywhere else. Mirrors editor/vite.config.ts. + config.worker = { + ...(config.worker ?? {}), + plugins: () => [editorPathAliases()], + }; // Point apiClient.saas at a mock origin so the SaaS-backed billing stories // (SubscribedPlanView, PaymentMethodCard, InvoicesList) resolve a base URL and // their MSW handlers (which match "*/api/v1/payg/...") can intercept. The host diff --git a/frontend/editor/playwright.config.ts b/frontend/editor/playwright.config.ts index 93e6572392..c4a3885b15 100644 --- a/frontend/editor/playwright.config.ts +++ b/frontend/editor/playwright.config.ts @@ -17,9 +17,12 @@ import { defineConfig, devices } from "@playwright/test"; * * @see https://playwright.dev/docs/test-configuration */ +/** Shared by every stubbed project so a spec sees one layout on all engines. */ +const STUBBED_VIEWPORT = { width: 1920, height: 1080 }; + const chromiumViewport = { ...devices["Desktop Chrome"], - viewport: { width: 1920, height: 1080 }, + viewport: STUBBED_VIEWPORT, }; export default defineConfig({ @@ -55,7 +58,8 @@ export default defineConfig({ }, projects: [ - // Stubbed - no backend required, chromium-only for CI speed + // Stubbed - no backend required. The chromium arm of the cross-browser + // set below; CI fans all three out, one job per engine. { name: "stubbed", testDir: "./src/core/tests/stubbed", @@ -93,16 +97,17 @@ export default defineConfig({ }, }, - // Cross-browser coverage for the stubbed suite (opt-in locally) + // Cross-browser coverage for the stubbed suite. Same viewport as `stubbed`, + // or a layout difference here reads as an engine outage. { name: "stubbed-firefox", testDir: "./src/core/tests/stubbed", - use: { ...devices["Desktop Firefox"] }, + use: { ...devices["Desktop Firefox"], viewport: STUBBED_VIEWPORT }, }, { name: "stubbed-webkit", testDir: "./src/core/tests/stubbed", - use: { ...devices["Desktop Safari"] }, + use: { ...devices["Desktop Safari"], viewport: STUBBED_VIEWPORT }, }, ], diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index d6b38bc2e8..bebe1b392d 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -3890,6 +3890,8 @@ addFiles = "Add files" addingFiles = "Adding files…" collapse = "Collapse sidebar" customizeGroups = "Customize groups" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" dropHint = "Open files to get started" dropToAdd = "Drop files to add" expand = "Expand sidebar" @@ -3908,6 +3910,8 @@ viewAll = "View all {{count}} files" [fileSidebar.fileItem] closeViewer = "Close viewer" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" moreActions = "More actions" openInViewer = "Open in viewer" diff --git a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx index 1a55c53a90..22c969e704 100644 --- a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -300,6 +300,10 @@ const FileEditorThumbnail = ({ const [showVersionHistory, setShowVersionHistory] = useState(false); const policyEnforcing = policies.some((p) => p.enforcing); + // The overlay swallows clicks, so a run that never settles would leave the card + // unusable with no way out. Dismissible, like the viewer's; resets per run. + const [enforcingDismissed, setEnforcingDismissed] = useState(false); + if (!policyEnforcing && enforcingDismissed) setEnforcingDismissed(false); // The policy currently enforcing, so the overlay's icon/spinner match that // policy's badge instead of a fixed blue. const enforcingPolicy = policies.find((p) => p.enforcing); @@ -548,8 +552,9 @@ const FileEditorThumbnail = ({ {/* Policy enforcement overlay — shown while any policy is in-flight */} setEnforcingDismissed(true)} accentVar={enforcingPolicy?.accentColor} categoryId={enforcingPolicy?.id} /> diff --git a/frontend/editor/src/core/components/layout/Workbench.tsx b/frontend/editor/src/core/components/layout/Workbench.tsx index 9f54e224e8..7dbc6f75be 100644 --- a/frontend/editor/src/core/components/layout/Workbench.tsx +++ b/frontend/editor/src/core/components/layout/Workbench.tsx @@ -1,7 +1,7 @@ import { useState, Suspense, lazy } from "react"; import { useTranslation } from "react-i18next"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import { Box, Loader, Center } from "@mantine/core"; +import { Box, Loader, Center, Stack, Text } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; import { useFileHandler } from "@app/hooks/useFileHandler"; @@ -44,7 +44,7 @@ export default function Workbench() { useCookieConsent({ analyticsEnabled: config?.enableAnalytics === true }); // Use context-based hooks to eliminate all prop drilling - const { files: activeFiles } = useAllFiles(); + const { files: activeFiles, fileIds } = useAllFiles(); const { workbench: currentView } = useNavigationState(); const { actions: navActions } = useNavigationActions(); const setCurrentView = navActions.setWorkbench; @@ -134,6 +134,20 @@ export default function Workbench() { } if (activeFiles.length === 0) { + // Files are open but their bytes are still loading (a cold PDF engine can + // take seconds). Showing the drop zone here reads as "the click did nothing". + if (fileIds.length > 0) { + return ( +

      + + + + {t("fileManager.loadingFiles", "Loading files...")} + + +
      + ); + } return ; } diff --git a/frontend/editor/src/core/components/shared/FileSidebar.tsx b/frontend/editor/src/core/components/shared/FileSidebar.tsx index f74e96220f..2514e7f556 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebar.tsx @@ -59,7 +59,8 @@ import { deleteServerFile, type DeleteScope, } from "@app/services/serverStorageDelete"; -import { fileStorage } from "@app/services/fileStorage"; +import { fileStorage, onRecordUnreadable } from "@app/services/fileStorage"; +import { alert } from "@app/components/toast"; import { useBulkAddProgress } from "@app/services/bulkAddProgress"; import { useFolderMembership } from "@app/hooks/useFolderMembership"; import { useAllWatchedFolders } from "@app/hooks/useAllWatchedFolders"; @@ -280,6 +281,19 @@ const FileSidebar = forwardRef( // Leaf files = user-visible files (excludes intermediate tool outputs) const [allFileStubs, setAllFileStubs] = useState([]); + // Files whose stored bytes this session PROVED unreadable. Rows render a + // "data lost" state instead of pretending the file can open; storage keeps + // the record so a reload re-tests it. + const [lostFileIds, setLostFileIds] = useState>( + () => new Set(), + ); + useEffect( + () => + onRecordUnreadable((fileId) => + setLostFileIds((prev) => new Set(prev).add(fileId as string)), + ), + [], + ); const [stubsLoaded, setStubsLoaded] = useState(false); // Kebab "Save to cloud" target; drives BulkUploadToServerModal. const [saveToServerTarget, setSaveToServerTarget] = useState< @@ -298,32 +312,45 @@ const FileSidebar = forwardRef( const storageEnabled = config?.storageEnabled === true && !isAnonymous; const refreshStubs = useCallback(async () => { - // Leaf files from IDB - same source as the file selection modal. - const stubs = await indexedDB.loadLeafMetadata(); - const idbIds = new Set(stubs.map((s) => s.id as string)); + // `stubsLoaded` gates the spinner, so the `finally` below must set it on + // every path - callers never await this, so a rejection goes nowhere. + let stubs: StirlingFileStub[] = []; + try { + // Leaf files from IDB - same source as the file selection modal. + stubs = await indexedDB.loadLeafMetadata(); + } catch (error) { + // Carry on with the in-memory workbench files: an unreadable library + // should cost the user their history, not the file they're working on. + console.error("Failed to read the file library from storage:", error); + } - // Also include workbench files not yet flushed to IDB. - const pendingStubs = state.files.ids - .map((id) => state.files.byId[id]) - .filter( - (stub): stub is NonNullable => - !!stub && stub.isLeaf !== false && !idbIds.has(stub.id as string), + try { + const idbIds = new Set(stubs.map((s) => s.id as string)); + + // Also include workbench files not yet flushed to IDB. + const pendingStubs = state.files.ids + .map((id) => state.files.byId[id]) + .filter( + (stub): stub is NonNullable => + !!stub && stub.isLeaf !== false && !idbIds.has(stub.id as string), + ); + + const allStubs = [...stubs, ...pendingStubs]; + // A version swap briefly lists both the old leaf (IDB) and its replacement (workbench); two stubs for one lineage collide on the row key and corrupt React reconciliation, so drop any stub another names as its parent. + const superseded = new Set( + allStubs.map((s) => s.parentFileId as string | undefined), ); - - const allStubs = [...stubs, ...pendingStubs]; - // A version swap briefly lists both the old leaf (IDB) and its replacement (workbench); two stubs for one lineage collide on the row key and corrupt React reconciliation, so drop any stub another names as its parent. - const superseded = new Set( - allStubs.map((s) => s.parentFileId as string | undefined), - ); - const currentStubs = allStubs.filter( - (s) => !superseded.has(s.id as string), - ); - setAllFileStubs( - currentStubs.sort( - (a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0), - ), - ); - setStubsLoaded(true); + const currentStubs = allStubs.filter( + (s) => !superseded.has(s.id as string), + ); + setAllFileStubs( + currentStubs.sort( + (a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0), + ), + ); + } finally { + setStubsLoaded(true); + } }, [indexedDB, state.files.ids, state.files.byId]); // Refresh on mount, workbench changes, or external IndexedDB writes — @@ -362,7 +389,9 @@ const FileSidebar = forwardRef( setDeleteTarget(stub); return; } - await fileActions.removeFiles([fileId], true); + // Its superseded versions go too - see orphanedAncestorIds. + const orphans = await fileStorage.orphanedAncestorIds([fileId]); + await fileActions.removeFiles([fileId, ...orphans], true); await refreshStubs(); }, [allFileStubs, fileActions, refreshStubs], @@ -380,7 +409,8 @@ const FileSidebar = forwardRef( await deleteServerFile(stub.remoteStorageId); } if (scope === "device" || scope === "everywhere") { - await fileActions.removeFiles([stub.id], true); + const orphans = await fileStorage.orphanedAncestorIds([stub.id]); + await fileActions.removeFiles([stub.id, ...orphans], true); } else if (scope === "cloud") { // Local copy kept - drop the dead remote pointer so the cloud badge // clears (the sidebar doesn't reconcile with the server itself). @@ -484,6 +514,22 @@ const FileSidebar = forwardRef( const stub = allFileStubs.find((s) => s.id === fileId); if (!stub) return; + // Its bytes are gone; opening it can only fail. Say so instead of a + // click that goes nowhere. + if (stub.dataUnavailable || lostFileIds.has(fileId as string)) { + alert({ + alertType: "warning", + title: t("fileSidebar.dataLostTitle", "File data is unavailable"), + body: t( + "fileSidebar.dataLostBody", + "This browser lost this file's contents. Upload it again to keep working with it.", + ), + expandable: false, + durationMs: 6000, + }); + return; + } + // In the Watched Folders view a click sends the file into the open folder // (mirrors how a click toggles a file into the active workbench elsewhere). // On the folder list (no folder open) it's a no-op so browsing isn't disrupted. @@ -538,6 +584,8 @@ const FileSidebar = forwardRef( }, [ allFileStubs, + lostFileIds, + t, state.files.ids, state.ui.selectedFileIds, fileActions, @@ -725,6 +773,8 @@ const FileSidebar = forwardRef( ? state.files.byId[workbenchFileId]?.thumbnailUrl : undefined) || stub.thumbnailUrl; const fileOrigin = getFileOrigin(stub); + const dataUnavailable = + stub.dataUnavailable === true || lostFileIds.has(stub.id as string); // Key by lineage (originalFileId) so a version swap updates the row in place instead of // remounting. But a 1-input→many-output op (split) yields sibling leaves that share one // originalFileId; those would collide on the key, so fall back to the unique leaf id when a @@ -747,6 +797,7 @@ const FileSidebar = forwardRef( thumbnailUrl={thumbnailUrl} onClick={handleFileClick} onEyeClick={handleEyeClick} + dataUnavailable={dataUnavailable} draggable={isWatchedFoldersActive} onDragStart={handleWatchedFolderDragStart} folders={memberFolders} diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.css b/frontend/editor/src/core/components/shared/FileSidebarFileItem.css index 794ed4ca97..874bf47e28 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.css +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.css @@ -447,3 +447,13 @@ transform: translateY(-50%) scale(1); } } + +/* The stored bytes are gone - the row says so instead of pretending to open. */ +.file-sidebar-datalost-badge { + display: inline-flex; + align-items: center; + gap: 0.15rem; + color: var(--c-danger); + font-size: 0.7rem; + white-space: nowrap; +} diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx index c1a93594c5..2b06c27e56 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx @@ -9,6 +9,7 @@ import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import CloudUploadOutlinedIcon from "@mui/icons-material/CloudUploadOutlined"; import CloudDoneIcon from "@mui/icons-material/CloudDone"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlineOutlined"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined"; import HistoryIcon from "@mui/icons-material/History"; import type { FileId } from "@app/types/file"; @@ -163,6 +164,9 @@ export interface FileItemProps { onVersionHistory?: (fileId: FileId) => void; /** Whether this file has more than one version (drives the menu item). */ hasVersionHistory?: boolean; + /** The stored bytes are gone (WebKit lost the blob's backing store). The row + * says so instead of pretending the file can open. */ + dataUnavailable?: boolean; } const MAX_VISIBLE_FOLDER_TAGS = 2; @@ -177,6 +181,7 @@ export const FileItem = React.memo(function FileItem({ isSelected, isActive, isViewedInViewer, + dataUnavailable, thumbnailUrl, onClick, onEyeClick, @@ -294,6 +299,21 @@ export const FileItem = React.memo(function FileItem({ )} + {dataUnavailable && ( + + + + {t("fileSidebar.fileItem.dataLost", "Data lost")} + + + )} {isUploadedToCloud && ( void; }) { return null; } diff --git a/frontend/editor/src/core/contexts/FileContext.tsx b/frontend/editor/src/core/contexts/FileContext.tsx index a982c347ca..c627d61f35 100644 --- a/frontend/editor/src/core/contexts/FileContext.tsx +++ b/frontend/editor/src/core/contexts/FileContext.tsx @@ -58,6 +58,7 @@ import { IndexedDBProvider, useIndexedDB, } from "@app/contexts/IndexedDBContext"; +import { onRecordUnreadable } from "@app/services/fileStorage"; import { useZipConfirmation } from "@app/hooks/useZipConfirmation"; import ZipWarningModal from "@app/components/shared/ZipWarningModal"; import EncryptedPdfUnlockModal from "@app/components/shared/EncryptedPdfUnlockModal"; @@ -186,6 +187,21 @@ function FileContextInner({ setUnlockError(null); }, [activeEncryptedFileId]); + // Storage proved a file's bytes unreadable (WebKit losing a blob's backing + // store). Drop it: the viewer would otherwise spin on a document that can + // never load. The record stays, so a reload re-tests it. + useEffect( + () => + onRecordUnreadable((fileId) => { + if (!stateRef.current.files.byId[fileId]) return; + console.error( + `[FileContext] dropping ${fileId} from the workbench: its stored bytes are unreadable`, + ); + lifecycleManager.removeFiles([fileId], stateRef); + }), + [lifecycleManager], + ); + const handleUnlockSkip = useCallback(() => { if (activeEncryptedFileId) { dismissedEncryptedFilesRef.current.add(activeEncryptedFileId); diff --git a/frontend/editor/src/core/contexts/FilesPageContext.tsx b/frontend/editor/src/core/contexts/FilesPageContext.tsx index 6774798aae..b8cf4ccf29 100644 --- a/frontend/editor/src/core/contexts/FilesPageContext.tsx +++ b/frontend/editor/src/core/contexts/FilesPageContext.tsx @@ -455,7 +455,10 @@ export function FilesPageProvider({ children }: { children: React.ReactNode }) { }) .map((s) => s.id); if (localIds.length > 0) { - await fileActions.removeFiles(localIds, true); + // Take the superseded versions with it, or their bytes sit in storage + // forever - invisible, because listings only show leaves. + const orphans = await fileStorage.orphanedAncestorIds(localIds); + await fileActions.removeFiles([...localIds, ...orphans], true); } } diff --git a/frontend/editor/src/core/contexts/file/FileReducer.test.ts b/frontend/editor/src/core/contexts/file/FileReducer.test.ts index 523ef2e358..bde6a17a02 100644 --- a/frontend/editor/src/core/contexts/file/FileReducer.test.ts +++ b/frontend/editor/src/core/contexts/file/FileReducer.test.ts @@ -222,3 +222,40 @@ describe("fileContextReducer — silent CONSUME_FILES (background enforcement)", expect(next.ui.selectedFileIds).toEqual(["b2"]); }); }); + +describe("fileContextReducer — REMOVE_FILES", () => { + /** Deleting from the library dispatches this for files that were never in the + * workbench; reallocating then re-renders every consumer for nothing. */ + it("is a true no-op when none of the ids are in the workbench", () => { + const state = stateWith([stub("a")]); + const next = fileContextReducer(state, { + type: "REMOVE_FILES", + payload: { fileIds: ["gone" as FileId] }, + }); + expect(next).toBe(state); + }); + + it("still removes the ids it does hold", () => { + const state = stateWith([stub("a"), stub("b")]); + const next = fileContextReducer(state, { + type: "REMOVE_FILES", + payload: { fileIds: ["a" as FileId, "gone" as FileId] }, + }); + expect(next.files.ids).toEqual(["b"]); + expect(next.files.byId["a" as FileId]).toBeUndefined(); + }); + + it("keeps the files slice when only a selection is cleared", () => { + const base = stateWith([stub("a")]); + const state: FileContextState = { + ...base, + ui: { ...base.ui, selectedFileIds: ["gone" as FileId] }, + }; + const next = fileContextReducer(state, { + type: "REMOVE_FILES", + payload: { fileIds: ["gone" as FileId] }, + }); + expect(next.files).toBe(state.files); + expect(next.ui.selectedFileIds).toEqual([]); + }); +}); diff --git a/frontend/editor/src/core/contexts/file/FileReducer.ts b/frontend/editor/src/core/contexts/file/FileReducer.ts index 2697974823..a9dcae7b38 100644 --- a/frontend/editor/src/core/contexts/file/FileReducer.ts +++ b/frontend/editor/src/core/contexts/file/FileReducer.ts @@ -183,6 +183,20 @@ export function fileContextReducer( const remainingIds = state.files.ids.filter( (id) => !fileIds.includes(id), ); + // Clear selections that reference removed files + const validSelectedFileIds = state.ui.selectedFileIds.filter( + (id) => !fileIds.includes(id), + ); + + // Deleting a library file that was never in the workbench removes nothing + // here, and must not re-render every file and UI consumer. + const removedFromWorkbench = + remainingIds.length !== state.files.ids.length || + fileIds.some((id) => id in state.files.byId); + const deselected = + validSelectedFileIds.length !== state.ui.selectedFileIds.length; + if (!removedFromWorkbench && !deselected) return state; + const newById = { ...state.files.byId }; // Remove files from state (resource cleanup handled by lifecycle manager) @@ -190,21 +204,14 @@ export function fileContextReducer( delete newById[id]; }); - // Clear selections that reference removed files - const validSelectedFileIds = state.ui.selectedFileIds.filter( - (id) => !fileIds.includes(id), - ); - return { ...state, - files: { - ids: remainingIds, - byId: newById, - }, - ui: { - ...state.ui, - selectedFileIds: validSelectedFileIds, - }, + files: removedFromWorkbench + ? { ids: remainingIds, byId: newById } + : state.files, + ui: deselected + ? { ...state.ui, selectedFileIds: validSelectedFileIds } + : state.ui, }; } diff --git a/frontend/editor/src/core/contexts/file/fileActions.ts b/frontend/editor/src/core/contexts/file/fileActions.ts index a95f42939d..4067a99069 100644 --- a/frontend/editor/src/core/contexts/file/fileActions.ts +++ b/frontend/editor/src/core/contexts/file/fileActions.ts @@ -26,6 +26,9 @@ import { clearBulkAddProgress, } from "@app/services/bulkAddProgress"; const DEBUG = process.env.NODE_ENV === "development"; +/** How long a file may sit unhydrated before the console says so. Reporting only: + * the read is never abandoned, because large files legitimately take time. */ +const STALLED_LOAD_MS = 8000; const HYDRATION_CONCURRENCY = 2; let activeHydrations = 0; const hydrationQueue: Array<() => Promise> = []; @@ -854,61 +857,78 @@ export async function addStirlingFileStubs( // Load File object and hydrate metadata in background (non-blocking) const fileId = stub.id; - // Load File object from IndexedDB asynchronously - scheduleMetadataHydration(async () => { - const stirlingFile = await fileStorage.getStirlingFile(fileId); + // Regenerate page metadata + thumbnails. Queued, because parsing several + // PDFs at once is what the concurrency limit exists to bound. + const scheduleMetadataFor = (stirlingFile: StirlingFile): void => { + scheduleMetadataHydration(async () => { + const processedFileMetadata = + await generateProcessedFileMetadata(stirlingFile); + if (!processedFileMetadata) return; + + const updates: Partial = { + processedFile: processedFileMetadata, + }; + + // Update thumbnail only if current stub doesn't have one + const currentStub = stateRef.current.files.byId[fileId]; + if ( + !currentStub?.thumbnailUrl && + processedFileMetadata.thumbnailUrl + ) { + updates.thumbnailUrl = processedFileMetadata.thumbnailUrl; + if (processedFileMetadata.thumbnailUrl.startsWith("blob:")) { + lifecycleManager.trackBlobUrl(processedFileMetadata.thumbnailUrl); + } + } + + lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef); + }); + }; + + // Load and publish the File, ahead of any parsing. NOT queued: whether a + // file opens at all must not wait on other files' parses. + void (async () => { + // A storage read that never settles renders as a file that silently won't + // open. Name it in the console rather than leaving the user guessing. + const stall = setTimeout( + () => + console.error( + `[Hydration] ${stub.name} (${fileId}) has been loading for ${STALLED_LOAD_MS / 1000}s - the IndexedDB read has not settled`, + ), + STALLED_LOAD_MS, + ); + const stirlingFile = await fileStorage + .getStirlingFile(fileId) + .finally(() => clearTimeout(stall)); if (!stirlingFile) { + // A row with no bytes renders empty and its clicks look dead, so take it + // back out. Storage keeps the record; fileStorage has said why. + console.error( + `[Hydration] No readable data for ${stub.name} (${fileId}); removing it from the workbench`, + ); + lifecycleManager.removeFiles([fileId], stateRef); return; } - // Store the loaded file in filesRef filesRef.current.set(fileId, stirlingFile); - - // Check if processedFile data needs regeneration - if (stirlingFile.type.startsWith("application/pdf")) { - const needsProcessing = - !stub.processedFile || - !stub.processedFile.pages || - stub.processedFile.pages.length === 0 || - stub.processedFile.totalPages !== stub.processedFile.pages.length; - - if (needsProcessing) { - // Regenerate metadata - const processedFileMetadata = - await generateProcessedFileMetadata(stirlingFile); - - if (processedFileMetadata) { - const updates: Partial = { - processedFile: processedFileMetadata, - }; - - // Update thumbnail only if current stub doesn't have one - const currentStub = stateRef.current.files.byId[fileId]; - if ( - !currentStub?.thumbnailUrl && - processedFileMetadata.thumbnailUrl - ) { - updates.thumbnailUrl = processedFileMetadata.thumbnailUrl; - if (processedFileMetadata.thumbnailUrl.startsWith("blob:")) { - lifecycleManager.trackBlobUrl( - processedFileMetadata.thumbnailUrl, - ); - } - } - - lifecycleManager.updateStirlingFileStub( - fileId, - updates, - stateRef, - ); - return; - } - } - } - - // Stub dispatch triggers re-render so the viewer appears (ADD_FILES alone doesn't update selectors). + // filesRef is a ref, so the selectors gating the workbench only see the + // file once something dispatches. Parsing it can't be a precondition. lifecycleManager.updateStirlingFileStub(fileId, {}, stateRef); - }); + + const needsProcessing = + !stub.processedFile || + !stub.processedFile.pages || + stub.processedFile.pages.length === 0 || + stub.processedFile.totalPages !== stub.processedFile.pages.length; + if ( + stirlingFile.type.startsWith("application/pdf") && + needsProcessing + ) { + scheduleMetadataFor(stirlingFile); + } + })().catch((error) => + console.error(`[Hydration] Failed to load ${fileId}:`, error), + ); } return loadedFiles; diff --git a/frontend/editor/src/core/contexts/file/hydrationPublish.test.ts b/frontend/editor/src/core/contexts/file/hydrationPublish.test.ts new file mode 100644 index 0000000000..c4bdda01c1 --- /dev/null +++ b/frontend/editor/src/core/contexts/file/hydrationPublish.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, test, vi } from "vitest"; +import type { + FileContextState, + StirlingFileStub, +} from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +/** + * A clicked file is only visible once hydration DISPATCHES: the workbench reads + * files out of a ref, so `activeFiles` stays empty until then. Parsing must not + * gate that - a PDF engine that stalls used to leave the workbench on its empty + * state with the row showing as open, and clicks doing nothing. + */ + +const getStirlingFile = vi.hoisted(() => vi.fn()); +vi.mock("@app/services/fileStorage", () => ({ + fileStorage: { getStirlingFile }, +})); +/** The stall under test: the page parse never settles. */ +vi.mock("@app/utils/thumbnailUtils", () => ({ + generateThumbnailPairWithMetadata: () => new Promise(() => {}), +})); + +const stub = (id: string): StirlingFileStub => + ({ + id: id as FileId, + name: `${id}.pdf`, + type: "application/pdf", + size: 10, + lastModified: 0, + }) as StirlingFileStub; + +async function harness(ids: string[]) { + vi.resetModules(); + getStirlingFile.mockImplementation( + async (id: FileId) => + new File(["%PDF-1.7"], `${id}.pdf`, { type: "application/pdf" }), + ); + const { addStirlingFileStubs } = + await import("@app/contexts/file/fileActions"); + + const stubs = ids.map(stub); + const state = { + files: { ids: [], byId: {} }, + pinnedFiles: new Set(), + ui: { selectedFileIds: [], selectedPageNumbers: [] }, + } as unknown as FileContextState; + const stateRef = { current: state }; + const filesRef = { current: new Map() }; + const published: FileId[] = []; + const lifecycleManager = { + updateStirlingFileStub: (fileId: FileId) => published.push(fileId), + removeFiles: () => {}, + trackBlobUrl: () => {}, + }; + + await addStirlingFileStubs( + stubs, + {}, + stateRef, + filesRef, + () => {}, + lifecycleManager as never, + ); + return { filesRef, published }; +} + +describe("workbench hydration — a stalled parse can't hide the file", () => { + test("publishes every file's bytes while their parses hang", async () => { + // Three, because the parse queue only runs two at a time: the third proves + // loading isn't queued behind parses that never finish. + const { filesRef, published } = await harness(["a", "b", "c"]); + + await vi.waitFor(() => expect(published).toHaveLength(3)); + expect([...filesRef.current.keys()]).toEqual(["a", "b", "c"]); + }); +}); diff --git a/frontend/editor/src/core/hooks/useFileManager.ts b/frontend/editor/src/core/hooks/useFileManager.ts index 442027364e..4393a22d61 100644 --- a/frontend/editor/src/core/hooks/useFileManager.ts +++ b/frontend/editor/src/core/hooks/useFileManager.ts @@ -389,6 +389,18 @@ export const useFileManager = () => { // Optimistic update — remove from UI immediately, delete IDB in background setFiles(files.filter((_, i) => i !== index)); onRemovedFromWorkbench?.(file.id); + // Superseded versions go with it (see orphanedAncestorIds); best-effort, + // because failing to tidy history must not fail the delete itself. + void fileStorage + .orphanedAncestorIds([file.id]) + .then((orphans) => + orphans.length > 0 + ? fileStorage.deleteMultipleStirlingFiles(orphans) + : undefined, + ) + .catch((error) => + console.warn("Failed to remove superseded versions:", error), + ); indexedDB.deleteFile(file.id).catch((error) => { console.error("Failed to remove file from IndexedDB:", error); // Restore consistency — file is still in IDB so refresh brings it back diff --git a/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts b/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts index d8530af026..d6f1c4b89d 100644 --- a/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts +++ b/frontend/editor/src/core/services/fileStorage.blobFallback.test.ts @@ -3,23 +3,26 @@ import "fake-indexeddb/auto"; import { expectConsole } from "@app/tests/failOnConsole"; /** - * Regression test for the WebKit nightly breakage introduced with the - * large-file OOM fix (#7175): `storeStirlingFile` began putting the `File` - * itself into IndexedDB (persisted by reference, so multi-GB uploads never - * materialize in JS memory). WebKit refuses blob values whenever it can't write - * the blob's backing file and rejects the request with `UnknownError: Error - * preparing Blob/File data to be stored in object store`, so on WebKit every - * upload silently failed to persist: files vanished on navigation, Compare - * slots never filled, and the classification backfill had no bytes to read. + * WebKit refuses blob values when it can't write the blob's backing file, so + * every upload silently failed to persist after #7175. Retried as a copy now. * - * The service now retries such a rejection with an ArrayBuffer copy and stops - * offering blobs for the rest of the session. + * It can also accept one and then lose the backing store. fake-indexeddb returns no + * Blob, so that loss is injected at the read; real round-trips: the e2e spec. */ -const nativeAdd = IDBObjectStore.prototype.add; +const alertMock = vi.hoisted(() => vi.fn()); +vi.mock("@app/components/toast", () => ({ + alert: (options: unknown) => alertMock(options), +})); -/** What each `add` attempt carried in `data` — the blob path or the copy path. */ +const nativeAdd = IDBObjectStore.prototype.add; +const nativePut = IDBObjectStore.prototype.put; +const nativeGet = IDBObjectStore.prototype.get; + +/** What each `add` attempt carried in `data`: blob path or copy path. */ let attempts: Array<"blob" | "copy"> = []; +/** The same, for `put` - the rewrite path a lost backing store recovers through. */ +let putAttempts: Array<"blob" | "copy"> = []; /** An IDBRequest that fails asynchronously, the way WebKit rejects blob puts. */ class FailingRequest extends EventTarget { @@ -32,10 +35,7 @@ class FailingRequest extends EventTarget { } } -/** - * Record every add attempt, optionally failing the blob-valued ones the way an - * engine without blob storage does. - */ +/** Record every add, optionally failing the blob-valued ones. */ function instrumentAdd(options: { rejectBlobs: boolean }) { IDBObjectStore.prototype.add = function ( this: IDBObjectStore, @@ -58,11 +58,72 @@ function instrumentAdd(options: { rejectBlobs: boolean }) { } as typeof IDBObjectStore.prototype.add; } -/** - * A fresh service per test: whether the engine accepts blobs is remembered for - * the process lifetime by design, so tests must not inherit that decision from - * each other. - */ +/** Record every put, so the copy-rewrite recovery can be observed. */ +function instrumentPut() { + IDBObjectStore.prototype.put = function ( + this: IDBObjectStore, + value: unknown, + key?: IDBValidKey, + ) { + putAttempts.push( + (value as { data?: unknown } | null)?.data instanceof Blob + ? "blob" + : "copy", + ); + return key === undefined + ? nativePut.call(this, value) + : nativePut.call(this, value, key); + } as typeof IDBObjectStore.prototype.put; +} + +/** A stored blob whose backing store the engine has lost: it still reports a name, + * type and size, and every read of its bytes fails the way WebKit's does. */ +function blobWithLostBackingStore(): Blob { + const lost = () => { + throw new DOMException( + "The object can not be found here.", + "NotFoundError", + ); + }; + return Object.assign( + new Blob(["%PDF-1.7 stirling"], { type: "application/pdf" }), + { slice: lost, arrayBuffer: lost, text: lost, stream: lost }, + ); +} + +/** The next `deadReads` reads come back with a lost backing store, later ones + * untouched - so a repaired record can still be read normally. */ +function loseBackingStoreOnRead(deadReads: number) { + let remaining = deadReads; + IDBObjectStore.prototype.get = function ( + this: IDBObjectStore, + key: IDBValidKey | IDBKeyRange, + ) { + const request = nativeGet.call(this, key as IDBValidKey); + // One substitution per request, however often `result` is read. + let injected = false; + return new Proxy(request, { + get(target, prop) { + // Receiver must be the real request: IDBRequest's accessors are branded. + const value = Reflect.get(target, prop, target); + if (prop !== "result") { + return typeof value === "function" ? value.bind(target) : value; + } + if (!value || injected || remaining === 0) return value; + injected = true; + remaining--; + return { ...(value as object), data: blobWithLostBackingStore() }; + }, + set(target, prop, value) { + Reflect.set(target, prop, value, target); + return true; + }, + }); + } as typeof IDBObjectStore.prototype.get; +} + +/** A fresh service per test: the blob decision is remembered by design, so + * tests must not inherit it from each other. */ async function freshFileStorage() { vi.resetModules(); const [{ fileStorage }, { createStirlingFile, createNewStirlingFileStub }] = @@ -86,10 +147,58 @@ async function freshFileStorage() { beforeEach(() => { attempts = []; + putAttempts = []; + alertMock.mockClear(); + // The blob verdict is deliberately durable, so each test must start undecided. + localStorage.clear(); }); afterEach(() => { IDBObjectStore.prototype.add = nativeAdd; + IDBObjectStore.prototype.put = nativePut; + IDBObjectStore.prototype.get = nativeGet; +}); + +/** Abort the transaction the moment a write is issued over it. */ +function abortOnPut() { + IDBObjectStore.prototype.put = function (this: IDBObjectStore) { + const request = new FailingRequest( + new DOMException("transaction aborted", "AbortError"), + ) as unknown as IDBRequest; + this.transaction.abort(); + return request; + } as typeof IDBObjectStore.prototype.put; +} + +describe("read-modify-write — a refused rewrite must not hang or vanish", () => { + /** The abort guard used to sit on the read promise, leaving the write with a + * dead reject - and `.catch` can't rescue a promise that never settles. */ + test("settles instead of hanging when the write transaction aborts", async () => { + expectConsole.error(/Failed to mark file as processed/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + const id = await store("aborts.pdf"); + + abortOnPut(); + + // Before the fix this never settled and the test timed out. + await expect(fileStorage.markFileAsProcessed(id)).resolves.toBe(false); + }); + + /** The copy-and-retry recovery can't be exercised here: it needs a record that + * reads back as a Blob, which fake-indexeddb never returns. */ + test("a metadata rewrite still commits, and reports commit not put", async () => { + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + const id = await store("rewrite.pdf"); + + await expect(fileStorage.markFileAsProcessed(id)).resolves.toBe(true); + // Missing record: `false`, not a throw and not a claim of success. + await expect( + fileStorage.markFileAsProcessed("nope" as never), + ).resolves.toBe(false); + expect((await fileStorage.getStirlingFile(id))?.name).toBe("rewrite.pdf"); + }); }); describe("storeStirlingFile — blob-value fallback", () => { @@ -113,8 +222,7 @@ describe("storeStirlingFile — blob-value fallback", () => { const id = await store("webkit.pdf"); expect(attempts).toEqual(["blob", "copy"]); - // Readable back is what every downstream consumer depends on: rehydration - // after navigation, thumbnails, the classification backfill. + // Readable back is what rehydration, thumbnails and backfill depend on. expect((await fileStorage.getStirlingFile(id))?.name).toBe("webkit.pdf"); }); @@ -127,12 +235,32 @@ describe("storeStirlingFile — blob-value fallback", () => { attempts = []; const id = await store("second.pdf"); - // Straight to the copy path — no repeated blob probe, and only the single - // warning expected above. + // Straight to the copy path, and only the one warning expected above. expect(attempts).toEqual(["copy"]); expect((await fileStorage.getStirlingFile(id))?.name).toBe("second.pdf"); }); + /** Committing is not evidence the bytes survived, and by the next reload the + * source File is gone: without this the upload looks fine and the file is dead. */ + test("repairs a record whose stored blob loses its backing store", async () => { + expectConsole.warn(/could not read its bytes back/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + instrumentPut(); + loseBackingStoreOnRead(1); // only the store's own read-back is dead + + const id = await store("dead-on-arrival.pdf"); + + // Accepted as a blob, then rewritten from the file still in hand. + expect(attempts).toEqual(["blob"]); + expect(putAttempts).toEqual(["copy"]); + expect((await fileStorage.getStirlingFile(id))?.name).toBe( + "dead-on-arrival.pdf", + ); + // Self-healed, so nothing to tell the user about. + expect(alertMock).not.toHaveBeenCalled(); + }); + test("does not retry a failure a copy can't fix (quota)", async () => { const { store } = await freshFileStorage(); IDBObjectStore.prototype.add = function (this: IDBObjectStore) { @@ -144,3 +272,246 @@ describe("storeStirlingFile — blob-value fallback", () => { expect(attempts).toEqual(["blob"]); }); }); + +/** An earlier session's record can't be repaired, and the user has to be told - but + * the telling must never gate the open. Awaiting the probe stalled every file in + * Safari, where the probe read of a lost backing store never settles. */ +describe("reads — a stored blob whose bytes are gone", () => { + test("hands the file over and reports the loss out of band", async () => { + expectConsole.warn(/could not read its bytes back/); + expectConsole.error(/cannot be read/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + const id = await store("lost.pdf"); + + loseBackingStoreOnRead(5); // every read from here on + + // Not null, and not awaited on the probe: the caller is never blocked. + expect((await fileStorage.getStirlingFile(id))?.name).toBe("lost.pdf"); + await new Promise((resolve) => setTimeout(resolve)); + + // Told once, not once per reader: every consumer of the file hits this record. + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock.mock.calls[0][0]).toMatchObject({ + alertType: "warning", + body: expect.stringContaining("lost.pdf"), + }); + }); + + /** The loop this closes: a reload re-decides optimistically, writes blobs the + * engine loses again, and the browser never settles on a shape that works. */ + test("remembers across reloads that this browser loses blob values", async () => { + expectConsole.warn(/could not read its bytes back/); + expectConsole.error(/cannot be read/); + const first = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + const id = await first.store("lost.pdf"); + loseBackingStoreOnRead(1); + expect(await first.fileStorage.getStirlingFile(id)).not.toBeNull(); + await new Promise((resolve) => setTimeout(resolve)); + + // A new page load: a fresh service, same browser profile. + const next = await freshFileStorage(); + attempts = []; + const later = await next.store("after-reload.pdf"); + + expect(attempts).toEqual(["copy"]); + expect((await next.fileStorage.getStirlingFile(later))?.name).toBe( + "after-reload.pdf", + ); + }); + + test("stops offering blob values for the rest of the session", async () => { + expectConsole.warn(/could not read its bytes back/); + expectConsole.error(/cannot be read/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + const first = await store("lost.pdf"); + + loseBackingStoreOnRead(1); + expect(await fileStorage.getStirlingFile(first)).not.toBeNull(); + await new Promise((resolve) => setTimeout(resolve)); + + // An engine that loses a blob it accepted can't be trusted with the next one, + // so the read failure degrades writes too. + attempts = []; + const second = await store("later.pdf"); + expect(attempts).toEqual(["copy"]); + expect((await fileStorage.getStirlingFile(second))?.name).toBe("later.pdf"); + }); +}); + +/** Deleting a file used to leave its superseded versions in storage forever, + * invisible (listings filter on isLeaf) and still holding their full bytes. */ +describe("orphanedAncestorIds", () => { + const store = async ( + fileStorage: { storeStirlingFile: (f: never, s: never) => Promise }, + id: string, + parentFileId: string | undefined, + isLeaf: boolean, + ) => { + const { createStirlingFile, createNewStirlingFileStub } = + await import("@app/types/fileContext"); + const file = new File(["%PDF-1.7"], `${id}.pdf`, { + type: "application/pdf", + }); + const base = createNewStirlingFileStub(file); + await fileStorage.storeStirlingFile( + createStirlingFile(file, id as never) as never, + { ...base, id, isLeaf, parentFileId, originalFileId: "v1" } as never, + ); + }; + + test("takes the superseded versions with the leaf", async () => { + const { fileStorage } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + await store(fileStorage as never, "v1", undefined, false); + await store(fileStorage as never, "v2", "v1", true); + + expect(await fileStorage.orphanedAncestorIds(["v2" as never])).toEqual([ + "v1", + ]); + }); + + test("leaves a split sibling's history alone", async () => { + const { fileStorage } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + // Distinct ids: the fake database outlives the module reset between tests. + await store(fileStorage as never, "split-root", undefined, false); + await store(fileStorage as never, "split-a", "split-root", true); + await store(fileStorage as never, "split-b", "split-root", true); + + // `split-b` still descends from the root, so deleting `split-a` can't strip it. + expect(await fileStorage.orphanedAncestorIds(["split-a" as never])).toEqual( + [], + ); + // Once both leaves go, the shared ancestor is genuinely unreachable. + expect( + await fileStorage.orphanedAncestorIds([ + "split-a" as never, + "split-b" as never, + ]), + ).toEqual(["split-root"]); + }); +}); + +/** Handing dead bytes over is only safe if whoever holds them is told to let go - + * otherwise the viewer renders a document that never loads (an endless spinner). */ +describe("confirmed-unreadable records", () => { + test("notifies listeners and refuses to hand the same file out twice", async () => { + expectConsole.warn(/could not read its bytes back/); + expectConsole.error(/cannot be read/); + const { fileStorage, store } = await freshFileStorage(); + const { onRecordUnreadable } = await import("@app/services/fileStorage"); + instrumentAdd({ rejectBlobs: false }); + const id = await store("doomed.pdf"); + + const dropped: string[] = []; + const unsubscribe = onRecordUnreadable((fileId) => dropped.push(fileId)); + + loseBackingStoreOnRead(5); + // First read still hands the file over: the probe is out of band. + expect(await fileStorage.getStirlingFile(id)).not.toBeNull(); + await new Promise((resolve) => setTimeout(resolve)); + + // The holder is told, so the workbench can drop it instead of spinning. + expect(dropped).toEqual([id]); + // And a second consumer never gets the same dead bytes. + expect(await fileStorage.getStirlingFile(id)).toBeNull(); + + unsubscribe(); + }); +}); + +/** A readable-blob substitute, for the rescue path: fake-indexeddb never returns + * Blob values, so a healthy legacy blob record is injected the same way a dead + * one is. */ +function substituteHealthyBlobOnRead(reads: number) { + let remaining = reads; + IDBObjectStore.prototype.get = function ( + this: IDBObjectStore, + key: IDBValidKey | IDBKeyRange, + ) { + const request = nativeGet.call(this, key as IDBValidKey); + let injected = false; + return new Proxy(request, { + get(target, prop) { + const value = Reflect.get(target, prop, target); + if (prop !== "result") { + return typeof value === "function" ? value.bind(target) : value; + } + if (!value || injected || remaining === 0) return value; + injected = true; + remaining--; + return { + ...(value as object), + data: new Blob(["%PDF-1.7 stirling"], { type: "application/pdf" }), + }; + }, + set(target, prop, value) { + Reflect.set(target, prop, value, target); + return true; + }, + }); + } as typeof IDBObjectStore.prototype.get; +} + +/** The library must tell the truth per row: a record whose bytes are gone lists + * as data-lost instead of a file that pretends to open. */ +describe("stub listings — data-lost auditing", () => { + test("flags a dead record on the stub once the audit lands", async () => { + expectConsole.warn(/could not read its bytes back/); + expectConsole.error(/cannot be read/); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + const id = await store("husk.pdf"); + + loseBackingStoreOnRead(1); + // First read schedules the out-of-band audit; unknown is not yet flagged. + expect( + (await fileStorage.getStirlingFileStub(id))?.dataUnavailable, + ).toBeUndefined(); + await new Promise((resolve) => setTimeout(resolve)); + + expect((await fileStorage.getStirlingFileStub(id))?.dataUnavailable).toBe( + true, + ); + }); + + test("rescues a still-readable legacy blob to a copy on a no-blob browser", async () => { + // The durable verdict says this browser loses blob values... + localStorage.setItem("stirling.indexeddb.blobValuesUnsupported", "true"); + const { fileStorage, store } = await freshFileStorage(); + instrumentAdd({ rejectBlobs: false }); + instrumentPut(); + const id = await store("legacy.pdf"); + + // ...and a legacy record still holds a READABLE blob: save it while we can. + substituteHealthyBlobOnRead(5); + await fileStorage.getStirlingFileStub(id); + await vi.waitFor(() => expect(putAttempts).toContain("copy")); + // Rescued, not condemned: the stub stays openable. + expect( + (await fileStorage.getStirlingFileStub(id))?.dataUnavailable, + ).toBeUndefined(); + }); +}); + +/** One hung request inside the TTL bump's readwrite transaction wedged the whole + * store: every later read and write queued behind it forever - the infinite + * "Loading files..." after a Safari reload. Maintenance must not touch + * blob-bodied records on a browser that can't rewrite them anyway. */ +describe("maintenanceMayRewrite", () => { + test("keeps maintenance away from blob records on a no-blob browser", async () => { + const { maintenanceMayRewrite } = await import("@app/services/fileStorage"); + const blobRecord = { data: new Blob(["x"]) }; + const copyRecord = { data: new ArrayBuffer(1) }; + + expect(maintenanceMayRewrite(blobRecord, false)).toBe(false); + // Copies never hang and their rewrite is accepted - always safe. + expect(maintenanceMayRewrite(copyRecord, false)).toBe(true); + // On engines that genuinely support blobs (Chrome), nothing changes. + expect(maintenanceMayRewrite(blobRecord, true)).toBe(true); + expect(maintenanceMayRewrite(copyRecord, true)).toBe(true); + }); +}); diff --git a/frontend/editor/src/core/services/fileStorage.ts b/frontend/editor/src/core/services/fileStorage.ts index 40f62642f0..27b8746604 100644 --- a/frontend/editor/src/core/services/fileStorage.ts +++ b/frontend/editor/src/core/services/fileStorage.ts @@ -15,6 +15,7 @@ import { indexedDBManager, DATABASE_CONFIGS, } from "@app/services/indexedDBManager"; +import { alert } from "@app/components/toast"; /** * Storage record - single source of truth @@ -75,15 +76,135 @@ function isBlobValueRejection(error: unknown): boolean { return name === "UnknownError" || name === "DataCloneError"; } +/** This engine loses Blob values, remembered per browser: session-scoped, each + * reload re-decides optimistically and writes more files it will lose. */ +const BLOB_VALUES_UNSUPPORTED_KEY = "stirling.indexeddb.blobValuesUnsupported"; + +function readBlobValuesSupported(): boolean { + try { + return localStorage.getItem(BLOB_VALUES_UNSUPPORTED_KEY) !== "true"; + } catch { + // Storage unavailable (private mode): decide fresh each session. + return true; + } +} + +function persistBlobValuesUnsupported(): void { + try { + localStorage.setItem(BLOB_VALUES_UNSUPPORTED_KEY, "true"); + } catch { + // Storage unavailable: the session-scoped flag still degrades this session. + } +} + +/** + * Whether maintenance writes (the thumbnail TTL bump) may re-read/re-write this + * record. In WebKit, a `get` touching a blob-bodied record whose backing store is + * damaged can HANG rather than error - and one pending request wedges the whole + * object store: every later transaction, read or write, queues behind it forever. + * That was the infinite "Loading files..." after a reload: the TTL bump's + * transaction never completed, so nothing else on the store ever ran. On a + * browser whose verdict is "blobs unsupported" the rewrite would be refused + * anyway, so blob-bodied records are not worth the risk of touching at all. + */ +export function maintenanceMayRewrite( + record: { data: ArrayBuffer | Blob }, + blobValuesSupported: boolean, +): boolean { + return !(record.data instanceof Blob) || blobValuesSupported; +} + +/** WebKit loses backing stores for blobs it accepted, and only a real read shows + * it. One byte is enough: what fails is opening the store, not the length. */ +async function blobReadFailure(data: Blob): Promise { + try { + await data.slice(0, 1).arrayBuffer(); + return null; + } catch (error) { + return error ?? new Error("Reading a stored blob's bytes failed"); + } +} + +/** Notified when a record's bytes are proven unreadable, so whoever is holding the + * file can drop it instead of rendering a document that never arrives. */ +const unreadableListeners = new Set<(fileId: FileId) => void>(); + +export function onRecordUnreadable( + listener: (fileId: FileId) => void, +): () => void { + unreadableListeners.add(listener); + return () => unreadableListeners.delete(listener); +} + +/** The probe read itself can hang in WebKit, so anything that awaits it needs a + * deadline. Distinct from a failure: nothing was proven either way. */ +const PROBE_UNANSWERED = { unanswered: true } as const; +const PROBE_DEADLINE_MS = 3000; + +function withProbeDeadline( + probe: Promise, +): Promise { + return Promise.race([ + probe, + new Promise((resolve) => + setTimeout(() => resolve(PROBE_UNANSWERED), PROBE_DEADLINE_MS), + ), + ]); +} + +/** + * The File for a stored record. Re-wrapping a stored blob can cost WebKit the + * backing handle, so hand it back untouched when its identity fields match. + */ +function fileFromRecord(record: StoredStirlingFileRecord): File { + const { data } = record; + if ( + data instanceof File && + data.name === record.name && + data.type === record.type && + data.lastModified === record.lastModified + ) { + return data; + } + return new File([data], record.name, { + type: record.type, + lastModified: record.lastModified, + }); +} + +/** + * Settle on abort, for promises whose settle paths (a cursor tick, a request not + * yet issued) never arrive. Call ONCE per transaction - there is one slot. + */ +function settleOnAbort( + transaction: IDBTransaction, + settle: (reason: Error) => void, +): void { + transaction.onabort = () => + settle( + transaction.error ?? + new Error("IndexedDB transaction aborted before it completed"), + ); +} + class FileStorageService { private readonly dbConfig = DATABASE_CONFIGS.FILES; private readonly storeName = "files"; - /** - * Whether this engine accepts Blob/File values in IndexedDB. Optimistic: the - * blob path avoids copying multi-GB files into JS memory, so we try it and - * remember the answer, rather than pre-emptively degrading everywhere. - */ - private blobValuesSupported = true; + /** Whether this engine takes Blob/File values, which avoid copying multi-GB + * files into JS memory. Optimistic; a No outlives the session (see the key). */ + private blobValuesSupported = readBlobValuesSupported(); + /** Whether a stored blob's bytes have come back yet. Until they have, each + * store proves it: accepting the write is no evidence the bytes survived. */ + private blobReadbackVerified = false; + /** Ids whose TTL write failed. Without this the swallowed failure repeats a + * whole-file rewrite on every listing. Session-scoped on purpose. */ + private readonly unwritableRecords = new Set(); + /** Ids already reported as unreadable, so one dead record is surfaced once + * rather than on every read of it. Session-scoped on purpose. */ + private readonly unreadableRecords = new Set(); + /** Ids whose blob bytes this session has already audited (either way), so + * listings don't re-probe every record on every refresh. */ + private readonly auditedRecords = new Set(); /** * Get database connection using centralized manager @@ -101,7 +222,8 @@ class FileStorageService { /** Fire-and-forget: bump thumbnailStoredAt (or clear expired thumbnail) for a set of ids. */ private async bumpThumbnailTTL(ids: FileId[], clear = false): Promise { - if (ids.length === 0) return; + const targets = ids.filter((id) => !this.unwritableRecords.has(id)); + if (targets.length === 0) return; const db = await this.getDatabase(); return new Promise((resolve, reject) => { const transaction = db.transaction([this.storeName], "readwrite"); @@ -112,7 +234,7 @@ class FileStorageService { // Issue all gets up front - each onsuccess creates a put before the // transaction can auto-commit, keeping it alive until all puts settle. - ids.forEach((id) => { + targets.forEach((id) => { const req = store.get(id); req.onsuccess = () => { const record = req.result as StoredStirlingFileRecord | undefined; @@ -123,7 +245,30 @@ class FileStorageService { } else { record.thumbnailStoredAt = Date.now(); } - store.put(record); + // One unwritable record must not take the batch with it: a rejected + // put aborts the transaction the other queued gets are still using. + try { + const put = store.put(record); + put.onerror = (event) => { + // The write we just swallowed is the one that would have taken + // this record out of the expiring set, so stop retrying it. + this.unwritableRecords.add(id); + this.noteBlobRefusal(put.error); + console.warn( + `[fileStorage] thumbnail TTL bump skipped for ${id}:`, + put.error, + ); + // Swallow it here so the failure doesn't abort the transaction. + event.preventDefault(); + event.stopPropagation(); + }; + } catch (error) { + this.unwritableRecords.add(id); + console.warn( + `[fileStorage] thumbnail TTL bump could not be issued for ${id}:`, + error, + ); + } }; req.onerror = () => reject(req.error); }); @@ -186,18 +331,255 @@ class FileStorageService { } catch (error) { // Recoverable: re-add as a copy, and stop offering blobs this session. // Anything else is the caller's to report. - if (!(record.data instanceof Blob) || !isBlobValueRejection(error)) { + if (!(record.data instanceof Blob) || !this.noteBlobRefusal(error)) { throw error; } - this.blobValuesSupported = false; - console.warn( - "IndexedDB rejected a Blob value; falling back to in-memory copies for this session. " + - "Very large files may now exhaust renderer memory.", - error, - ); record.data = await record.data.arrayBuffer(); await this.addFileRecord(db, record); + return; } + + // Committed is not retrievable. Prove the round-trip while the source File is + // still in hand; after a reload there is nothing left to repair from. + if (record.data instanceof Blob && !this.blobReadbackVerified) { + await this.verifyStoredBlobReadable(db, record, stirlingFile); + } + } + + /** Read one stored blob back, rewriting the record from {@code source} if its + * bytes don't come with it. Runs until one round-trip succeeds. */ + private async verifyStoredBlobReadable( + db: IDBDatabase, + record: StoredStirlingFileRecord, + source: File, + ): Promise { + // A record we can't read back at all is the caller's problem, not the probe's. + const stored = await this.readRecord(db, record.id).catch(() => undefined); + if (!(stored?.data instanceof Blob)) return; + + const failure = await withProbeDeadline(blobReadFailure(stored.data)); + if (!failure) { + this.blobReadbackVerified = true; + return; + } + if (failure === PROBE_UNANSWERED) { + // Nothing proven, and an upload must never wait on a probe. Leave the record + // as written; the read path reports it if the bytes really are gone. + console.warn( + `[fileStorage] readability probe for ${record.id} did not answer in ${PROBE_DEADLINE_MS}ms`, + ); + return; + } + + this.noteBlobUnreadable(failure); + try { + record.data = await source.arrayBuffer(); + await this.putRecord(db, record); + } catch (error) { + // The record is unusable either way, and the read path reports that to the + // user. Don't turn a write that already committed into a failure. + console.warn( + `[fileStorage] could not rewrite ${record.id} as an in-memory copy:`, + error, + ); + } + } + + /** Refused a Blob value? Stop offering blobs on this browser. Any write can flip + * this: WebKit refuses per-operation, not per-engine. */ + private noteBlobRefusal(error: unknown): boolean { + if (!isBlobValueRejection(error)) return false; + this.disableBlobValues( + "IndexedDB rejected a Blob value; falling back to in-memory copies on this browser. " + + "Very large files may now exhaust renderer memory.", + error, + ); + return true; + } + + /** A stored blob whose bytes won't read back means blob values can't be trusted + * on this engine either, even though it accepted the write. */ + private noteBlobUnreadable(error: unknown): void { + this.disableBlobValues( + "IndexedDB accepted a Blob value but could not read its bytes back; " + + "falling back to in-memory copies on this browser. " + + "Very large files may now exhaust renderer memory.", + error, + ); + } + + private disableBlobValues(message: string, error: unknown): void { + if (!this.blobValuesSupported) return; + this.blobValuesSupported = false; + persistBlobValuesUnsupported(); + console.warn(message, error); + } + + /** + * Audit a blob-backed record's bytes WITHOUT gating anything on the answer. + * Awaiting this was a mistake: in Safari the probe read of a lost backing store + * can stay pending forever, so it stalled every file open instead of the one + * consumer that would have failed anyway. + * + * Two outcomes, both out of band: + * - Bytes gone: mark + report, so the library shows "data lost" instead of a + * file that pretends to open. + * - Bytes readable on a browser whose verdict is "blobs unsupported": RESCUE the + * record to an ArrayBuffer copy now, while the bytes still exist. Legacy blob + * records on WebKit are one engine hiccup away from being lost for good. + */ + private reportIfUnreadable(record: StoredStirlingFileRecord): void { + if (!(record.data instanceof Blob)) return; + if (this.auditedRecords.has(record.id)) return; + this.auditedRecords.add(record.id); + void blobReadFailure(record.data).then((failure) => { + if (!failure) { + this.blobReadbackVerified = true; + if (!this.blobValuesSupported) void this.rescueBlobRecord(record.id); + return; + } + this.noteBlobUnreadable(failure); + this.reportUnreadableRecord(record, failure); + }); + } + + /** + * Rewrite one still-readable legacy blob record as an ArrayBuffer copy. Reads + * the FULL bytes (the audit only proved the first one) and goes through + * {@link updateRecord}'s read-modify-write so a concurrent metadata update + * isn't clobbered by a stale snapshot. + */ + private async rescueBlobRecord(fileId: FileId): Promise { + try { + const db = await this.getDatabase(); + const record = await this.readRecord(db, fileId); + if (!(record?.data instanceof Blob)) return; + const bytes = await withProbeDeadline(record.data.arrayBuffer()); + if (bytes === PROBE_UNANSWERED || !(bytes instanceof ArrayBuffer)) return; + record.data = bytes; + await this.putRecord(db, record); + console.info( + `[fileStorage] rescued "${record.name}" (${fileId}) to an in-memory copy before this browser could lose its blob`, + ); + } catch (error) { + // Best-effort: a failed rescue leaves the record exactly as it was. + console.warn(`[fileStorage] could not rescue ${fileId}:`, error); + } + } + + /** One console error and one toast per dead record: every consumer of the file + * hits the same record, and the user needs the reason once, not per reader. */ + private reportUnreadableRecord( + record: StoredStirlingFileRecord, + failure: unknown, + ): void { + if (this.unreadableRecords.has(record.id)) return; + this.unreadableRecords.add(record.id); + // Whoever is holding it needs to let go, or the viewer renders a document + // whose bytes never arrive - a spinner with no terminal state. + for (const listener of unreadableListeners) listener(record.id); + console.error( + `[fileStorage] stored data for "${record.name}" (${record.id}) cannot be read; ` + + "the browser no longer has the blob's backing store", + failure, + ); + alert({ + alertType: "warning", + title: "File data is unavailable", + body: + `"${record.name}" is saved in this browser but its contents can no longer be read. ` + + "Upload the file again to keep working on it.", + expandable: false, + durationMs: 8000, + }); + } + + /** Read-modify-write one record in one transaction, resolving on COMMIT. Split + * across two promises, the abort guard covers one and the other hangs. */ + private async updateRecord( + fileId: FileId, + mutate: (record: StoredStirlingFileRecord) => boolean | void, + ): Promise { + const db = await this.getDatabase(); + try { + return await this.readModifyWrite(db, fileId, mutate); + } catch (error) { + // The record we read back still carries its Blob body; retry as a copy. + if (!this.noteBlobRefusal(error)) throw error; + return await this.rewriteRecordAsCopy(db, fileId, mutate); + } + } + + /** {@link updateRecord}'s happy path: one transaction, resolve on commit. */ + private readModifyWrite( + db: IDBDatabase, + fileId: FileId, + mutate: (record: StoredStirlingFileRecord) => boolean | void, + ): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction([this.storeName], "readwrite"); + let written = false; + settleOnAbort(transaction, reject); + transaction.onerror = () => reject(transaction.error); + transaction.oncomplete = () => resolve(written); + + const store = transaction.objectStore(this.storeName); + const getRequest = store.get(fileId); + getRequest.onerror = () => reject(getRequest.error); + getRequest.onsuccess = () => { + const record = getRequest.result as + | StoredStirlingFileRecord + | undefined; + // Nothing to write: let the empty transaction commit and report false. + if (!record || mutate(record) === false) return; + written = true; + store.put(record); + }; + }); + } + + /** Recovery path: two transactions, because materializing the copy is async + * and a transaction cannot survive an await. Last-write-wins either way. */ + private async rewriteRecordAsCopy( + db: IDBDatabase, + fileId: FileId, + mutate: (record: StoredStirlingFileRecord) => boolean | void, + ): Promise { + const record = await this.readRecord(db, fileId); + if (!record || mutate(record) === false) return false; + if (record.data instanceof Blob) { + record.data = await record.data.arrayBuffer(); + } + await this.putRecord(db, record); + return true; + } + + /** One record by id, in its own transaction. */ + private readRecord( + db: IDBDatabase, + fileId: FileId, + ): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction([this.storeName], "readonly"); + settleOnAbort(transaction, reject); + const request = transaction.objectStore(this.storeName).get(fileId); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); + } + + /** One `put`, resolving on commit. */ + private putRecord( + db: IDBDatabase, + record: StoredStirlingFileRecord, + ): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction([this.storeName], "readwrite"); + settleOnAbort(transaction, reject); + transaction.onerror = () => reject(transaction.error); + transaction.oncomplete = () => resolve(); + transaction.objectStore(this.storeName).put(record); + }); } /** Single `add` of a file record. Rejects with the underlying IDB error. */ @@ -215,6 +597,7 @@ class FileStorageService { } const transaction = db.transaction([this.storeName], "readwrite"); + settleOnAbort(transaction, reject); const store = transaction.objectStore(this.storeName); const request = store.add(record); @@ -227,37 +610,21 @@ class FileStorageService { }); } - /** - * Get StirlingFile with full data - for loading into workbench - */ + /** Get StirlingFile with full data - for loading into workbench. Null covers + * both no such record and bytes gone; neither is a file callers can use. */ async getStirlingFile(id: FileId): Promise { + // Already proven unreadable this session: don't hand the same dead bytes to + // another consumer that will spin on them. Session-scoped, so a reload retries. + if (this.unreadableRecords.has(id)) return null; const db = await this.getDatabase(); + const record = await this.readRecord(db, id); + if (!record) return null; + // Reporting only, and NEVER awaited: WebKit can leave a read of a lost backing + // store pending forever, and this is the path every file open goes through. + this.reportIfUnreadable(record); - return new Promise((resolve, reject) => { - const transaction = db.transaction([this.storeName], "readonly"); - const store = transaction.objectStore(this.storeName); - const request = store.get(id); - - request.onerror = () => reject(request.error); - request.onsuccess = () => { - const record = request.result as StoredStirlingFileRecord | undefined; - if (!record) { - resolve(null); - return; - } - - // Create File from stored data - const blob = new Blob([record.data], { type: record.type }); - const file = new File([blob], record.name, { - type: record.type, - lastModified: record.lastModified, - }); - - // Convert to StirlingFile with preserved IDs - const stirlingFile = createStirlingFile(file, record.fileId); - resolve(stirlingFile); - }; - }); + // Convert to StirlingFile with preserved IDs + return createStirlingFile(fileFromRecord(record), record.fileId); } /** @@ -278,6 +645,7 @@ class FileStorageService { return new Promise((resolve, reject) => { const transaction = db.transaction([this.storeName], "readonly"); + settleOnAbort(transaction, reject); const store = transaction.objectStore(this.storeName); const request = store.get(id); @@ -295,9 +663,13 @@ class FileStorageService { // We still gate thumbnailUrl on freshness so stale thumbnails // don't leak through this read path. const fresh = this.isThumbnailFresh(record); + // Out-of-band byte audit, so the library reflects lost data (and rescues + // still-readable legacy blobs) instead of listing files that can't open. + this.reportIfUnreadable(record); const stub: StirlingFileStub = { id: record.id, + dataUnavailable: this.unreadableRecords.has(record.id) || undefined, name: record.name, type: record.type, size: record.size, @@ -338,6 +710,7 @@ class FileStorageService { return new Promise((resolve, reject) => { const transaction = db.transaction([this.storeName], "readonly"); + settleOnAbort(transaction, reject); const store = transaction.objectStore(this.storeName); const request = store.openCursor(); const stubs: StirlingFileStub[] = []; @@ -352,12 +725,18 @@ class FileStorageService { const record = cursor.value as StoredStirlingFileRecord; if (record && record.name && typeof record.size === "number") { const fresh = this.isThumbnailFresh(record); - if (record.thumbnail) { + if ( + record.thumbnail && + maintenanceMayRewrite(record, this.blobValuesSupported) + ) { if (fresh) tobump.push(record.id); else toexpire.push(record.id); } + this.reportIfUnreadable(record); stubs.push({ id: record.id, + dataUnavailable: + this.unreadableRecords.has(record.id) || undefined, name: record.name, type: record.type, size: record.size, @@ -425,6 +804,7 @@ class FileStorageService { return new Promise((resolve, reject) => { const transaction = db.transaction([this.storeName], "readonly"); + settleOnAbort(transaction, reject); const store = transaction.objectStore(this.storeName); const request = store.openCursor(); const leafStubs: StirlingFileStub[] = []; @@ -444,12 +824,18 @@ class FileStorageService { record.isLeaf !== false ) { const fresh = this.isThumbnailFresh(record); - if (record.thumbnail) { + if ( + record.thumbnail && + maintenanceMayRewrite(record, this.blobValuesSupported) + ) { if (fresh) tobump.push(record.id); else toexpire.push(record.id); } + this.reportIfUnreadable(record); leafStubs.push({ id: record.id, + dataUnavailable: + this.unreadableRecords.has(record.id) || undefined, name: record.name, type: record.type, size: record.size, @@ -579,6 +965,46 @@ class FileStorageService { return cleared; } + /** + * Superseded versions that nothing else needs once {@code deleting} goes. + * + * Deleting a file removes one record; its older versions keep their full bytes + * and are invisible (listings filter on isLeaf), so they accumulate forever. + * Only for user-facing "delete this file" - deleting ONE version from the + * history journey must leave the rest of the chain alone. + */ + async orphanedAncestorIds(deleting: FileId[]): Promise { + if (deleting.length === 0) return []; + const stubs = await this.getAllStirlingFileStubs(); + const byId = new Map(stubs.map((s) => [s.id as string, s])); + const doomed = new Set(deleting.map(String)); + + // Anything a surviving record descends from has to stay: split siblings + // share a lineage, so one leaf's delete must not strip another's history. + const keep = new Set(); + for (const stub of stubs) { + if (doomed.has(stub.id as string)) continue; + let cursor = stub.parentFileId as string | undefined; + while (cursor && !keep.has(cursor)) { + keep.add(cursor); + cursor = byId.get(cursor)?.parentFileId as string | undefined; + } + } + + const orphans: FileId[] = []; + for (const id of deleting) { + let cursor = byId.get(String(id))?.parentFileId as string | undefined; + while (cursor) { + if (!keep.has(cursor) && !doomed.has(cursor) && byId.has(cursor)) { + doomed.add(cursor); + orphans.push(cursor as FileId); + } + cursor = byId.get(cursor)?.parentFileId as string | undefined; + } + } + return orphans; + } + /** * Delete StirlingFile - single operation, no sync issues */ @@ -587,11 +1013,12 @@ class FileStorageService { return new Promise((resolve, reject) => { const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - const request = store.delete(id); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); + // On commit, not on the request: callers refresh their list from storage as + // soon as this resolves, and an aborted delete would put the row back. + settleOnAbort(transaction, reject); + transaction.onerror = () => reject(transaction.error); + transaction.oncomplete = () => resolve(); + transaction.objectStore(this.storeName).delete(id); }); } @@ -617,45 +1044,16 @@ class FileStorageService { * Update thumbnail for existing file */ async updateThumbnail(id: FileId, thumbnail: string): Promise { - const db = await this.getDatabase(); - - return new Promise((resolve, _reject) => { - try { - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - const getRequest = store.get(id); - - getRequest.onsuccess = () => { - const record = getRequest.result as StoredStirlingFileRecord; - if (record) { - record.thumbnail = thumbnail; - record.thumbnailStoredAt = Date.now(); - const updateRequest = store.put(record); - - updateRequest.onsuccess = () => { - resolve(true); - }; - updateRequest.onerror = () => { - console.error("Failed to update thumbnail:", updateRequest.error); - resolve(false); - }; - } else { - resolve(false); - } - }; - - getRequest.onerror = () => { - console.error( - "Failed to get file for thumbnail update:", - getRequest.error, - ); - resolve(false); - }; - } catch (error) { - console.error("Transaction error during thumbnail update:", error); - resolve(false); - } - }); + // Reports failure as `false` rather than rejecting; callers just need an answer. + try { + return await this.updateRecord(id, (record) => { + record.thumbnail = thumbnail; + record.thumbnailStoredAt = Date.now(); + }); + } catch (error) { + console.error("Failed to update thumbnail:", error); + return false; + } } /** @@ -666,6 +1064,7 @@ class FileStorageService { return new Promise((resolve, reject) => { const transaction = db.transaction([this.storeName], "readwrite"); + settleOnAbort(transaction, reject); const store = transaction.objectStore(this.storeName); const request = store.clear(); @@ -720,24 +1119,16 @@ class FileStorageService { async createBlobUrl(id: FileId): Promise { try { const db = await this.getDatabase(); + const record = await this.readRecord(db, id); + if (!record) return null; - return new Promise((resolve, reject) => { - const transaction = db.transaction([this.storeName], "readonly"); - const store = transaction.objectStore(this.storeName); - const request = store.get(id); - - request.onerror = () => reject(request.error); - request.onsuccess = () => { - const record = request.result as StoredStirlingFileRecord | undefined; - if (record) { - const blob = new Blob([record.data], { type: record.type }); - const url = URL.createObjectURL(blob); - resolve(url); - } else { - resolve(null); - } - }; - }); + // Stored blobs are handed straight to createObjectURL — re-wrapping + // one can cost WebKit the backing handle. See fileFromRecord. + const blob = + record.data instanceof Blob + ? record.data + : new Blob([record.data], { type: record.type }); + return URL.createObjectURL(blob); } catch (error) { console.warn(`Failed to create blob URL for ${id}:`, error); return null; @@ -750,32 +1141,9 @@ class FileStorageService { */ async markFileAsProcessed(fileId: FileId): Promise { try { - const db = await this.getDatabase(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - const record = await new Promise( - (resolve, reject) => { - const request = store.get(fileId); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }, - ); - - if (!record) { - return false; // File not found - } - - // Update the isLeaf flag to false - record.isLeaf = false; - - await new Promise((resolve, reject) => { - const request = store.put(record); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + return await this.updateRecord(fileId, (record) => { + record.isLeaf = false; }); - - return true; } catch (error) { console.error("Failed to mark file as processed:", error); return false; @@ -835,32 +1203,9 @@ class FileStorageService { */ async markFileAsLeaf(fileId: FileId): Promise { try { - const db = await this.getDatabase(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - const record = await new Promise( - (resolve, reject) => { - const request = store.get(fileId); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }, - ); - - if (!record) { - return false; // File not found - } - - // Update the isLeaf flag to true - record.isLeaf = true; - - await new Promise((resolve, reject) => { - const request = store.put(record); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + return await this.updateRecord(fileId, (record) => { + record.isLeaf = true; }); - - return true; } catch (error) { console.error("Failed to mark file as leaf:", error); return false; @@ -870,41 +1215,16 @@ class FileStorageService { /** * Update metadata fields for a stored file record. * - * Resolves on transaction.oncomplete, NOT on the individual put's onsuccess, - * so callers only receive `true` once the write actually commits. If the - * transaction aborts after put() succeeded but before commit, we return false - * - the previous behavior incorrectly claimed success in that window. + * Returns `true` only once the write commits, never on the put's `onsuccess`. + * {@link updateRecord} owns that guarantee for every write in this class. */ async updateFileMetadata( fileId: FileId, updates: Partial, ): Promise { try { - const db = await this.getDatabase(); - return await new Promise((resolve, reject) => { - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - let recordFound = false; - - const getRequest = store.get(fileId); - getRequest.onsuccess = () => { - const record = getRequest.result as - | StoredStirlingFileRecord - | undefined; - if (!record) { - // Don't commit anything; caller wants false. - return; - } - recordFound = true; - const updatedRecord = { ...record, ...updates }; - store.put(updatedRecord); - }; - getRequest.onerror = () => reject(getRequest.error); - - transaction.oncomplete = () => resolve(recordFound); - transaction.onerror = () => reject(transaction.error); - transaction.onabort = () => - reject(transaction.error ?? new Error("updateFileMetadata aborted")); + return await this.updateRecord(fileId, (record) => { + Object.assign(record, updates); }); } catch (error) { console.error("Failed to update file metadata:", error); diff --git a/frontend/editor/src/core/services/indexedDBManager.blocked.test.ts b/frontend/editor/src/core/services/indexedDBManager.blocked.test.ts new file mode 100644 index 0000000000..9031b079bb --- /dev/null +++ b/frontend/editor/src/core/services/indexedDBManager.blocked.test.ts @@ -0,0 +1,79 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; +import "fake-indexeddb/auto"; +import { expectConsole } from "@app/tests/failOnConsole"; +import type { DatabaseConfig } from "@app/services/indexedDBManager"; + +/** + * A blocked open fires `blocked` and then nothing at all - no success, no error - + * until the other connection goes away. Unguarded, the open promise never settles + * and every caller hangs SILENTLY: the file library spun forever with an empty + * console, which is why this kept being reported as unreproducible. + */ + +const config = (name: string, version: number): DatabaseConfig => ({ + name, + version, + stores: [{ name: "things", keyPath: "id" }], +}); + +/** A raw connection on an older version that never yields, i.e. the other tab. */ +function holdOlderVersion(name: string): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name, 1); + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains("things")) { + request.result.createObjectStore("things", { keyPath: "id" }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +afterEach(() => { + vi.useRealTimers(); + vi.resetModules(); +}); + +describe("openDatabase — blocked by another connection", () => { + test("rejects with something actionable instead of hanging", async () => { + expectConsole.warn(/blocked by another connection/); + const { indexedDBManager } = await import("@app/services/indexedDBManager"); + const held = await holdOlderVersion("blocked-db"); + + vi.useFakeTimers(); + const open = indexedDBManager.openDatabase(config("blocked-db", 2)); + const settled = vi.fn(); + void open.then(settled, settled); + + // Still pending before the grace period is up: a tab that yields quickly + // must not be failed prematurely. + await vi.advanceTimersByTimeAsync(4_000); + expect(settled).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(2_000); + await expect(open).rejects.toThrow(/blocked by another connection/); + + held.close(); + }); + + test("dedupes concurrent callers onto one connection", async () => { + const { indexedDBManager } = await import("@app/services/indexedDBManager"); + const spy = vi.spyOn(indexedDB, "open"); + + // Racing in the same tick is the case registration-after-await could not + // dedupe, and only the first request would ever receive `blocked`. + const [a, b, c] = await Promise.all([ + indexedDBManager.openDatabase(config("shared-db", 1)), + indexedDBManager.openDatabase(config("shared-db", 1)), + indexedDBManager.openDatabase(config("shared-db", 1)), + ]); + + expect(a).toBe(b); + expect(b).toBe(c); + expect( + spy.mock.calls.filter(([name]) => name === "shared-db"), + ).toHaveLength(1); + spy.mockRestore(); + }); +}); diff --git a/frontend/editor/src/core/services/indexedDBManager.ts b/frontend/editor/src/core/services/indexedDBManager.ts index 043b2e4386..dd823a2bd9 100644 --- a/frontend/editor/src/core/services/indexedDBManager.ts +++ b/frontend/editor/src/core/services/indexedDBManager.ts @@ -19,6 +19,11 @@ export interface DatabaseConfig { }[]; } +/** How long to wait out another connection before failing an open with something + * the user can act on. Rejecting does NOT cancel the request, so a connection + * that arrives later is closed rather than held. */ +const BLOCKED_GRACE_MS = 5000; + class IndexedDBManager { private static instance: IndexedDBManager; private databases = new Map(); @@ -47,6 +52,26 @@ class IndexedDBManager { return existingPromise; } + // Registered BEFORE anything async. A map written after a yield point can't + // dedupe callers racing into it in the same tick, so every context that opened + // this database during boot got its own connection - and per spec only the + // FIRST request ever receives `blocked`, leaving the rest waiting on an event + // that never comes. + const initPromise = this.openWithRecovery(config); + this.initPromises.set(config.name, initPromise); + + try { + const db = await initPromise; + this.databases.set(config.name, db); + return db; + } catch (error) { + this.initPromises.delete(config.name); + throw error; + } + } + + /** The v6/v7 wipe, kept off {@link openDatabase}'s synchronous registration path. */ + private async openWithRecovery(config: DatabaseConfig): Promise { // SaaS lineage shipped a v6 and a v7 of stirling-pdf-files whose // upgrade paths corrupted records (separate cursor walks racing in // one versionchange transaction). The SaaS build wipes those @@ -64,18 +89,7 @@ class IndexedDBManager { await this.deleteDatabase(config.name); } } - - const initPromise = this.performDatabaseInit(config); - this.initPromises.set(config.name, initPromise); - - try { - const db = await initPromise; - this.databases.set(config.name, db); - return db; - } catch (error) { - this.initPromises.delete(config.name); - throw error; - } + return this.performDatabaseInit(config); } private performDatabaseInit(config: DatabaseConfig): Promise { @@ -83,15 +97,60 @@ class IndexedDBManager { console.log(`Opening IndexedDB: ${config.name} v${config.version}`); const request = indexedDB.open(config.name, config.version); + // A blocked upgrade fires `blocked` and then NOTHING - no success, no error - + // until the other connection goes away. Unguarded, the promise never settles + // and every awaiting caller hangs with nothing in the console. + let settled = false; + let blockedTimer: ReturnType | undefined; + + request.onblocked = () => { + console.warn( + `Opening ${config.name} is blocked by another connection (another tab on an older version?). ` + + `Giving up in ${BLOCKED_GRACE_MS}ms if it doesn't yield.`, + ); + blockedTimer = setTimeout(() => { + if (settled) return; + settled = true; + reject( + new Error( + `Opening ${config.name} was blocked by another connection for ${BLOCKED_GRACE_MS}ms. ` + + "Close other tabs of this app and reload.", + ), + ); + }, BLOCKED_GRACE_MS); + }; + request.onerror = () => { + clearTimeout(blockedTimer); + if (settled) return; + settled = true; console.error(`Failed to open ${config.name}:`, request.error); reject(request.error); }; request.onsuccess = () => { + clearTimeout(blockedTimer); const db = request.result; + // We already gave up waiting: close it rather than hold a handle nobody + // awaits, or we become the next tab's blocker. + if (settled) { + db.close(); + return; + } + settled = true; console.log(`Successfully opened ${config.name}`); + // Another tab wants a newer schema. Forget BEFORE closing: a cached but + // closed handle is worse than none, because every transaction on it throws. + db.onversionchange = () => { + console.warn( + `${config.name}: another tab requested a version change; closing this connection`, + ); + this.databases.delete(config.name); + this.initPromises.delete(config.name); + db.close(); + }; + // Set up close handler to clean up our references db.onclose = () => { console.log(`Database ${config.name} closed`); @@ -329,9 +388,36 @@ class IndexedDBManager { return new Promise((resolve, reject) => { const deleteRequest = indexedDB.deleteDatabase(name); + // A delete blocks exactly like an upgrade, and this one is awaited on the + // files open path - so an unguarded block hangs the whole storage layer. + let settled = false; + let blockedTimer: ReturnType | undefined; - deleteRequest.onerror = () => reject(deleteRequest.error); + deleteRequest.onblocked = () => { + console.warn( + `Deleting ${name} is blocked by another connection; giving up in ${BLOCKED_GRACE_MS}ms.`, + ); + blockedTimer = setTimeout(() => { + if (settled) return; + settled = true; + reject( + new Error( + `Deleting ${name} was blocked by another connection for ${BLOCKED_GRACE_MS}ms.`, + ), + ); + }, BLOCKED_GRACE_MS); + }; + + deleteRequest.onerror = () => { + clearTimeout(blockedTimer); + if (settled) return; + settled = true; + reject(deleteRequest.error); + }; deleteRequest.onsuccess = () => { + clearTimeout(blockedTimer); + if (settled) return; + settled = true; console.log(`Deleted database: ${name}`); resolve(); }; @@ -343,17 +429,32 @@ class IndexedDBManager { */ async getDatabaseVersion(name: string): Promise { return new Promise((resolve) => { + // This probe runs BEFORE the guarded open, and a versionless open can be + // delayed indefinitely by another tab mid-versionchange. Unknown after the + // grace period beats hanging every storage consumer: the real open that + // follows has its own blocked guard and a message the user can act on. + const giveUp = setTimeout(() => { + console.warn( + `Version probe for ${name} did not answer in ${BLOCKED_GRACE_MS}ms; proceeding without it.`, + ); + resolve(null); + }, BLOCKED_GRACE_MS); const request = indexedDB.open(name); request.onsuccess = () => { + clearTimeout(giveUp); const db = request.result; const version = db.version; db.close(); resolve(version); }; - request.onerror = () => resolve(null); + request.onerror = () => { + clearTimeout(giveUp); + resolve(null); + }; request.onupgradeneeded = () => { // Cancel the upgrade request.transaction?.abort(); + clearTimeout(giveUp); resolve(null); }; }); diff --git a/frontend/editor/src/core/services/pdfiumInit.test.ts b/frontend/editor/src/core/services/pdfiumInit.test.ts new file mode 100644 index 0000000000..315c39a662 --- /dev/null +++ b/frontend/editor/src/core/services/pdfiumInit.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test, vi } from "vitest"; + +/** + * A WASM instantiate that fails must reject, not hang. `instantiateWasm` reports + * success by callback, so a swallowed rejection leaves `init()` pending and takes + * every thumbnail, page parse and form read with it - silently. + */ + +const init = vi.hoisted(() => vi.fn()); +vi.mock("@embedpdf/pdfium", () => ({ init })); + +const wasmModule = vi.hoisted(() => ({}) as WebAssembly.Module); +vi.mock("@app/services/wasmPrecompiler", () => ({ + pdfiumWasmModulePromise: Promise.resolve(wasmModule), + startEagerWasmCompilation: () => {}, + pdfiumWasmUrl: "http://localhost/pdfium.wasm", +})); + +/** emscripten's contract: it calls instantiateWasm and waits to be called back. */ +function emscriptenInit( + instantiate: (imports: object, ok: () => void) => void, +) { + return new Promise(() => { + instantiate({}, () => {}); + }); +} + +async function loadService() { + vi.resetModules(); + return await import("@app/services/pdfiumService"); +} + +describe("pdfium bootstrap", () => { + test("rejects when instantiating the pre-compiled module fails", async () => { + const failure = new Error("LinkError: import mismatch"); + vi.spyOn(WebAssembly, "instantiate").mockRejectedValue(failure as never); + init.mockImplementation((overrides: Record) => + emscriptenInit( + overrides.instantiateWasm as unknown as ( + imports: object, + ok: () => void, + ) => void, + ), + ); + + const { getPdfiumModule } = await loadService(); + + // Before the fix this never settled, so the test timed out. + await expect(getPdfiumModule()).rejects.toThrow(/LinkError/); + }); + + test("a failed load isn't cached, so the next call retries", async () => { + const instantiate = vi + .spyOn(WebAssembly, "instantiate") + .mockRejectedValueOnce(new Error("transient") as never) + .mockResolvedValue({} as never); + const ready = { PDFiumExt_Init: () => {} }; + init.mockImplementation( + (overrides: Record) => + new Promise((resolve) => { + const instantiateWasm = overrides.instantiateWasm as unknown as ( + imports: object, + ok: () => void, + ) => void; + instantiateWasm({}, () => resolve(ready)); + }), + ); + + const { getPdfiumModule } = await loadService(); + + await expect(getPdfiumModule()).rejects.toThrow(/transient/); + await expect(getPdfiumModule()).resolves.toBe(ready); + expect(instantiate).toHaveBeenCalledTimes(2); + }); +}); diff --git a/frontend/editor/src/core/services/pdfiumService.ts b/frontend/editor/src/core/services/pdfiumService.ts index a9a44c693b..d6cf12a311 100644 --- a/frontend/editor/src/core/services/pdfiumService.ts +++ b/frontend/editor/src/core/services/pdfiumService.ts @@ -80,17 +80,24 @@ function wasmUrl(): string { * This is the low-level PDFium WASM interface with all C functions wrapped. * Prefer `withDocument()` for document-scoped work. */ -export async function getPdfiumModule(): Promise { - if (_module) return _module; - if (!_initPromise) { - // Ensure eager compilation has started if PDF service is requested before idle timeout - startEagerWasmCompilation(); +/** Reuses the WASM pre-compiled at boot. Every failure must reach this promise: + * `instantiateWasm` reports success by callback, so a rejection inside it leaves + * `init()` pending forever - and with it every thumbnail, parse and form read. */ +async function initPdfiumModule(): Promise { + // Ensure eager compilation has started if PDF service is requested before idle timeout + startEagerWasmCompilation(); - const overrides: PdfiumModuleOverrides = { - locateFile: () => wasmUrl(), - }; + const overrides: PdfiumModuleOverrides = { locateFile: () => wasmUrl() }; + const precompiled = await pdfiumWasmModulePromise; - // Eagerly reuse pre-compiled WASM module from app boot if available + let reportFailure: (error: unknown) => void = () => {}; + const instantiateFailed = new Promise((_, reject) => { + reportFailure = reject; + }); + + // No pre-compiled module: leave instantiateWasm alone so emscripten fetches the + // WASM itself and rejects init() on failure, instead of a fallback that can't. + if (precompiled) { overrides.instantiateWasm = ( imports: WebAssembly.Imports, successCallback: ( @@ -98,40 +105,34 @@ export async function getPdfiumModule(): Promise { module: WebAssembly.Module, ) => void, ) => { - pdfiumWasmModulePromise - .then((wasmModule) => { - if (wasmModule) { - return WebAssembly.instantiate(wasmModule, imports).then( - (instance) => { - successCallback(instance, wasmModule); - }, - ); - } else { - throw new Error("No pre-compiled WASM module found"); - } - }) - .catch((err: unknown) => { - console.warn( - "Eager WebAssembly instantiation failed, falling back to streaming compilation:", - err, - ); - WebAssembly.instantiateStreaming(fetch(wasmUrl()), imports).then( - (result) => { - successCallback(result.instance, result.module); - }, - ); - }); + WebAssembly.instantiate(precompiled, imports) + .then((instance) => successCallback(instance, precompiled)) + .catch(reportFailure); }; + } - _initPromise = init(overrides as Partial).then((m) => { - // Call PDFiumExt_Init to ensure extensions (form fill etc.) are set up - try { - m.PDFiumExt_Init(); - } catch { - /* already initialized */ - } - _module = m; - return m; + const m = await Promise.race([ + init(overrides as Partial), + instantiateFailed, + ]); + // Call PDFiumExt_Init to ensure extensions (form fill etc.) are set up + try { + m.PDFiumExt_Init(); + } catch { + /* already initialized */ + } + _module = m; + return m; +} + +export async function getPdfiumModule(): Promise { + if (_module) return _module; + if (!_initPromise) { + _initPromise = initPdfiumModule().catch((error: unknown) => { + // Don't cache the failure: every PDF feature in the app goes through here, + // so a transient WASM fetch would take them all down for the session. + _initPromise = null; + throw error; }); } return _initPromise; diff --git a/frontend/editor/src/core/setupTests.ts b/frontend/editor/src/core/setupTests.ts index 57bcaa76a5..d01c3cb589 100644 --- a/frontend/editor/src/core/setupTests.ts +++ b/frontend/editor/src/core/setupTests.ts @@ -2,6 +2,10 @@ import "@testing-library/jest-dom"; import { vi } from "vitest"; import { installFailOnConsole } from "@app/tests/failOnConsole"; +// jsdom is missing the same APIs WebKit is, so tests must agree with the +// browser. Same module `src/index.tsx` installs. +import "@app/utils/engineShims"; + installFailOnConsole(); // Mock localStorage for tests diff --git a/frontend/editor/src/core/tests/convert/ConvertIntegration.test.tsx b/frontend/editor/src/core/tests/convert/ConvertIntegration.test.tsx index 06f9198f05..42c61cf5dc 100644 --- a/frontend/editor/src/core/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/editor/src/core/tests/convert/ConvertIntegration.test.tsx @@ -68,6 +68,8 @@ const mockedApiClient = vi.mocked(apiClient); // Mock only essential services that are actually called by the tests vi.mock("../../services/fileStorage", () => ({ + // FileContext subscribes to this to drop files whose bytes are unreadable. + onRecordUnreadable: () => () => {}, fileStorage: { init: vi.fn().mockResolvedValue(undefined), storeFile: vi.fn().mockImplementation((file, thumbnail) => { diff --git a/frontend/editor/src/core/tests/convert/ConvertSmartDetectionIntegration.test.tsx b/frontend/editor/src/core/tests/convert/ConvertSmartDetectionIntegration.test.tsx index 1e1e273de4..aec52300ca 100644 --- a/frontend/editor/src/core/tests/convert/ConvertSmartDetectionIntegration.test.tsx +++ b/frontend/editor/src/core/tests/convert/ConvertSmartDetectionIntegration.test.tsx @@ -66,6 +66,8 @@ const mockedApiClient = vi.mocked(apiClient); // Mock only essential services that are actually called by the tests vi.mock("../../services/fileStorage", () => ({ + // FileContext subscribes to this to drop files whose bytes are unreadable. + onRecordUnreadable: () => () => {}, fileStorage: { init: vi.fn().mockResolvedValue(undefined), storeFile: vi.fn().mockImplementation((file, thumbnail) => { diff --git a/frontend/editor/src/core/tests/stubbed/compare.spec.ts b/frontend/editor/src/core/tests/stubbed/compare.spec.ts index 6893721465..c3c6a990a1 100644 --- a/frontend/editor/src/core/tests/stubbed/compare.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/compare.spec.ts @@ -230,4 +230,7 @@ test.describe("Compare tool slot selection", () => { page.locator('[data-testid="compare-slot-comparison"]'), ).toHaveAttribute("data-slot-state", "empty"); }); + + // These specs stop at slot state. Actually running a comparison lives in + // `engine-capabilities.spec.ts`, which is cross-browser in PR CI. }); diff --git a/frontend/editor/src/core/tests/stubbed/engine-capabilities.spec.ts b/frontend/editor/src/core/tests/stubbed/engine-capabilities.spec.ts new file mode 100644 index 0000000000..6107fb5276 --- /dev/null +++ b/frontend/editor/src/core/tests/stubbed/engine-capabilities.spec.ts @@ -0,0 +1,138 @@ +/** Runs on all three engines in PR CI, asserting on evidence that can only exist + * if the engine did the work. Keep small - it is paid for three times per PR. */ + +import path from "path"; +import type { Page } from "@playwright/test"; +import { test, expect } from "@app/tests/helpers/stub-test-base"; +import { dismissTourTooltip, uploadFiles } from "@app/tests/helpers/ui-helpers"; + +const FIXTURES_DIR = path.join(import.meta.dirname, "../test-fixtures"); +const SAMPLE_PDF = path.join(FIXTURES_DIR, "sample.pdf"); +const PDF_A = path.join(FIXTURES_DIR, "compare_sample_a.pdf"); +const PDF_B = path.join(FIXTURES_DIR, "compare_sample_b.pdf"); + +/** A missing global or prototype method always surfaces as one of these. + * Matching the shape keeps benign engine noise out (console-clean.spec.ts). */ +const MISSING_API_ERROR = + /is not a function|is not a constructor|undefined is not an object|has no method/i; + +/** Collect the "this engine lacks an API we used" errors seen on the page. */ +function recordMissingApiErrors(page: Page): string[] { + const errors: string[] = []; + page.on("pageerror", (error: Error) => { + const text = String(error); + if (MISSING_API_ERROR.test(text)) errors.push(text); + }); + return errors; +} + +async function fillCompareSlot( + page: Page, + role: "base" | "comparison", + filePath: string, +) { + await page + .getByTestId(`compare-slot-${role}-add-input`) + .setInputFiles(filePath); + await expect( + page.locator(`[data-testid="compare-slot-${role}"]`), + ).toHaveAttribute("data-slot-state", "filled", { timeout: 20_000 }); + // The upload modal's overlay outlives its close transition and eats clicks. + await page + .locator(".mantine-Modal-overlay") + .waitFor({ state: "detached", timeout: 5_000 }) + .catch(() => { + /* already gone */ + }); +} + +test.describe("engine capabilities", { tag: "@engine-capability" }, () => { + test("extracts PDF text and completes a comparison", async ({ page }) => { + test.setTimeout(120_000); + const missingApis = recordMissingApiErrors(page); + + await page.locator('[data-tour="tool-button-compare"]').first().click(); + await page.waitForSelector('[data-testid="compare-slot-base"]', { + timeout: 20_000, + }); + + await fillCompareSlot(page, "base", PDF_A); + await fillCompareSlot(page, "comparison", PDF_B); + + // By test id: `name` matches as a substring, so "Compare" also hits the + // tool button that opened this panel. + await page.getByTestId("compare-execute").click(); + + // Counted results, not headings: an extraction returning nothing still + // renders empty panes, which is how the WebKit failure looked like success. + const deletions = page.getByText(/Deletions \((\d+)\)/); + const additions = page.getByText(/Additions \((\d+)\)/); + await expect(deletions).toBeVisible({ timeout: 60_000 }); + await expect(additions).toBeVisible(); + expect(await deletions.innerText()).not.toMatch(/\(0\)/); + expect(await additions.innerText()).not.toMatch(/\(0\)/); + + expect(missingApis, "no missing-API errors during comparison").toEqual([]); + }); + + test("rasterises page thumbnails via the PDF engine", async ({ page }) => { + test.setTimeout(120_000); + const missingApis = recordMissingApiErrors(page); + + await uploadFiles(page, SAMPLE_PDF); + await dismissTourTooltip(page); + + // A page thumbnail only exists if the WASM engine loaded, rendered and + // encoded. When it fails the grid still renders, just with no . + await page.getByText("PDF Multi Tool", { exact: true }).first().click(); + + const thumbnail = page + .locator("[data-page-id] img[data-original-rotation]") + .first(); + await expect(thumbnail).toBeVisible({ timeout: 60_000 }); + + // An empty encode still yields a src; require enough payload to be real. + const src = await thumbnail.getAttribute("src"); + expect(src ?? "").toMatch(/^data:image\//); + expect(src?.length ?? 0).toBeGreaterThan(1_000); + + expect(missingApis, "no missing-API errors during thumbnailing").toEqual( + [], + ); + }); + + test("reads a stored file's bytes back after a reload", async ({ page }) => { + test.setTimeout(120_000); + const missingApis = recordMissingApiErrors(page); + + await uploadFiles(page, SAMPLE_PDF); + + // Full reload: FileContext rehydrates from IndexedDB, not from memory. + await page.reload({ waitUntil: "domcontentloaded" }); + + const restored = page.locator(".file-sidebar-file-item").first(); + await expect(restored).toBeVisible({ timeout: 30_000 }); + + // Rendering it is the assertion that matters: the metadata record survives + // even when the bytes were never stored, so a filename proves nothing. + await restored.hover(); + await restored + .locator(".file-sidebar-eye-btn") + .click({ timeout: 15_000, force: true }); + + const firstPage = page.locator('[data-page-index="0"]').first(); + await expect(firstPage).toBeVisible({ timeout: 60_000 }); + + // A tile that decoded has non-zero naturalWidth. A blob stored but not + // readable back resolves to nothing, and renders as an empty page. + const tile = firstPage.locator('img[src^="blob:"]').first(); + await expect(tile).toBeAttached({ timeout: 30_000 }); + await expect + .poll(() => tile.evaluate((img: HTMLImageElement) => img.naturalWidth), { + timeout: 30_000, + }) + .toBeGreaterThan(0); + + expect(missingApis, "no missing-API errors after rehydration").toEqual([]); + }); +}); diff --git a/frontend/editor/src/core/tools/formFill/FormFill.tsx b/frontend/editor/src/core/tools/formFill/FormFill.tsx index 599d586319..5ed9dcd47b 100644 --- a/frontend/editor/src/core/tools/formFill/FormFill.tsx +++ b/frontend/editor/src/core/tools/formFill/FormFill.tsx @@ -35,7 +35,7 @@ import { } from "@app/tools/formFill/FormFillContext"; import { useNavigation } from "@app/contexts/NavigationContext"; import { useViewer } from "@app/contexts/ViewerContext"; -import { useFileState } from "@app/contexts/FileContext"; +import { useAllFiles, useFileState } from "@app/contexts/FileContext"; import { Skeleton } from "@mantine/core"; import { isStirlingFile, getFormFillFileId } from "@app/types/fileContext"; import type { BaseToolProps } from "@app/types/tool"; @@ -124,7 +124,7 @@ const _MODE_TABS: ModeTabDef[] = [ const FormFill = (_props: BaseToolProps) => { const { t } = useTranslation(); const { selectedTool } = useNavigation(); - const { selectors, state: fileState } = useFileState(); + const { state: fileState } = useFileState(); const { state: formState, @@ -178,7 +178,9 @@ const FormFill = (_props: BaseToolProps) => { const isDirtyRef = useRef(formState.isDirty); isDirtyRef.current = formState.isDirty; - const activeFiles = selectors.getFiles(); + // Subscribing read: getFiles() during render doesn't re-run when the workbench + // changes, so the panel kept showing the pre-hydration (or pre-version) file. + const { files: activeFiles } = useAllFiles(); const selectedFileIds = fileState.ui.selectedFileIds; const currentFile = useMemo(() => { if (activeFiles.length === 0) return null; diff --git a/frontend/editor/src/core/types/fileContext.ts b/frontend/editor/src/core/types/fileContext.ts index 0d99b3589d..3dc00c306d 100644 --- a/frontend/editor/src/core/types/fileContext.ts +++ b/frontend/editor/src/core/types/fileContext.ts @@ -61,6 +61,12 @@ export interface StirlingFileStub extends BaseFileMetadata { * unclassified files / non-SaaS builds. */ classificationLabels?: string[]; + /** + * This session proved the stored bytes unreadable (WebKit losing a blob's + * backing store). The row renders as "data lost" instead of pretending the + * file can open; re-uploading is the only recovery. + */ + dataUnavailable?: boolean; // Note: File object stored in provider ref, not in state } diff --git a/frontend/editor/src/core/utils/canvasImageEncoding.test.ts b/frontend/editor/src/core/utils/canvasImageEncoding.test.ts new file mode 100644 index 0000000000..4e94909d53 --- /dev/null +++ b/frontend/editor/src/core/utils/canvasImageEncoding.test.ts @@ -0,0 +1,91 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + lossyEncodeOptions, + resetCanvasEncodingProbe, +} from "@app/utils/canvasImageEncoding"; + +/** An engine that can't encode a format returns PNG instead of throwing, so + * only the returned Blob's `type` reveals what happened. */ + +/** Stand in for OffscreenCanvas, honouring only the given MIME types. */ +function stubOffscreenCanvas(honoured: string[]): void { + class FakeOffscreenCanvas { + constructor( + public width: number, + public height: number, + ) {} + + getContext() { + return { fillStyle: "", fillRect: () => {} }; + } + + convertToBlob({ type }: { type: string }) { + // Per spec: an unsupported type silently serialises as PNG. + const actual = honoured.includes(type) ? type : "image/png"; + return Promise.resolve(new Blob([new Uint8Array([0])], { type: actual })); + } + } + vi.stubGlobal("OffscreenCanvas", FakeOffscreenCanvas); +} + +afterEach(() => { + vi.unstubAllGlobals(); + resetCanvasEncodingProbe(); +}); + +describe("lossyEncodeOptions", () => { + it("uses WebP when the engine really encodes it", async () => { + stubOffscreenCanvas(["image/webp", "image/jpeg", "image/png"]); + await expect(lossyEncodeOptions()).resolves.toEqual({ + type: "image/webp", + quality: 0.85, + }); + }); + + it("falls back to JPEG when a WebP request silently yields PNG", async () => { + // WebKit's actual behaviour: no WebP encoder, no error either. + stubOffscreenCanvas(["image/jpeg", "image/png"]); + await expect(lossyEncodeOptions()).resolves.toEqual({ + type: "image/jpeg", + quality: 0.85, + }); + }); + + it("falls back to PNG when no lossy format is honoured", async () => { + stubOffscreenCanvas(["image/png"]); + await expect(lossyEncodeOptions()).resolves.toEqual({ + type: "image/png", + quality: 0.85, + }); + }); + + it("passes the caller's quality through", async () => { + stubOffscreenCanvas(["image/webp", "image/png"]); + await expect(lossyEncodeOptions(0.5)).resolves.toEqual({ + type: "image/webp", + quality: 0.5, + }); + }); + + it("probes once and reuses the answer", async () => { + stubOffscreenCanvas(["image/webp", "image/png"]); + const spy = vi.spyOn( + (globalThis as unknown as { OffscreenCanvas: { prototype: object } }) + .OffscreenCanvas.prototype as { convertToBlob: () => unknown }, + "convertToBlob", + ); + await lossyEncodeOptions(); + const afterFirst = spy.mock.calls.length; + await lossyEncodeOptions(); + expect(spy.mock.calls.length).toBe(afterFirst); + }); + + it("returns PNG where there is no OffscreenCanvas at all", async () => { + vi.stubGlobal("OffscreenCanvas", undefined); + await expect(lossyEncodeOptions()).resolves.toEqual({ + type: "image/png", + quality: 0.85, + }); + }); +}); diff --git a/frontend/editor/src/core/utils/canvasImageEncoding.ts b/frontend/editor/src/core/utils/canvasImageEncoding.ts new file mode 100644 index 0000000000..45de65bebf --- /dev/null +++ b/frontend/editor/src/core/utils/canvasImageEncoding.ts @@ -0,0 +1,45 @@ +/** `convertToBlob` silently serialises to PNG for a format it can't encode + * (WebKit, WebP), so only the returned `type` reveals it. Probe once per realm. */ + +/** Candidates in preference order, best compression first. PNG always works. */ +const LOSSY_CANDIDATES = ["image/webp", "image/jpeg"] as const; +const PNG_TYPE = "image/png"; + +let probe: Promise | null = null; + +async function honoursType(type: string, quality: number): Promise { + try { + const canvas = new OffscreenCanvas(2, 2); + const ctx = canvas.getContext("2d"); + if (!ctx) return false; + // JPEG has no alpha, and the pixels are thrown away either way. + ctx.fillStyle = "#000000"; + ctx.fillRect(0, 0, 2, 2); + const blob = await canvas.convertToBlob({ type, quality }); + return blob.type === type; + } catch { + return false; + } +} + +async function detectLossyType(quality: number): Promise { + if (typeof OffscreenCanvas === "undefined") return PNG_TYPE; + for (const candidate of LOSSY_CANDIDATES) { + if (await honoursType(candidate, quality)) return candidate; + } + return PNG_TYPE; +} + +/** Encode options for a page raster: the best lossy format this engine really + * supports, PNG only if it supports none. `quality` is ignored by PNG. */ +export function lossyEncodeOptions( + quality = 0.85, +): Promise { + probe ??= detectLossyType(quality); + return probe.then((type) => ({ type, quality })); +} + +/** Reset the cached probe. Tests only. */ +export function resetCanvasEncodingProbe(): void { + probe = null; +} diff --git a/frontend/editor/src/core/utils/engineShims.ts b/frontend/editor/src/core/utils/engineShims.ts new file mode 100644 index 0000000000..603c88f969 --- /dev/null +++ b/frontend/editor/src/core/utils/engineShims.ts @@ -0,0 +1,4 @@ +// Browser APIs the app is entitled to assume exist, stood in where an engine +// omits them. Import once, first, before anything that might use them. +import "@app/utils/patchReadableStreamAsyncIterator"; +import "@app/utils/patchRequestIdleCallback"; diff --git a/frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.test.ts b/frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.test.ts new file mode 100644 index 0000000000..d9db1fe62d --- /dev/null +++ b/frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.test.ts @@ -0,0 +1,93 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { patchReadableStreamAsyncIterator } from "@app/utils/patchReadableStreamAsyncIterator"; + +/** Node implements the API, so remove it to exercise the shim, then restore. */ +const KEY: PropertyKey = Symbol.asyncIterator; +const proto: object = ReadableStream.prototype; +let native: PropertyDescriptor | undefined; + +beforeEach(() => { + native = Object.getOwnPropertyDescriptor(proto, KEY); + Reflect.deleteProperty(proto, KEY); + patchReadableStreamAsyncIterator(); +}); + +afterEach(() => { + if (native) Object.defineProperty(proto, KEY, native); +}); + +function streamOf(...chunks: number[]): ReadableStream { + return new ReadableStream({ + start(controller) { + chunks.forEach((c) => controller.enqueue(c)); + controller.close(); + }, + }); +} + +function failingStream(error: Error): ReadableStream { + return new ReadableStream({ + start(controller) { + controller.enqueue(1); + controller.error(error); + }, + }); +} + +describe("patchReadableStreamAsyncIterator", () => { + it("drains a stream with for await, then unlocks it", async () => { + const stream = streamOf(1, 2, 3); + const seen: number[] = []; + for await (const chunk of stream) seen.push(chunk); + + expect(seen).toEqual([1, 2, 3]); + expect(stream.locked).toBe(false); + }); + + // The reader must be released in the read's error steps. `for await` does not + // call `return()` when `next()` rejects, so nothing else would ever unlock it. + it("releases the lock when the stream errors mid-read", async () => { + const boom = new Error("boom"); + const stream = failingStream(boom); + + await expect( + (async () => { + for await (const _ of stream) { + /* drain until it throws */ + } + })(), + ).rejects.toThrow("boom"); + + expect(stream.locked).toBe(false); + }); + + it("cancels and unlocks when the consumer breaks early", async () => { + const stream = streamOf(1, 2, 3); + for await (const chunk of stream) { + expect(chunk).toBe(1); + break; + } + expect(stream.locked).toBe(false); + }); + + // Native short-circuits once finished; throwing "reader owned by no readable + // stream" would break Array.fromAsync and defensive `return()` in a finally. + it("keeps reporting done after the stream is drained", async () => { + const iterator = streamOf(1)[Symbol.asyncIterator](); + await iterator.next(); + + await expect(iterator.next()).resolves.toEqual({ + done: true, + value: undefined, + }); + await expect(iterator.return?.()).resolves.toMatchObject({ done: true }); + }); + + it("leaves a real implementation alone", () => { + if (native) Object.defineProperty(proto, KEY, native); + const before = Object.getOwnPropertyDescriptor(proto, KEY); + patchReadableStreamAsyncIterator(); + expect(Object.getOwnPropertyDescriptor(proto, KEY)).toEqual(before); + }); +}); diff --git a/frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.ts b/frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.ts new file mode 100644 index 0000000000..2aef7c6ce7 --- /dev/null +++ b/frontend/editor/src/core/utils/patchReadableStreamAsyncIterator.ts @@ -0,0 +1,58 @@ +// WebKit ships no `ReadableStream[Symbol.asyncIterator]`, and pdf.js reads its +// text stream with `for await`, so all text extraction threw on Safari. + +export function patchReadableStreamAsyncIterator(): void { + if (typeof ReadableStream === "undefined") return; + if (Symbol.asyncIterator in ReadableStream.prototype) return; + + Object.defineProperty(ReadableStream.prototype, Symbol.asyncIterator, { + writable: true, + configurable: true, + value: function (this: ReadableStream): AsyncIterableIterator { + const reader = this.getReader(); + // Releasing must be idempotent and must NOT happen after a successful + // read - the next `read()` on a released reader throws. + let finished = false; + const release = () => { + if (finished) return; + finished = true; + reader.releaseLock(); + }; + return { + async next(): Promise> { + // Spec short-circuits once finished; throwing here would break any + // consumer that calls `next()` or `return()` defensively. + if (finished) return { done: true, value: undefined }; + try { + const { done, value } = await reader.read(); + if (done) { + release(); + return { done: true, value: undefined }; + } + return { done: false, value }; + } catch (error) { + // The spec releases the reader in the read request's error steps. + // Without this an errored stream stays locked for good: `for await` + // does not call `return()` when `next()` rejects. + release(); + throw error; + } + }, + async return(value?: unknown): Promise> { + if (finished) return { done: true, value: value as T }; + try { + await reader.cancel(); + } finally { + release(); + } + return { done: true, value: value as T }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + }, + }); +} + +patchReadableStreamAsyncIterator(); diff --git a/frontend/editor/src/core/utils/patchRequestIdleCallback.test.ts b/frontend/editor/src/core/utils/patchRequestIdleCallback.test.ts new file mode 100644 index 0000000000..dcd0dc722e --- /dev/null +++ b/frontend/editor/src/core/utils/patchRequestIdleCallback.test.ts @@ -0,0 +1,92 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { patchRequestIdleCallback } from "@app/utils/patchRequestIdleCallback"; + +/** Two things must hold: it stands in where the API is absent, and it never + * displaces a real implementation. */ + +type IdleGlobals = { + requestIdleCallback?: unknown; + cancelIdleCallback?: unknown; +}; + +const globals = globalThis as IdleGlobals; + +let saved: IdleGlobals; + +beforeEach(() => { + vi.useFakeTimers(); + saved = { + requestIdleCallback: globals.requestIdleCallback, + cancelIdleCallback: globals.cancelIdleCallback, + }; +}); + +afterEach(() => { + vi.useRealTimers(); + globals.requestIdleCallback = saved.requestIdleCallback; + globals.cancelIdleCallback = saved.cancelIdleCallback; +}); + +describe("patchRequestIdleCallback", () => { + it("stands in where the engine has no requestIdleCallback", () => { + delete globals.requestIdleCallback; + delete globals.cancelIdleCallback; + patchRequestIdleCallback(); + + const task = vi.fn(); + requestIdleCallback(task); + + // Asynchronous: never runs on the caller's own turn. + expect(task).not.toHaveBeenCalled(); + // With no deadline given, it yields briefly and then runs. + vi.advanceTimersByTime(200); + expect(task).toHaveBeenCalledTimes(1); + + const deadline = task.mock.calls[0][0] as IdleDeadline; + expect(deadline.didTimeout).toBe(false); + expect(deadline.timeRemaining()).toBeGreaterThan(0); + }); + + it("never runs later than the deadline the caller asked for", () => { + delete globals.requestIdleCallback; + patchRequestIdleCallback(); + + const task = vi.fn(); + requestIdleCallback(task, { timeout: 50 }); + vi.advanceTimersByTime(50); + expect(task).toHaveBeenCalledTimes(1); + }); + + // `src/index.tsx` asks for 2000ms so the pdfium WASM compile doesn't land on + // top of the app's first renders. + it("does not run background work earlier than the caller allowed for", () => { + delete globals.requestIdleCallback; + patchRequestIdleCallback(); + + const task = vi.fn(); + requestIdleCallback(task, { timeout: 2000 }); + vi.advanceTimersByTime(1999); + expect(task).not.toHaveBeenCalled(); + vi.advanceTimersByTime(1); + expect(task).toHaveBeenCalledTimes(1); + }); + + it("cancels a scheduled task by handle", () => { + delete globals.requestIdleCallback; + delete globals.cancelIdleCallback; + patchRequestIdleCallback(); + + const task = vi.fn(); + cancelIdleCallback(requestIdleCallback(task)); + vi.advanceTimersByTime(1000); + expect(task).not.toHaveBeenCalled(); + }); + + it("leaves a real implementation alone", () => { + const native = vi.fn(); + globals.requestIdleCallback = native; + patchRequestIdleCallback(); + expect(globals.requestIdleCallback).toBe(native); + }); +}); diff --git a/frontend/editor/src/core/utils/patchRequestIdleCallback.ts b/frontend/editor/src/core/utils/patchRequestIdleCallback.ts new file mode 100644 index 0000000000..899af7c14c --- /dev/null +++ b/frontend/editor/src/core/utils/patchRequestIdleCallback.ts @@ -0,0 +1,37 @@ +// WebKit ships no `requestIdleCallback`. The shim honours the async/deadline/ +// cancel contract, not the *idle* part, and is window-only like the real API. + +/** Short delay before running, so the current turn's work drains first. */ +const YIELD_MS = 200; + +/** Idle-deadline shape for the timer stand-in. A small positive budget keeps a + * `while (timeRemaining() > 0)` loop doing one chunk rather than spinning. */ +function makeDeadline(): IdleDeadline { + return { didTimeout: false, timeRemaining: () => 1 }; +} + +export function patchRequestIdleCallback(): void { + if (typeof globalThis === "undefined") return; + const target = globalThis as typeof globalThis & { + requestIdleCallback?: typeof requestIdleCallback; + cancelIdleCallback?: typeof cancelIdleCallback; + }; + if (typeof target.requestIdleCallback === "function") return; + + target.requestIdleCallback = ( + callback: IdleRequestCallback, + options?: IdleRequestOptions, + ): number => + setTimeout( + () => callback(makeDeadline()), + // Honour the caller's deadline: `timeout` is them saying how long this may + // wait, so firing earlier lands background work on top of startup. + options?.timeout ?? YIELD_MS, + ) as unknown as number; + + target.cancelIdleCallback = (handle: number): void => { + clearTimeout(handle); + }; +} + +patchRequestIdleCallback(); diff --git a/frontend/editor/src/core/utils/thumbnailUtils.ts b/frontend/editor/src/core/utils/thumbnailUtils.ts index b356b26db9..00f047bd6d 100644 --- a/frontend/editor/src/core/utils/thumbnailUtils.ts +++ b/frontend/editor/src/core/utils/thumbnailUtils.ts @@ -31,6 +31,12 @@ export function calculateScaleFromFileSize(fileSize: number): number { /** PDFium error code 4 = password required (encrypted PDF). */ const PDFIUM_ERR_PASSWORD = 4; +/** Callers still get a placeholder, but log the cause: an empty thumbnail is + * indistinguishable from "no raster preview", so an outage hides as a nicety. */ +function reportThumbnailFailure(file: File, error: unknown): void { + console.warn(`Thumbnail generation failed for ${file.name}:`, error); +} + /** PDFs at or above this size never get a full-buffer client-side parse * (renderer OOM) - only the linearized-prefix attempt below. */ export const LARGE_PDF_PARSE_LIMIT = 100 * 1024 * 1024; @@ -262,7 +268,7 @@ export async function generateThumbnailForFile(file: File): Promise { const fullArrayBuffer = await file.arrayBuffer(); return await generatePDFThumbnail(fullArrayBuffer, scale); } catch (error) { - console.warn(`PDF processing failed for ${file.name}:`, error); + reportThumbnailFailure(file, error); return ""; } } @@ -314,7 +320,8 @@ export async function generateThumbnailWithMetadata( pageRotations: result.pageRotations, pageDimensions: result.pageDimensions, }; - } catch { + } catch (error) { + reportThumbnailFailure(file, error); return { thumbnail: "", pageCount: 0 }; } } @@ -344,7 +351,8 @@ export async function generateThumbnailWithMetadata( pageRotations: result.pageRotations, pageDimensions: result.pageDimensions, }; - } catch { + } catch (error) { + reportThumbnailFailure(file, error); return { thumbnail: "", pageCount: 1 }; } } @@ -389,7 +397,8 @@ export async function generateThumbnailPairWithMetadata(file: File): Promise<{ unrotated: toPublic(pair.unrotated), rotated: toPublic(pair.rotated), }; - } catch { + } catch (error) { + reportThumbnailFailure(file, error); return { unrotated: { thumbnail: "", pageCount: 0 }, rotated: { thumbnail: "", pageCount: 0 }, diff --git a/frontend/editor/src/core/workers/pixelCompareWorker.ts b/frontend/editor/src/core/workers/pixelCompareWorker.ts index c1390391ba..cc0e85a164 100644 --- a/frontend/editor/src/core/workers/pixelCompareWorker.ts +++ b/frontend/editor/src/core/workers/pixelCompareWorker.ts @@ -14,6 +14,7 @@ import type { PixelCompareWorkerResponse, PixelCompareWorkerWarnings, } from "@app/types/compare"; +import { lossyEncodeOptions } from "@app/utils/canvasImageEncoding"; declare const self: DedicatedWorkerGlobalScope; @@ -155,7 +156,7 @@ const renderPageToBitmap = async ( return { imageData, bitmap }; }; -const ENCODE_OPTS: ImageEncodeOptions = { type: "image/webp", quality: 0.85 }; +const ENCODE_QUALITY = 0.85; const bitmapToBlob = async ( bitmap: ImageBitmap, @@ -168,7 +169,7 @@ const bitmapToBlob = async ( if (!ctx) throw new Error(errorStrings.canvasContextUnavailable); ctx.drawImage(bitmap, 0, 0); bitmap.close(); - return await canvas.convertToBlob(ENCODE_OPTS); + return await canvas.convertToBlob(await lossyEncodeOptions(ENCODE_QUALITY)); }; const diffDataToBlob = async ( @@ -183,7 +184,7 @@ const diffDataToBlob = async ( ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, width, height); ctx.putImageData(diff, 0, 0); - return await canvas.convertToBlob(ENCODE_OPTS); + return await canvas.convertToBlob(await lossyEncodeOptions(ENCODE_QUALITY)); }; interface PageTotals { diff --git a/frontend/editor/src/index.tsx b/frontend/editor/src/index.tsx index d66f092bab..1d05928f60 100644 --- a/frontend/editor/src/index.tsx +++ b/frontend/editor/src/index.tsx @@ -3,6 +3,9 @@ // (Edge / Google Translate / extensions) from crashing the app via // parent-mismatch DOMExceptions. See the module for details. import "@app/utils/patchDomForTranslators"; +// WebKit is missing several APIs the app assumes (ReadableStream async +// iteration, which pdf.js needs for all text extraction; requestIdleCallback). +import "@app/utils/engineShims"; import "@mantine/core/styles.css"; import "@mantine/dates/styles.css"; import "../vite-env.d.ts"; // oxlint-disable-line no-restricted-imports -- Outside app paths @@ -21,13 +24,8 @@ import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler"; applyDevWorktreeLabel(); if (typeof window !== "undefined") { - const scheduleCompilation = () => { - if (typeof requestIdleCallback === "function") { - requestIdleCallback(() => startEagerWasmCompilation(), { timeout: 2000 }); - } else { - setTimeout(startEagerWasmCompilation, 1000); - } - }; + const scheduleCompilation = () => + requestIdleCallback(() => startEagerWasmCompilation(), { timeout: 2000 }); if (document.readyState === "complete") { scheduleCompilation(); diff --git a/frontend/editor/src/portal/setupTests.ts b/frontend/editor/src/portal/setupTests.ts index 9064c734ba..7de4f9f309 100644 --- a/frontend/editor/src/portal/setupTests.ts +++ b/frontend/editor/src/portal/setupTests.ts @@ -1,6 +1,9 @@ import "@testing-library/jest-dom"; import { vi } from "vitest"; +// The shims `src/index.tsx` installs - see core/setupTests.ts. +import "@app/utils/engineShims"; + // Mirrors the editor's setup: jsdom lacks a handful of browser APIs that shared // components (Mantine FocusTrap, responsive helpers) touch on render. diff --git a/frontend/editor/src/proprietary/components/policies/policyRunSettles.test.ts b/frontend/editor/src/proprietary/components/policies/policyRunSettles.test.ts new file mode 100644 index 0000000000..fdce331821 --- /dev/null +++ b/frontend/editor/src/proprietary/components/policies/policyRunSettles.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { finishedWithNothingToDeliver } from "@app/components/policies/usePolicyAutoRun"; +import type { PolicyRunRecord } from "@app/components/policies/policyRunStore"; + +/** + * A redaction that matches nothing completes with no output file. The import + * effect used to skip those runs entirely, so `imported` never flipped and the + * file's badge + blocking overlay spun forever - on every engine. + */ +const run = (overrides: Partial = {}): PolicyRunRecord => + ({ + runId: "r", + categoryId: "security", + fileId: "f", + fileName: "f.pdf", + fileSize: 1, + target: "saas", + status: "COMPLETED", + outputs: [], + error: null, + startedAt: 0, + ...overrides, + }) as PolicyRunRecord; + +describe("finishedWithNothingToDeliver", () => { + it("settles a completed run that produced no output", () => { + expect(finishedWithNothingToDeliver(run())).toBe(true); + }); + + it("leaves a run with outputs to the import path", () => { + expect( + finishedWithNothingToDeliver( + run({ outputs: [{ fileId: "o", fileName: "o.pdf" }] as never }), + ), + ).toBe(false); + }); + + it("ignores runs that aren't finished, or are already settled", () => { + expect(finishedWithNothingToDeliver(run({ status: "PENDING" }))).toBe( + false, + ); + expect(finishedWithNothingToDeliver(run({ status: "FAILED" }))).toBe(false); + expect(finishedWithNothingToDeliver(run({ imported: true }))).toBe(false); + }); + + it("leaves classification alone - it settles via its own label path", () => { + expect( + finishedWithNothingToDeliver(run({ categoryId: "classification" })), + ).toBe(false); + }); +}); diff --git a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts index 597136a32f..7a2c4bda2e 100644 --- a/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts +++ b/frontend/editor/src/proprietary/components/policies/usePolicyAutoRun.ts @@ -124,6 +124,21 @@ function failRun(runId: string, message: string): void { const FILE_WAIT_TRIES = 20; const FILE_WAIT_MS = 250; +/** + * A policy that changed nothing (redaction matched no text, say) completes with no + * output: nothing to deliver, but finished. Left unimported, the file's badge and + * its blocking overlay spin forever. + */ +export function finishedWithNothingToDeliver(run: PolicyRunRecord): boolean { + return ( + run.status === "COMPLETED" && + !run.imported && + (run.outputs?.length ?? 0) === 0 && + // Classification has its own settle path: labels, no output file. + !isClassificationCategory(run.categoryId) + ); +} + function isTerminal(status: PolicyRunStatus): boolean { return ( status === "COMPLETED" || status === "FAILED" || status === "CANCELLED" @@ -351,13 +366,14 @@ export function usePolicyAutoRun(): void { if ( run.status !== "COMPLETED" || run.imported || - importing.current.has(run.runId) || - // Classification settles even with no outputs (nothing to tag); other - // policies need an output to import. - (!run.outputs?.length && !classification) + importing.current.has(run.runId) ) { continue; } + if (finishedWithNothingToDeliver(run)) { + updateRun(run.runId, { imported: true }); + continue; + } importing.current.add(run.runId); // Classification is metadata-only: stamp labels onto the current leaf of // the file it ran on (no version fork). See importClassificationLabels. diff --git a/frontend/editor/src/proprietary/utils/scheduleIdle.ts b/frontend/editor/src/proprietary/utils/scheduleIdle.ts index 8d3e5bec96..55d395b504 100644 --- a/frontend/editor/src/proprietary/utils/scheduleIdle.ts +++ b/frontend/editor/src/proprietary/utils/scheduleIdle.ts @@ -1,11 +1,10 @@ // Idle-time scheduling shared by the classification/backfill passes. -/** Schedule work for the browser's idle time (or soon after, as a fallback). */ +/** + * Schedule work for the browser's idle time. Main thread only: the entry-point + * shim guarantees `requestIdleCallback`, but not in worker scope. + */ export function scheduleIdle(task: () => void): () => void { - if (typeof requestIdleCallback === "function") { - const handle = requestIdleCallback(task, { timeout: 2000 }); - return () => cancelIdleCallback(handle); - } - const timer = window.setTimeout(task, 200); - return () => window.clearTimeout(timer); + const handle = requestIdleCallback(task, { timeout: 2000 }); + return () => cancelIdleCallback(handle); } diff --git a/frontend/editor/src/saas/setupTests.ts b/frontend/editor/src/saas/setupTests.ts index 2c5f53e271..3e8864e49e 100644 --- a/frontend/editor/src/saas/setupTests.ts +++ b/frontend/editor/src/saas/setupTests.ts @@ -2,6 +2,9 @@ import "@testing-library/jest-dom"; import { vi } from "vitest"; import { installFailOnConsole } from "@app/tests/failOnConsole"; +// The shims `src/index.tsx` installs - see core/setupTests.ts. +import "@app/utils/engineShims"; + installFailOnConsole(); // Mock localStorage for tests diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts index 3dc3272d59..96ce29c480 100644 --- a/frontend/editor/vite.config.ts +++ b/frontend/editor/vite.config.ts @@ -335,6 +335,11 @@ export default defineConfig(async ({ mode, command }) => { compressStaticCopyPlugin(), prerenderOgPlugin(effectiveMode === "saas"), ], + // Worker bundles are a separate Rollup pass and do NOT inherit `plugins`, + // so without this `@app/*` resolves in the app and fails in a worker. + worker: { + plugins: () => [tsconfigPaths({ projects: [tsconfigProject] })], + }, server: { host: true, allowedHosts: allowedHosts.length > 0 ? allowedHosts : undefined, From 4a2329ab6d38c7b6d1903a9ced27229e2215bcda Mon Sep 17 00:00:00 2001 From: brios <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:24:45 +0200 Subject: [PATCH 115/154] refactor(hibernate): implement manual Hibernate-compliant equals/hashCode for entity classes (#6433) # Description of Changes This PR refactors our JPA entity classes to replace Lombok's `@Data` and auto-generated `@EqualsAndHashCode` annotations with explicit Lombok annotations and custom, JPA-compliant `equals()` and `hashCode()` implementations. ### Rationale Lombok's default `@Data` and `@EqualsAndHashCode` annotations are not recommended for JPA entities. They often lead to: - Severe performance issues (e.g., loading lazy collections when evaluating `hashCode` or `toString`). - Identity mismatches or collection bugs (e.g., when database-generated IDs transition from `null` to assigned, breaking the entity's lookup in a `Set` or `Map`). This change ensures all JPA entities use safe Hibernate proxy checking and use only the entity's database identifier for equality and hash code calculations. --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have run `task check` to verify linters, typechecks, and tests pass - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- .../software/proprietary/model/Team.java | 36 +++++++++++++++- .../model/security/PersistentAuditEvent.java | 42 ++++++++++++++++-- .../security/model/PersistentLogin.java | 39 ++++++++++++++++- .../security/model/SessionEntity.java | 43 +++++++++++++++++-- .../proprietary/security/model/User.java | 43 +++++++++++++------ .../model/UserServerCertificateEntity.java | 31 ++++++++++++- 6 files changed, 208 insertions(+), 26 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java index a54959b0ff..661e42c771 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java @@ -2,13 +2,19 @@ package stirling.software.proprietary.model; import java.io.Serializable; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import org.hibernate.proxy.HibernateProxy; + import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; import stirling.software.proprietary.security.model.User; @@ -18,7 +24,6 @@ import stirling.software.proprietary.security.model.User; @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) public class Team implements Serializable { @@ -47,4 +52,31 @@ public class Team implements Serializable { users.remove(user); user.setTeam(null); } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + Team team = (Team) o; + return getId() != null && Objects.equals(getId(), team.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java index ccaf337c0b..aeb66b47a8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java @@ -1,6 +1,9 @@ package stirling.software.proprietary.model.security; import java.time.Instant; +import java.util.Objects; + +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.*; @@ -28,7 +31,9 @@ import lombok.*; name = "idx_audit_source_timestamp_principal", columnList = "source,timestamp,principal") }) -@Data +@Getter +@Setter +@ToString(onlyExplicitlyIncluded = true) @Builder @NoArgsConstructor @AllArgsConstructor @@ -36,14 +41,43 @@ public class PersistentAuditEvent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @ToString.Include private Long id; - private String principal; - private String type; + @ToString.Include private String principal; + + @ToString.Include private String type; private String source; @Column(columnDefinition = "text") private String data; // JSON blob - private Instant timestamp; + @ToString.Include private Instant timestamp; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + PersistentAuditEvent that = (PersistentAuditEvent) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java index fe9c9f4209..312cddb662 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java @@ -1,17 +1,23 @@ package stirling.software.proprietary.security.model; import java.time.Instant; +import java.util.Objects; + +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.Data; +import lombok.*; @Entity @Table(name = "persistent_logins") -@Data +@Getter +@Setter +@ToString(onlyExplicitlyIncluded = true) +@NoArgsConstructor public class PersistentLogin { @Id @@ -19,11 +25,40 @@ public class PersistentLogin { private String series; @Column(name = "username", length = 64, nullable = false) + @ToString.Include private String username; @Column(name = "token", length = 64, nullable = false) private String token; @Column(name = "last_used", nullable = false) + @ToString.Include private Instant lastUsed; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + PersistentLogin that = (PersistentLogin) o; + return getSeries() != null && Objects.equals(getSeries(), that.getSeries()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java index 552d97d022..44b2153500 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java @@ -2,16 +2,22 @@ package stirling.software.proprietary.security.model; import java.io.Serializable; import java.time.Instant; +import java.util.Objects; + +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Index; import jakarta.persistence.Table; -import lombok.Data; +import lombok.*; @Entity -@Data +@Getter +@Setter +@ToString +@NoArgsConstructor @Table( name = "sessions", indexes = { @@ -23,11 +29,42 @@ import lombok.Data; @Index(name = "idx_sessions_expired", columnList = "expired") }) public class SessionEntity implements Serializable { - @Id private String sessionId; + @Id + @Setter(AccessLevel.NONE) + private String sessionId; private String principalName; private Instant lastRequest; private boolean expired; + + public void setSessionId(String sessionId) { + if (this.sessionId != null && !this.sessionId.equals(sessionId)) { + throw new IllegalStateException("sessionId is immutable once set"); + } + this.sessionId = sessionId; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + SessionEntity that = (SessionEntity) o; + return getSessionId() != null && Objects.equals(getSessionId(), that.getSessionId()); + } + + @Override + public final int hashCode() { + return getSessionId() != null ? getSessionId().hashCode() : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java index 32733f5fc5..9455ed6e4b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java @@ -2,27 +2,19 @@ package stirling.software.proprietary.security.model; import java.io.Serializable; import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.proxy.HibernateProxy; import org.springframework.security.core.userdetails.UserDetails; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import stirling.software.common.model.enumeration.Role; import stirling.software.proprietary.model.Team; @@ -35,7 +27,6 @@ import stirling.software.proprietary.model.Team; @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) public class User implements UserDetails, Serializable { @@ -44,7 +35,6 @@ public class User implements UserDetails, Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") - @EqualsAndHashCode.Include private Long id; @Column(name = "username", unique = true) @@ -181,4 +171,31 @@ public class User implements UserDetails, Serializable { public void setOauthGrandfathered(boolean oauthGrandfathered) { this.oauthGrandfathered = oauthGrandfathered; } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + User user = (User) o; + return getId() != null && Objects.equals(getId(), user.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java index 0ad30a3bf7..aef781dd9b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java @@ -2,9 +2,11 @@ package stirling.software.proprietary.workflow.model; import java.io.Serializable; import java.time.LocalDateTime; +import java.util.Objects; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.proxy.HibernateProxy; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -19,7 +21,6 @@ import stirling.software.proprietary.security.model.User; @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) public class UserServerCertificateEntity implements Serializable { @@ -28,7 +29,6 @@ public class UserServerCertificateEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") - @EqualsAndHashCode.Include @ToString.Include private Long id; @@ -70,4 +70,31 @@ public class UserServerCertificateEntity implements Serializable { @UpdateTimestamp @Column(name = "updated_at") private LocalDateTime updatedAt; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + UserServerCertificateEntity that = (UserServerCertificateEntity) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } From 51705096956f8672869ab63bfb55a70d86fe98b9 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 13 Aug 2026 23:25:36 +0200 Subject: [PATCH 116/154] deps: upgrade mwiede JSch to 2.28.6 and adapt SFTP password handling (#7496) # Description of Changes This PR replaces #7490 and upgrades `com.github.mwiede:jsch` from `0.2.23` to `2.28.6`. In addition to the dependency bump from the original Dependabot PR, this PR includes the required compatibility adjustment for SFTP password authentication: - Updated `jschVersion` in `build.gradle` from `0.2.23` to `2.28.6`. - Updated `SftpFileClient` to pass the configured password to JSch as UTF-8 encoded bytes instead of using the `String` overload. - Preserved the existing SFTP connection and host-key verification behavior. - Addresses the API compatibility changes introduced by the newer JSch version that prevented the dependency upgrade from being used unchanged. This supersedes #7490 --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../software/proprietary/policy/network/SftpFileClient.java | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java index 5ecc29392d..d222e3be40 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java @@ -65,7 +65,7 @@ final class SftpFileClient implements RemoteFileClient { } Session session = jsch.getSession(config.username(), config.host(), config.port()); if (config.password() != null) { - session.setPassword(config.password()); + session.setPassword(config.password().getBytes(StandardCharsets.UTF_8)); } if (config.hostKeyFingerprint() != null) { // Pinned key: only the configured fingerprint is ever accepted. diff --git a/build.gradle b/build.gradle index a4a62df0a9..2b2d0c625f 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ ext { jpdfiumVersion = "1.0.4" jwtVersion = "0.13.0" awsSdkVersion = "2.44.12" - jschVersion = "0.2.23" + jschVersion = "2.28.6" commonsNetVersion = "3.11.1" smbjVersion = "0.14.0" tinkVersion = "1.23.0" From 929ded41a873f9b024aca38487a1e52208f86444 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:03:47 +0100 Subject: [PATCH 117/154] Harden actions secret handling (#7435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes ## Harden GitHub Actions secret handling Moves secrets behind deployment environments, removes the GitHub App token from workflows that only comment and label, and moves PR preview images to GHCR so the preview path needs no registry credential. Builds on #6005 by @dagecko — that commit is preserved with original authorship, rebased onto current main. ### Extract secrets from `run:` blocks (@dagecko, #6005 rebased) - Secrets referenced in shell bodies moved to step-level `env:` so values never reach a rendered command line - Two `workflow_dispatch` inputs moved out of shell interpolation (`multiOSReleases`, `push-docker-base`) - Dropped the hunks main has since solved — `setup-uv`, `reviewdog`, `build-push-action` and `github-script` are all pinned newer on main now - Fixed a bug in the original: `PR-Demo-cleanup.yml` uses a **quoted** `<< 'ENDSSH'` heredoc, so rewriting `${{ secrets.DOCKER_HUB_USERNAME }}` to `${DOCKER_HUB_USERNAME}` would have sent the literal string to the VPS and expanded to empty, silently orphaning preview images behind `|| true` ### Gate secret-bearing jobs behind environments - `environment:` added to 15 jobs across 10 workflows, mapping to `release-signing`, `docker-publish`, `package-publish`, `pr-preview` and `bot-identity` - Environment branch/tag policies are enforced by GitHub before the job starts, so editing the workflow file cannot bypass them - Four jobs deliberately **not** gated — `tauri-build`, `frontend-backend-licenses-update`, `swagger` and `push-docker-base` would fail their own triggers under the current policies and need restructuring first - Removed the `testMain` trigger from `push-docker` — the branch doesn't exist and isn't in the environment's policy ### Publish PR previews to GHCR instead of Docker Hub - Preview images now go to `ghcr.io/stirling-tools/stirling-pdf-test`, authenticated with `GITHUB_TOKEN` rather than `DOCKER_HUB_API` - Docker Hub personal access tokens cannot be scoped to a single repository, so the preview path was holding the same credential that publishes `s-pdf` and `stirling-pdf` - `DOCKER_HUB_API` no longer appears in any PR-reachable workflow - Login now precedes every `docker manifest inspect` — `deploy-on-v2-commit` had them reversed, which only worked because the Docker Hub repo was public ### Use `GITHUB_TOKEN` for comment and label workflows - Seven workflows no longer mint a GitHub App token; only `sync_files_v2`, `sync-portal-docs` and `frontend-backend-licenses-update` still do, so unattended auto-merge is unaffected - `permissions:` blocks derived per job from the API calls each actually makes — these were previously inert, since an App installation token ignores them, and one job had no block at all - Comment-threading matchers updated to `github-actions[bot]` so workflows still edit their own previous comment instead of posting duplicates - Removed the App token from the `refs/pull/N/merge` checkout in `PR-Demo-Comment-with-react` and set `persist-credentials: false` — it was written into `.git/config` of an untrusted tree that the same job then builds - Fixed a script injection in `check_toml.yml`: a fork-controlled branch name was interpolated into `actions/github-script` JS source, with validation running after the injected code had already executed. Values now come from `process.env` and are validated before use. --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: dagecko --- .github/workflows/PR-Auto-Deploy-V2.yml | 84 +++++++++------- .../workflows/PR-Demo-Comment-with-react.yml | 95 +++++++++---------- .github/workflows/PR-Demo-cleanup.yml | 34 ++++--- .github/workflows/ai_pr_title_review.yml | 21 ++-- .github/workflows/aur-publish.yml | 1 + .github/workflows/auto-labelerV2.yml | 13 +-- .github/workflows/check_toml.yml | 61 ++++++------ .github/workflows/deploy-on-v2-commit.yml | 54 +++++++---- .github/workflows/multiOSReleases.yml | 21 ++-- .github/workflows/package-managers.yml | 1 + .github/workflows/pr-conflict-labeler.yml | 15 +-- .github/workflows/push-docker-base.yml | 4 +- .github/workflows/push-docker.yml | 2 +- .github/workflows/rollback-latest.yml | 1 + .github/workflows/sync-portal-docs.yml | 1 + .github/workflows/sync_files_v2.yml | 1 + .github/workflows/tauri-build.yml | 16 +++- .github/workflows/testdriver.yml | 42 ++++++-- 18 files changed, 264 insertions(+), 203 deletions(-) diff --git a/.github/workflows/PR-Auto-Deploy-V2.yml b/.github/workflows/PR-Auto-Deploy-V2.yml index 50be9fe4cf..1ae657fa76 100644 --- a/.github/workflows/PR-Auto-Deploy-V2.yml +++ b/.github/workflows/PR-Auto-Deploy-V2.yml @@ -26,6 +26,10 @@ jobs: check-pr: if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + # Only reads the PR via pulls.get with the default GITHUB_TOKEN. + permissions: + contents: read + pull-requests: read outputs: should_deploy: ${{ steps.decide.outputs.should_deploy }} is_fork: ${{ steps.resolve.outputs.is_fork }} @@ -97,6 +101,7 @@ jobs: echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT deploy-v2-pr: + environment: pr-preview needs: check-pr runs-on: ubuntu-latest if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true') @@ -107,6 +112,7 @@ jobs: permissions: contents: read issues: write + packages: write pull-requests: write env: # Single source of truth for whether this preview embeds the admin portal: @@ -125,20 +131,11 @@ jobs: repository: ${{ github.repository }} ref: main - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Add deployment started comment id: deployment-started uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; @@ -180,7 +177,8 @@ jobs: with: repository: ${{ needs.check-pr.outputs.pr_repository }} ref: ${{ needs.check-pr.outputs.pr_ref }} - token: ${{ secrets.GITHUB_TOKEN }} + # untrusted tree is built below - never leave credentials in .git/config + persist-credentials: false fetch-depth: 0 # Fetch full history for commit hash detection - name: Set up Docker Buildx @@ -192,11 +190,16 @@ jobs: VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - - name: Login to Docker Hub + - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT - name: Get commit hash for app id: commit-hash @@ -220,7 +223,7 @@ jobs: - name: Check if image exists id: check-image run: | - if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then + if docker manifest inspect ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "Image already exists, skipping build" else @@ -228,6 +231,8 @@ jobs: echo "Image needs to be built" fi + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - name: Build and push V2 image if: steps.check-image.outputs.exists == 'false' uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -237,7 +242,7 @@ jobs: push: true cache-from: type=gha,scope=stirling-pdf-latest cache-to: type=gha,mode=max,scope=stirling-pdf-latest - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-${{ steps.commit-hash.outputs.app_short }} build-args: | VERSION_TAG=v2-alpha BUILD_PORTAL=${{ env.BUILD_PORTAL }} @@ -246,9 +251,11 @@ jobs: - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Deploy V2 to VPS id: deploy run: | @@ -261,7 +268,7 @@ jobs: services: stirling-pdf-v2: container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }} - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} + image: ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} ports: - "${V2_PORT}:8080" volumes: @@ -273,8 +280,8 @@ jobs: DISABLE_ADDITIONAL_FEATURES: "false" STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true" SECURITY_ENABLELOGIN: "true" - SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}" - SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}" + SECURITY_INITIALLOGIN_USERNAME: "${TEST_LOGIN_USERNAME}" + SECURITY_INITIALLOGIN_PASSWORD: "${TEST_LOGIN_PASSWORD}" SYSTEM_DEFAULTLOCALE: en-US UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}" UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture" @@ -288,9 +295,9 @@ jobs: EOF # Deploy to VPS - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose-v2.yml + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose-v2.yml - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH # Create V2 PR-specific directories mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs,storage} @@ -315,6 +322,13 @@ jobs: # Set port for output echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + TEST_LOGIN_USERNAME: ${{ secrets.TEST_LOGIN_USERNAME }} + TEST_LOGIN_PASSWORD: ${{ secrets.TEST_LOGIN_PASSWORD }} + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} + # ---- Storybook preview (only when this PR touches stories/.storybook) ---- # Runs inside the same approved-contributor-gated deploy job, so it deploys # under the exact same access rules as the app preview. @@ -379,8 +393,9 @@ jobs: env: SB_URL: ${{ steps.storybook.outputs.url }} SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; @@ -401,7 +416,7 @@ jobs: } } - const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${v2Port}`; + const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${v2Port}`; // Only mention the portal when this image actually embeds it. // Use the direct IP URL - the SSL hostname isn't supported yet. @@ -447,6 +462,7 @@ jobs: }); cleanup-v2-deployment: + environment: pr-preview if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: @@ -463,19 +479,10 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Clean up V2 deployment comments uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ github.event.pull_request.number }}; @@ -504,12 +511,14 @@ jobs: - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Cleanup V2 deployment run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH' + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH' if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then echo "Found V2 PR directory, proceeding with cleanup..." @@ -542,6 +551,9 @@ jobs: # Only remove PR-specific containers and directories ENDSSH + env: + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - name: Cleanup temporary files if: always() run: | diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index c804489836..8e0e66032e 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -37,7 +37,8 @@ jobs: check-comment: runs-on: ubuntu-latest permissions: - issues: write + contents: read # actions/checkout + issues: write # add reaction to the triggering issue comment if: | vars.CI_PROFILE != 'lite' && ( github.event_name == 'workflow_dispatch' || @@ -76,15 +77,6 @@ jobs: - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Get PR data id: get-pr uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -155,7 +147,7 @@ jobs: id: add-eyes-reaction uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`); try { @@ -174,11 +166,14 @@ jobs: } deploy-pr: + environment: pr-preview needs: check-comment runs-on: ubuntu-latest permissions: - issues: write + contents: read # actions/checkout, incl. the PR merge ref + issues: write # reactions, 'pr-deployed' label, deployment URL comment pull-requests: write + packages: write # push PR image to ghcr.io steps: - name: Harden Runner @@ -189,20 +184,12 @@ jobs: - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge - token: ${{ steps.setup-bot.outputs.token }} + # untrusted tree gets built below - never leave credentials in .git/config + persist-credentials: false - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 @@ -240,11 +227,16 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - name: Login to Docker Hub + - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT - name: Build and push PR-specific image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -254,7 +246,7 @@ jobs: push: true cache-from: type=gha,scope=stirling-pdf-latest cache-to: type=gha,mode=max,scope=stirling-pdf-latest - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ needs.check-comment.outputs.pr_number }} build-args: | VERSION_TAG=alpha PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }} @@ -269,15 +261,17 @@ jobs: push: true cache-from: type=gha,scope=stirling-pdf-engine cache-to: type=gha,mode=max,scope=stirling-pdf-engine - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ needs.check-comment.outputs.pr_number }} platforms: linux/amd64 - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Deploy to VPS id: deploy run: | @@ -295,11 +289,11 @@ jobs: # Set pro/enterprise settings (enterprise implies pro) if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then PREMIUM_ENABLED="true" - PREMIUM_KEY="${{ secrets.ENTERPRISE_KEY }}" + PREMIUM_KEY="${ENTERPRISE_KEY}" PREMIUM_PROFEATURES_AUDIT_ENABLED="true" elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then PREMIUM_ENABLED="true" - PREMIUM_KEY="${{ secrets.PREMIUM_KEY }}" + PREMIUM_KEY="${PRO_KEY}" PREMIUM_PROFEATURES_AUDIT_ENABLED="true" else PREMIUM_ENABLED="false" @@ -309,7 +303,6 @@ jobs: ENABLE_PROTOTYPES="${{ needs.check-comment.outputs.enable_prototypes }}" PR_NUMBER="${{ needs.check-comment.outputs.pr_number }}" - DOCKER_USER="${{ secrets.DOCKER_HUB_USERNAME }}" # Build engine env vars for backend (only set when prototypes enabled) if [ "$ENABLE_PROTOTYPES" == "true" ]; then @@ -319,9 +312,9 @@ jobs: ENGINE_SERVICE=" stirling-pdf-engine: container_name: stirling-pdf-engine-pr-${PR_NUMBER} - image: ${DOCKER_USER}/test:engine-pr-${PR_NUMBER} + image: ${IMAGE_BASE}:engine-pr-${PR_NUMBER} environment: - ANTHROPIC_API_KEY: \"${{ secrets.ANTHROPIC_API_KEY }}\" + ANTHROPIC_API_KEY: \"${ANTHROPIC_API_KEY}\" networks: - pr-network restart: on-failure:5" @@ -344,7 +337,7 @@ jobs: services: stirling-pdf: container_name: stirling-pdf-pr-${PR_NUMBER} - image: ${DOCKER_USER}/test:pr-${PR_NUMBER} + image: ${IMAGE_BASE}:pr-${PR_NUMBER} ports: - "${PR_NUMBER}:8080" volumes: @@ -368,9 +361,9 @@ jobs: EOF # Then copy the file and execute commands - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH # Create PR-specific directories mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs} @@ -386,11 +379,19 @@ jobs: # Set output for use in PR comment echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV + env: + ENTERPRISE_KEY: ${{ secrets.ENTERPRISE_KEY }} + # named PRO_KEY, not PREMIUM_KEY, so the shell var it feeds is not self-referential + PRO_KEY: ${{ secrets.PREMIUM_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - name: Add success reaction to comment if: success() && github.event_name == 'issue_comment' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`); try { @@ -425,7 +426,7 @@ jobs: if: failure() && github.event_name == 'issue_comment' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`); try { @@ -444,15 +445,17 @@ jobs: - name: Post deployment URL to PR if: success() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { GITHUB_REPOSITORY } = process.env; const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const prNumber = ${{ needs.check-comment.outputs.pr_number }}; const securityStatus = process.env.security_status || "Security Disabled"; - const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${prNumber}`; + const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${prNumber}`; const commentBody = `## 🚀 PR Test Deployment\n\n` + `Your PR has been deployed for testing!\n\n` + `🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` + @@ -477,6 +480,9 @@ jobs: handle-label-commands: if: ${{ github.event.issue.pull_request != null }} runs-on: ubuntu-latest + permissions: + contents: read # actions/checkout, reads repo_devs.json and labels.yml + issues: write # add/remove labels, delete the command comment steps: - name: Harden Runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -486,17 +492,10 @@ jobs: - name: Check out the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Apply label commands uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const fs = require('fs'); const path = require('path'); diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index 146f5c7f78..7b4ee8b3a3 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -13,11 +13,13 @@ env: jobs: cleanup: + environment: pr-preview if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: + contents: read # actions/checkout pull-requests: write - issues: write + issues: write # list/remove labels, list/delete comments steps: - name: Harden Runner @@ -28,20 +30,11 @@ jobs: - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Remove 'pr-deployed' label if present id: remove-label-comment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const prNumber = ${{ github.event.pull_request.number }}; const owner = context.repo.owner; @@ -100,14 +93,22 @@ jobs: if: steps.remove-label-comment.outputs.present == 'true' run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT - name: Cleanup PR deployment if: steps.remove-label-comment.outputs.present == 'true' id: cleanup + # ENDSSH heredoc is quoted, so its body is sent literally: secrets inside it + # must stay as GitHub expressions, a shell var would be empty on the remote host. run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH' + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH' if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then echo "Found PR directory, proceeding with cleanup..." @@ -122,8 +123,8 @@ jobs: rm -rf /stirling/PR-${{ github.event.pull_request.number }} # Remove the Docker images - docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true - docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ github.event.pull_request.number }} || true + docker rmi --no-prune ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ github.event.pull_request.number }} || true + docker rmi --no-prune ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ github.event.pull_request.number }} || true echo "PERFORMED_CLEANUP" else @@ -131,6 +132,9 @@ jobs: echo "NO_CLEANUP_NEEDED" fi ENDSSH + env: + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - name: Cleanup temporary files if: always() diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml index 563e94c9b3..9922177b4b 100644 --- a/.github/workflows/ai_pr_title_review.yml +++ b/.github/workflows/ai_pr_title_review.yml @@ -10,10 +10,12 @@ permissions: # required for secure-repo hardening jobs: ai-title-review: + # GITHUB_TOKEN obeys this block, so it must cover every API call made below. permissions: - contents: read - pull-requests: write - models: read + contents: read # actions/checkout, git fetch/diff + issues: write # issues.listComments / createComment / updateComment on the PR + pull-requests: write # same endpoints when the target is a pull request + models: read # actions/ai-inference runs-on: ubuntu-latest @@ -30,15 +32,6 @@ jobs: - name: Configure Git to suppress detached HEAD warning run: git config --global advice.detachedHead false - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Check if actor is repo developer id: actor run: | @@ -161,7 +154,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 continue-on-error: true with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const fs = require('fs'); const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8'); @@ -172,7 +165,7 @@ jobs: const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/); const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null; - const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]"; + const expectedActor = "github-actions[bot]"; const comments = await github.rest.issues.listComments({ owner, repo, issue_number }); const existing = comments.data.find(c => diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index f5af23da07..f1ca2be8ca 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -66,6 +66,7 @@ jobs: echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT" publish-aur: + environment: package-publish needs: get-release-info runs-on: ubuntu-latest steps: diff --git a/.github/workflows/auto-labelerV2.yml b/.github/workflows/auto-labelerV2.yml index 6039c0e7df..bcbd0fcba5 100644 --- a/.github/workflows/auto-labelerV2.yml +++ b/.github/workflows/auto-labelerV2.yml @@ -13,7 +13,9 @@ jobs: labeler: runs-on: ubuntu-latest permissions: - pull-requests: write + contents: read # checkout + labeler fetching its config from the repo + pull-requests: write # read changed files, apply labels to the PR + issues: write # labels are applied through the issues API steps: - name: Harden Runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -22,17 +24,10 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0 with: config_path: .github/labeler-config-srvaroa.yml use_local_config: false fail_on_error: true env: - GITHUB_TOKEN: "${{ steps.setup-bot.outputs.token }}" + GITHUB_TOKEN: "${{ github.token }}" diff --git a/.github/workflows/check_toml.yml b/.github/workflows/check_toml.yml index d134c80a9b..eff416c379 100644 --- a/.github/workflows/check_toml.yml +++ b/.github/workflows/check_toml.yml @@ -23,6 +23,7 @@ jobs: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest permissions: + contents: read # Checkout, and read translation files via the contents API issues: write # Allow posting comments on issues/PRs pull-requests: write # Allow writing to pull requests steps: @@ -34,18 +35,11 @@ jobs: - name: Checkout main branch first uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Get PR data id: get-pr-data uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const prNumber = context.payload.pull_request.number; const repoOwner = context.payload.repository.owner.login; @@ -66,17 +60,18 @@ jobs: - name: Fetch PR changed files id: fetch-pr-changes env: - GH_TOKEN: ${{ steps.setup-bot.outputs.token }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.get-pr-data.outputs.pr_number }} run: | echo "Fetching PR changed files..." echo "Getting list of changed files from PR..." # Check if PR number exists - if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then + if [ -z "${PR_NUMBER}" ]; then echo "Error: PR number is empty" exit 1 fi # Get changed files and filter for TOML translation files - gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR" + gh pr view "${PR_NUMBER}" --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR" # Check if any files were found if [ ! -s changed_files.txt ]; then echo "No TOML translation files changed in this PR" @@ -88,32 +83,36 @@ jobs: - name: Determine reference file id: determine-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + # Untrusted, fork-controlled values are passed via env, never interpolated into the script + PR_NUMBER: ${{ steps.get-pr-data.outputs.pr_number }} + REPO_OWNER: ${{ steps.get-pr-data.outputs.repo_owner }} + REPO_NAME: ${{ steps.get-pr-data.outputs.repo_name }} + PR_REPO_OWNER: ${{ github.event.pull_request.head.repo.owner.login }} + PR_REPO_NAME: ${{ github.event.pull_request.head.repo.name }} + PR_BRANCH: ${{ steps.get-pr-data.outputs.branch }} with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const fs = require("fs"); const path = require("path"); - const prNumber = ${{ steps.get-pr-data.outputs.pr_number }}; - const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}"; - const repoName = "${{ steps.get-pr-data.outputs.repo_name }}"; - - const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}"; - const prRepoName = "${{ github.event.pull_request.head.repo.name }}"; - const branch = "${{ steps.get-pr-data.outputs.branch }}"; - - console.log(`Determining reference file for PR #${prNumber}`); - - // Validate inputs + // Validate inputs before any use const validateInput = (input, regex, name) => { - if (!regex.test(input)) { + if (typeof input !== "string" || !regex.test(input)) { throw new Error(`Invalid ${name}: ${input}`); } + return input; }; - validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner"); - validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name"); - validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name"); + const repoOwner = validateInput(process.env.REPO_OWNER, /^[a-zA-Z0-9_-]+$/, "repository owner"); + const repoName = validateInput(process.env.REPO_NAME, /^[a-zA-Z0-9._-]+$/, "repository name"); + const prRepoOwner = validateInput(process.env.PR_REPO_OWNER, /^[a-zA-Z0-9_-]+$/, "PR repository owner"); + const prRepoName = validateInput(process.env.PR_REPO_NAME, /^[a-zA-Z0-9._-]+$/, "PR repository name"); + const branch = validateInput(process.env.PR_BRANCH, /^[a-zA-Z0-9._/-]+$/, "branch name"); + const prNumber = Number(validateInput(process.env.PR_NUMBER, /^[0-9]+$/, "PR number")); + + console.log(`Determining reference file for PR #${prNumber}`); // Get the list of changed files in the PR const { data: files } = await github.rest.pulls.listFiles({ @@ -209,10 +208,12 @@ jobs: - name: Run Python script to check files id: run-check + env: + PR_ACTOR: ${{ github.event.pull_request.user.login }} run: | echo "Running Python script to check TOML files..." uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py \ - --actor ${{ github.event.pull_request.user.login }} \ + --actor "${PR_ACTOR}" \ --reference-file "${REFERENCE_FILE}" \ --branch "pr-branch" \ --files "${FILES_LIST[@]}" > result.txt @@ -245,7 +246,7 @@ jobs: if: env.SCRIPT_OUTPUT != '' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env; const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); @@ -261,7 +262,7 @@ jobs: const comment = comments.data.find(c => c.body.includes("## 🌐 TOML Translation Verification Summary")); // Only update or create comments by the action user - const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]"; + const expectedActor = "github-actions[bot]"; if (comment && comment.user.login === expectedActor) { // Update existing comment diff --git a/.github/workflows/deploy-on-v2-commit.yml b/.github/workflows/deploy-on-v2-commit.yml index c98ec8641c..21d12044f5 100644 --- a/.github/workflows/deploy-on-v2-commit.yml +++ b/.github/workflows/deploy-on-v2-commit.yml @@ -11,7 +11,11 @@ permissions: jobs: deploy-v2-on-push: + environment: pr-preview runs-on: ubuntu-latest + permissions: + contents: read + packages: write concurrency: group: deploy-v2-push-V2 cancel-in-progress: true @@ -62,10 +66,21 @@ jobs: echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT fi + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT + + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Check if frontend image exists id: check-frontend run: | - if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then + if docker manifest inspect ${IMAGE_BASE}:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "Frontend image already exists, skipping build" else @@ -73,10 +88,12 @@ jobs: echo "Frontend image needs to be built" fi + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - name: Check if backend image exists id: check-backend run: | - if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then + if docker manifest inspect ${IMAGE_BASE}:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "Backend image already exists, skipping build" else @@ -84,11 +101,8 @@ jobs: echo "Backend image needs to be built" fi - - name: Login to Docker Hub - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - name: Build and push frontend image if: steps.check-frontend.outputs.exists == 'false' @@ -100,8 +114,8 @@ jobs: cache-from: type=gha,scope=stirling-v2-frontend cache-to: type=gha,mode=max,scope=stirling-v2-frontend tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-frontend-latest build-args: VERSION_TAG=v2-alpha platforms: linux/amd64 @@ -115,17 +129,19 @@ jobs: cache-from: type=gha,scope=stirling-v2-backend cache-to: type=gha,mode=max,scope=stirling-v2-backend tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-backend-latest build-args: VERSION_TAG=v2-alpha platforms: linux/amd64 - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Deploy to VPS on port 3000 run: | export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml @@ -135,7 +151,7 @@ jobs: services: backend: container_name: stirling-v2-backend - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} + image: ${IMAGE_BASE}:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} ports: - "13000:8080" volumes: @@ -158,21 +174,21 @@ jobs: frontend: container_name: stirling-v2-frontend - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} + image: ${IMAGE_BASE}:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} ports: - "3000:80" environment: - VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:13000" + VITE_API_BASE_URL: "http://${NEW_VPS_HOST}:13000" depends_on: - backend restart: on-failure:5 EOF # Copy to remote with unique name - scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/$UNIQUE_NAME + scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/$UNIQUE_NAME # SSH and rename/move atomically to avoid interference - ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH + ssh -i ../private.key -o StrictHostKeyChecking=no ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH mkdir -p /stirling/V2/{data,config,logs} mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml cd /stirling/V2 @@ -183,6 +199,10 @@ jobs: docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true ENDSSH + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} - name: Cleanup temporary files if: always() run: | diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index f02e6c612c..c712467e7b 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -93,7 +93,7 @@ jobs: ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - case "${{ github.event.inputs.platform }}" in + case "${INPUT_PLATFORM}" in "windows") echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT ;; @@ -115,6 +115,8 @@ jobs: echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT fi + env: + INPUT_PLATFORM: ${{ github.event.inputs.platform }} build-jars: needs: determine-matrix runs-on: ubuntu-latest @@ -194,6 +196,7 @@ jobs: retention-days: 1 build: + environment: release-signing needs: determine-matrix strategy: fail-fast: false @@ -308,16 +311,16 @@ jobs: Write-Host "Setting up DigiCert KeyLocker environment..." # Decode client certificate - $certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}") + $certBytes = [Convert]::FromBase64String("$env:SM_CLIENT_CERT_FILE_B64") $certPath = "D:\Certificate_pkcs12.p12" [IO.File]::WriteAllBytes($certPath, $certBytes) # Set environment variables echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV - echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV - echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV - echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV - echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV + echo "SM_HOST=$env:SM_HOST" >> $env:GITHUB_ENV + echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV + echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV + echo "SM_KEYPAIR_ALIAS=$env:SM_KEYPAIR_ALIAS" >> $env:GITHUB_ENV # Get PKCS11 config path from DigiCert action $pkcs11Config = $env:PKCS11_CONFIG @@ -335,6 +338,12 @@ jobs: } } + env: + SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }} + SM_HOST: ${{ secrets.SM_HOST }} + SM_API_KEY: ${{ secrets.SM_API_KEY }} + SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - name: Import Windows Code Signing Certificate if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} diff --git a/.github/workflows/package-managers.yml b/.github/workflows/package-managers.yml index e88c5e400e..751b3c1877 100644 --- a/.github/workflows/package-managers.yml +++ b/.github/workflows/package-managers.yml @@ -73,6 +73,7 @@ jobs: echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT" update-homebrew-and-scoop: + environment: package-publish needs: get-release-info runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/pr-conflict-labeler.yml b/.github/workflows/pr-conflict-labeler.yml index a44d4f7b28..362421d0f8 100644 --- a/.github/workflows/pr-conflict-labeler.yml +++ b/.github/workflows/pr-conflict-labeler.yml @@ -27,9 +27,9 @@ jobs: name: Label conflicted PRs runs-on: ubuntu-latest permissions: - contents: read - issues: write - pull-requests: read + contents: read # actions/checkout + issues: write # get/create the repo-level conflict label + pull-requests: write # pulls.get/list plus add/remove the label on PRs steps: - name: Harden Runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -39,17 +39,10 @@ jobs: - name: Check out the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up stirling-bot token - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Apply conflict label uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const conflictLabel = process.env.CONFLICT_LABEL; const owner = context.repo.owner; diff --git a/.github/workflows/push-docker-base.yml b/.github/workflows/push-docker-base.yml index 167b71531e..f7dd1bcab4 100644 --- a/.github/workflows/push-docker-base.yml +++ b/.github/workflows/push-docker-base.yml @@ -32,9 +32,11 @@ jobs: - name: Set version id: version + env: + INPUT_VERSION: ${{ github.event.inputs.version }} run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - VERSION="${{ github.event.inputs.version }}" + VERSION="${INPUT_VERSION}" elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then VERSION="1.0.3" else diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 906884aaab..c9caf1b2f2 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -23,7 +23,6 @@ on: - master - main - V2-master - - testMain # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed @@ -42,6 +41,7 @@ permissions: jobs: push: + environment: docker-publish if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-24.04-8core permissions: diff --git a/.github/workflows/rollback-latest.yml b/.github/workflows/rollback-latest.yml index 27141eff31..442a6f499e 100644 --- a/.github/workflows/rollback-latest.yml +++ b/.github/workflows/rollback-latest.yml @@ -13,6 +13,7 @@ permissions: jobs: rollback: + environment: docker-publish runs-on: ubuntu-latest permissions: packages: write diff --git a/.github/workflows/sync-portal-docs.yml b/.github/workflows/sync-portal-docs.yml index 0b27858d5f..5ff4f465c7 100644 --- a/.github/workflows/sync-portal-docs.yml +++ b/.github/workflows/sync-portal-docs.yml @@ -24,6 +24,7 @@ permissions: jobs: sync: + environment: bot-identity name: Sync docs manifest runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/sync_files_v2.yml b/.github/workflows/sync_files_v2.yml index 1888c08ca7..7e6f999618 100644 --- a/.github/workflows/sync_files_v2.yml +++ b/.github/workflows/sync_files_v2.yml @@ -33,6 +33,7 @@ permissions: jobs: sync-files: + environment: bot-identity runs-on: ubuntu-latest steps: - name: Harden Runner diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index b0888d83cf..5c04f6009f 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -227,20 +227,26 @@ jobs: - name: Setup DigiCert KeyLocker Certificate if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: pwsh + env: + SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }} + SM_HOST: ${{ secrets.SM_HOST }} + SM_API_KEY: ${{ secrets.SM_API_KEY }} + SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} run: | Write-Host "Setting up DigiCert KeyLocker environment..." # Decode client certificate - $certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}") + $certBytes = [Convert]::FromBase64String("$env:SM_CLIENT_CERT_FILE_B64") $certPath = "D:\Certificate_pkcs12.p12" [IO.File]::WriteAllBytes($certPath, $certBytes) # Set environment variables echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV - echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV - echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV - echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV - echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV + echo "SM_HOST=$env:SM_HOST" >> $env:GITHUB_ENV + echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV + echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV + echo "SM_KEYPAIR_ALIAS=$env:SM_KEYPAIR_ALIAS" >> $env:GITHUB_ENV # Get PKCS11 config path from DigiCert action $pkcs11Config = $env:PKCS11_CONFIG diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 7c03e57967..f98d874dfa 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -21,8 +21,12 @@ permissions: jobs: deploy: + environment: pr-preview if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Harden Runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -66,11 +70,16 @@ jobs: VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - - name: Login to Docker Hub + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT + + - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} - name: Build and push test image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -80,16 +89,18 @@ jobs: push: true cache-from: type=gha,scope=stirling-pdf-latest cache-to: type=gha,mode=max,scope=stirling-pdf-latest - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:test-${{ github.sha }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} platforms: linux/amd64 - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Deploy to VPS run: | cat > docker-compose.yml << EOF @@ -97,7 +108,7 @@ jobs: services: stirling-pdf: container_name: stirling-pdf-test-${{ github.sha }} - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }} + image: ${IMAGE_BASE}:test-${{ github.sha }} ports: - "1337:8080" volumes: @@ -118,9 +129,9 @@ jobs: restart: on-failure:5 EOF - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << EOF mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs} mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml cd /stirling/test-${{ github.sha }} @@ -128,6 +139,10 @@ jobs: docker-compose up -d EOF + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} files-changed: if: always() name: detect what files changed @@ -150,6 +165,7 @@ jobs: filters: ".github/config/.files.yaml" test: + environment: pr-preview if: needs.files-changed.outputs.frontend == 'true' needs: [deploy, files-changed] runs-on: ubuntu-latest @@ -185,6 +201,7 @@ jobs: FORCE_COLOR: "3" cleanup: + environment: pr-preview needs: [deploy, test] runs-on: ubuntu-latest if: always() @@ -198,16 +215,21 @@ jobs: - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Cleanup deployment if: always() run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << EOF cd /stirling/test-${{ github.sha }} docker-compose down cd /stirling rm -rf test-${{ github.sha }} EOF + env: + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} continue-on-error: true # Ensure cleanup runs even if previous steps fail From 4b26797ad8da0b5696b3324f0287ae27a8150b66 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 14 Aug 2026 07:44:41 +0100 Subject: [PATCH 118/154] Redesign New/Edit Pipeline top bars (#7438) # Description of Changes Replace the dev-UI top-bar in the New Pipeline and Edit Pipeline pages with a redesigned layout appropriate for users. I've got a big list of extra tweaks I'd like to do to the rest of the page including graph tweaks etc. but this is the only thing on the New/Edit Pipelines pages that is blocking for the release. ## Before ### New Pipeline image ### Edit Pipeline image ## After ### New Pipeline image ### Edit Pipeline image --- .../public/locales/en-US/translation.toml | 26 +- .../pipelines/PipelineBlockerTooltip.css | 22 ++ .../pipelines/PipelineBlockerTooltip.tsx | 48 +++ .../pipelines/PipelineCreateHeader.css | 47 +++ .../PipelineCreateHeader.stories.tsx | 52 +++ .../pipelines/PipelineCreateHeader.test.tsx | 86 +++++ .../pipelines/PipelineCreateHeader.tsx | 90 +++++ .../pipelines/PipelineEditHeader.css | 67 ++++ .../pipelines/PipelineEditHeader.stories.tsx | 71 ++++ .../pipelines/PipelineEditHeader.test.tsx | 156 ++++++++ .../pipelines/PipelineEditHeader.tsx | 235 ++++++++++++ .../pipelines/PipelineGraphToolbar.css | 48 +++ .../PipelineGraphToolbar.stories.tsx | 54 +++ .../pipelines/PipelineGraphToolbar.test.tsx | 103 ++++++ .../pipelines/PipelineGraphToolbar.tsx | 147 ++++++++ .../components/pipelines/PipelineHeader.css | 133 ------- .../pipelines/PipelineHeader.stories.tsx | 118 ------- .../pipelines/PipelineHeader.test.tsx | 200 ----------- .../components/pipelines/PipelineHeader.tsx | 301 ---------------- .../src/portal/views/PipelineBuilder.css | 40 +++ .../src/portal/views/PipelineBuilder.test.tsx | 59 +++- .../src/portal/views/PipelineBuilder.tsx | 333 +++++++++++------- 22 files changed, 1548 insertions(+), 888 deletions(-) create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineEditHeader.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineEditHeader.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineEditHeader.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineEditHeader.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.css create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.stories.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.test.tsx create mode 100644 frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.tsx delete mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.css delete mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx delete mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx delete mode 100644 frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index bebe1b392d..1948b71bf4 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7878,6 +7878,7 @@ title = "Pipelines" newPipeline = "New pipeline" [portal.pipelines.builder] +activate = "Activate" back = "Back to pipelines" cannotFollow = "Can't take {{produced}}" chooseAccount = "Choose an account" @@ -7885,7 +7886,6 @@ chooseDestination = "Choose a destination" chooseOperation = "Choose what this step does" chooseSource = "Choose a source" discard = "Discard changes" -enabled = "Enabled" inputs = "Input" inputSource = "Input source" inputTrigger = "Trigger" @@ -7896,6 +7896,8 @@ needsDestination = "No destination chosen" needsSource = "No source chosen" needsUpload = "Needs an uploaded file" noToolMatches = "No tools match your search." +pause = "Pause" +rename = "Rename pipeline" searchTools = "Search tools" sendToSystem = "Send to another system" stepsIncompatible = "These steps can't run on what their prior step produces: {{tools}}." @@ -7908,6 +7910,17 @@ uploadUnsupported = "Uploaded files aren't supported in pipelines yet, so these usesDefaults = "Runs with default settings" viewDefinition = "View definition" +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" +upload = "Remove steps that need an uploaded file: {{tools}}" + [portal.pipelines.builder.diagnostic] fan-in = "Combines every incoming file" fan-out = "Runs once per incoming file" @@ -7918,12 +7931,12 @@ undeclared-operation = "Can't check what this step accepts" [portal.pipelines.composer] addTool = "Add a tool" -cancel = "Cancel" create = "Create pipeline" +createPaused = "Create paused" editingUnsupported = "Displaying these tool params for editing is not supported yet." editSource = "Edit source" name = "Name" -namePlaceholder = "e.g. Redaction sweep" +namePlaceholder = "Pipeline name" noToolSettings = "This tool has no configurable settings." output = "Destination" save = "Save changes" @@ -7955,7 +7968,7 @@ confirm = "Delete" title = "Delete pipeline?" [portal.pipelines.detail] -clearHistory = "Clear history" +clearHistory = "Process ignored files in source" delete = "Delete pipeline" run = "Run now" @@ -8008,10 +8021,9 @@ completed_one = "Run completed." completed_other = "All {{count}} runs completed." empty = "Nothing to run: the sources had no documents to process." failed = "Run failed: {{error}}" -historyCleared = "History cleared. The next run reprocesses everything currently in the sources." inFlight = "Nothing new to run: documents are still being processed from an earlier run." -parked_one = "Nothing to run: {{count}} document failed previously and is parked. Fix the cause, then clear history to retry it." -parked_other = "Nothing to run: {{count}} documents failed previously and are parked. Fix the cause, then clear history to retry them." +parked_one = "Nothing to run: {{count}} document failed previously and is parked. Fix the cause, then reprocess the source to retry it." +parked_other = "Nothing to run: {{count}} documents failed previously and are parked. Fix the cause, then reprocess the source to retry them." running = "Run started; still in progress." timeout = "Run is taking longer than expected; it may still finish in the background." diff --git a/frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.css b/frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.css new file mode 100644 index 0000000000..c0779b9aeb --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.css @@ -0,0 +1,22 @@ +/** + * The "why is this disabled" list, inside a save/create button's tooltip. Left-aligned (a bulleted + * list reads oddly centred) and inheriting the tooltip's own colours. + */ + +.portal-pipeline-blockers { + text-align: left; +} + +.portal-pipeline-blockers__heading { + margin: 0 0 0.25rem; + font-weight: 600; +} + +.portal-pipeline-blockers ul { + margin: 0; + padding-left: 1.1rem; +} + +.portal-pipeline-blockers li { + margin: 0.125rem 0; +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.tsx b/frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.tsx new file mode 100644 index 0000000000..adba6ecd26 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineBlockerTooltip.tsx @@ -0,0 +1,48 @@ +import type { ReactElement } from "react"; +import { Tooltip } from "@mantine/core"; +import "@portal/components/pipelines/PipelineBlockerTooltip.css"; + +export interface PipelineBlockerTooltipProps { + /** Short line above the list, e.g. "To create this pipeline:" / "To save your changes:". */ + heading: string; + /** Everything still owed before the action is possible; empty means the action is allowed. */ + blockers: string[]; + /** The disabled control (wrapped so its hover still reaches the tooltip - see below). */ + children: ReactElement; +} + +/** + * Explains why a disabled save/create control can't be used yet, by listing what is still owed. + * + * A disabled button swallows its own pointer events, so the caller must pass a NON-disabled wrapper + * (a span/div around the button) as the child - that wrapper is what the pointer lands on. The + * tooltip hides itself when there is nothing to list (the action is allowed, or it is only + * mid-save), so callers can wire it unconditionally. + */ +export function PipelineBlockerTooltip({ + heading, + blockers, + children, +}: PipelineBlockerTooltipProps) { + return ( + +

      {heading}

      +
        + {blockers.map((blocker) => ( +
      • {blocker}
      • + ))} +
      + + } + disabled={blockers.length === 0} + position="bottom-end" + withinPortal + multiline + w={280} + > + {children} +
      + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.css b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.css new file mode 100644 index 0000000000..d3e5e4827c --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.css @@ -0,0 +1,47 @@ +/** + * Create mode's toolbar: name on the left, commit actions on the right - the same shape as the edit + * header, so the two modes feel like one page. + */ + +.portal-pipeline-create-header { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +/* Sized to a name, not the width of the page: a title-length field reads as a title, where the old + full-bleed input read as a search bar. It gives a little on narrow viewports but never grows to + fill the row - the actions on the right anchor the far end instead. */ +.portal-pipeline-create-header__name { + flex: 0 1 22rem; + min-width: 12rem; +} + +.portal-pipeline-create-header__name input { + font-size: 1rem; + font-weight: 500; +} + +/* Pinned to the right, mirroring the edit header. Buttons hold their width and the row wraps rather + than clipping. */ +.portal-pipeline-create-header__actions { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + flex: none; +} + +/* The create buttons hold their width, so their labels never squash. */ +.portal-pipeline-create-header .sui-btn { + flex: none; + white-space: nowrap; +} + +/* The two create buttons share one tooltip target, so they sit in their own inline group. */ +.portal-pipeline-create-header__create { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.stories.tsx new file mode 100644 index 0000000000..b9717c0848 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.stories.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { PipelineCreateHeader } from "@portal/components/pipelines/PipelineCreateHeader"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineCreateHeader", + component: PipelineCreateHeader, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +const noop = () => {}; + +/** + * The name is live, so the toolbar can be seen as it is filled in. Until it is named (a stand-in for + * the app's full validity check) the create buttons are disabled and carry a tooltip of what's owed. + */ +function Playground({ initialName }: { initialName: string }) { + const [name, setName] = useState(initialName); + const blockers = + name.trim() === "" + ? [ + "Give the pipeline a name", + "Choose an input source", + "Choose a destination", + ] + : []; + return ( + + ); +} + +/** A new pipeline: create is disabled, and hovering it lists what's still needed. */ +export const New: Story = { + render: () => , +}; + +/** Named: the create actions become available. */ +export const Named: Story = { + render: () => , +}; diff --git a/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.test.tsx new file mode 100644 index 0000000000..c2f1826136 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.test.tsx @@ -0,0 +1,86 @@ +import { describe, expect, it, vi } from "vitest"; +import { + fireEvent, + render as baseRender, + screen, +} from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import { + PipelineCreateHeader, + type PipelineCreateHeaderProps, +} from "@portal/components/pipelines/PipelineCreateHeader"; + +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +function renderHeader(overrides: Partial = {}) { + const handlers = { + onNameChange: vi.fn(), + onCreate: vi.fn(), + onCreatePaused: vi.fn(), + onBack: vi.fn(), + }; + render( + , + ); + return handlers; +} + +describe("PipelineCreateHeader", () => { + it("edits the pipeline's name", () => { + const handlers = renderHeader(); + fireEvent.change( + screen.getByRole("textbox", { name: "portal.pipelines.composer.name" }), + { target: { value: "Renamed" } }, + ); + expect(handlers.onNameChange).toHaveBeenCalledWith("Renamed"); + }); + + it("creates the pipeline, live or paused, and backs out", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByText("portal.pipelines.composer.create")); + expect(handlers.onCreate).toHaveBeenCalled(); + fireEvent.click(screen.getByText("portal.pipelines.composer.createPaused")); + expect(handlers.onCreatePaused).toHaveBeenCalled(); + fireEvent.click(screen.getByLabelText("portal.pipelines.builder.back")); + expect(handlers.onBack).toHaveBeenCalled(); + }); + + it("blocks both create actions until the pipeline is valid", () => { + renderHeader({ canSave: false, blockers: ["Choose a destination"] }); + expect( + screen.getByText("portal.pipelines.composer.create").closest("button"), + ).toBeDisabled(); + expect( + screen + .getByText("portal.pipelines.composer.createPaused") + .closest("button"), + ).toBeDisabled(); + }); + + it("explains, on hover, why the create buttons are disabled", async () => { + renderHeader({ + canSave: false, + blockers: ["Give the pipeline a name", "Choose a destination"], + }); + const group = document.querySelector( + ".portal-pipeline-create-header__create", + ) as HTMLElement; + fireEvent.pointerEnter(group); + fireEvent.mouseEnter(group); + expect(await screen.findByText("Choose a destination")).toBeInTheDocument(); + expect(screen.getByText("Give the pipeline a name")).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.tsx b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.tsx new file mode 100644 index 0000000000..b4e3fa7951 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineCreateHeader.tsx @@ -0,0 +1,90 @@ +import { useTranslation } from "react-i18next"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import { ActionIcon, Button, Input } from "@app/ui"; +import { PipelineBlockerTooltip } from "@portal/components/pipelines/PipelineBlockerTooltip"; +import "@portal/components/pipelines/PipelineCreateHeader.css"; + +export interface PipelineCreateHeaderProps { + name: string; + onNameChange: (name: string) => void; + + canSave: boolean; + /** Everything still owed before the pipeline can be created, shown on the disabled create button. */ + blockers: string[]; + saving: boolean; + /** Which create action is mid-save, so only the button that was clicked shows its spinner. */ + pendingCreateEnabled: boolean | null; + onCreate: () => void; + onCreatePaused: () => void; + onBack: () => void; +} + +/** + * The create-mode toolbar. Mirrors the edit header's shape - a back arrow and the name on the left, + * actions on the right - so the two modes read as the same page in two states rather than two + * different screens. The right commits the pipeline live or paused; while it can't yet, the disabled + * create buttons carry a tooltip listing exactly what is still owed, so "disabled" is never a dead end. + */ +export function PipelineCreateHeader({ + name, + onNameChange, + canSave, + blockers, + saving, + pendingCreateEnabled, + onCreate, + onCreatePaused, + onBack, +}: PipelineCreateHeaderProps) { + const { t } = useTranslation(); + + return ( +
      + + + + + onNameChange(e.target.value)} + /> + +
      + {/* The pair share one tooltip target because a disabled button swallows its own hover - the + wrapper is what the pointer lands on. */} + +
      + + +
      +
      +
      +
      + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.css b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.css new file mode 100644 index 0000000000..a83d3ff6ae --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.css @@ -0,0 +1,67 @@ +/** + * Edit mode's toolbar: identity on the left, operational actions on the right. + */ + +.portal-pipeline-edit-header { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.portal-pipeline-edit-header__identity { + display: flex; + align-items: center; + gap: 0.375rem; + min-width: 0; + flex: 1 1 16rem; +} + +/* The name is the page's title. It takes the room the identity row leaves and truncates rather than + wrapping, so a long name never pushes the pencil out of reach. */ +.portal-pipeline-edit-header__title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--c-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.portal-pipeline-edit-header__name-input { + flex: 1 1 16rem; + min-width: 12rem; +} + +.portal-pipeline-edit-header__name-input input { + font-size: 1.125rem; + font-weight: 600; +} + +/* Never let the labels squash: buttons hold their width and the row wraps instead of clipping. */ +.portal-pipeline-edit-header__actions { + display: flex; + align-items: center; + gap: 0.5rem; + flex: none; +} + +.portal-pipeline-edit-header__actions .sui-btn { + flex: none; + white-space: nowrap; +} + +/* Save is wrapped so its disabled hover reaches the blocker tooltip; the wrapper must not shrink. */ +.portal-pipeline-edit-header__save { + display: inline-flex; + flex: none; +} + +/* Destructive item in the overflow tray: red label and icon, so it reads as the exception among + the neutral entries above it. */ +.portal-pipeline-edit-header__delete-item .sui-dd__item-label, +.portal-pipeline-edit-header__delete-item .sui-dd__item-leading { + color: var(--c-danger); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.stories.tsx new file mode 100644 index 0000000000..538024e933 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.stories.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { PipelineEditHeader } from "@portal/components/pipelines/PipelineEditHeader"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineEditHeader", + component: PipelineEditHeader, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +const noop = () => {}; + +/** The name and the pause/activate state are live, so both can be exercised. */ +function Playground({ + initialName, + initialEnabled = true, + canSave = true, + blockers = [], +}: { + initialName: string; + initialEnabled?: boolean; + canSave?: boolean; + blockers?: string[]; +}) { + const [name, setName] = useState(initialName); + const [enabled, setEnabled] = useState(initialEnabled); + return ( + setEnabled((e) => !e)} + togglingEnabled={false} + onBack={noop} + canSave={canSave} + blockers={blockers} + saving={false} + onSave={noop} + onRun={noop} + running={false} + onReprocess={noop} + reprocessing={false} + onDelete={noop} + /> + ); +} + +/** A live pipeline: the toggle offers to pause it. */ +export const Active: Story = { + render: () => , +}; + +/** A paused pipeline: the toggle offers to activate it. */ +export const Paused: Story = { + render: () => ( + + ), +}; + +/** Edits that cannot yet be saved: Save is disabled and hovering it lists what's still needed. */ +export const CannotSave: Story = { + render: () => ( + + ), +}; diff --git a/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.test.tsx new file mode 100644 index 0000000000..e216dbb292 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.test.tsx @@ -0,0 +1,156 @@ +import { describe, expect, it, vi } from "vitest"; +import { + fireEvent, + render as baseRender, + screen, +} from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import { + PipelineEditHeader, + type PipelineEditHeaderProps, +} from "@portal/components/pipelines/PipelineEditHeader"; + +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +function renderHeader(overrides: Partial = {}) { + const handlers = { + onNameChange: vi.fn(), + onTogglePause: vi.fn(), + onBack: vi.fn(), + onSave: vi.fn(), + onRun: vi.fn(), + onReprocess: vi.fn(), + onDelete: vi.fn(), + }; + render( + , + ); + return handlers; +} + +describe("PipelineEditHeader", () => { + it("shows the name as the title and renames it in place", () => { + const handlers = renderHeader(); + expect(screen.getByText("Claims redaction")).toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText("portal.pipelines.builder.rename")); + const input = screen.getByRole("textbox", { + name: "portal.pipelines.composer.name", + }); + fireEvent.change(input, { target: { value: "Renamed" } }); + fireEvent.keyDown(input, { key: "Enter" }); + expect(handlers.onNameChange).toHaveBeenCalledWith("Renamed"); + }); + + it("abandons a rename on Escape, keeping the old name", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByLabelText("portal.pipelines.builder.rename")); + const input = screen.getByRole("textbox", { + name: "portal.pipelines.composer.name", + }); + fireEvent.change(input, { target: { value: "Discarded" } }); + fireEvent.keyDown(input, { key: "Escape" }); + // Escape must not commit, even via the blur that unmounting the field fires in a real browser. + fireEvent.blur(input); + expect(handlers.onNameChange).not.toHaveBeenCalled(); + expect(screen.getByText("Claims redaction")).toBeInTheDocument(); + }); + + it("commits a rename when focus leaves the field", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByLabelText("portal.pipelines.builder.rename")); + const input = screen.getByRole("textbox", { + name: "portal.pipelines.composer.name", + }); + fireEvent.change(input, { target: { value: "Renamed" } }); + fireEvent.blur(input); + expect(handlers.onNameChange).toHaveBeenCalledWith("Renamed"); + }); + + it("offers to pause a live pipeline and to activate a paused one", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByText("portal.pipelines.builder.pause")); + expect(handlers.onTogglePause).toHaveBeenCalled(); + + renderHeader({ enabled: false }); + expect( + screen.getByText("portal.pipelines.builder.activate"), + ).toBeInTheDocument(); + }); + + it("runs the saved pipeline from the row", () => { + const handlers = renderHeader(); + fireEvent.click(screen.getByText("portal.pipelines.detail.run")); + expect(handlers.onRun).toHaveBeenCalled(); + }); + + it("keeps clear-history and delete behind the overflow tray", () => { + const handlers = renderHeader(); + // Not in the row itself... + expect( + screen.queryByText("portal.pipelines.detail.clearHistory"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText("portal.pipelines.detail.delete"), + ).not.toBeInTheDocument(); + + fireEvent.click( + screen.getByLabelText("portal.pipelines.builder.moreActions"), + ); + fireEvent.click(screen.getByText("portal.pipelines.detail.clearHistory")); + expect(handlers.onReprocess).toHaveBeenCalled(); + + fireEvent.click( + screen.getByLabelText("portal.pipelines.builder.moreActions"), + ); + fireEvent.click(screen.getByText("portal.pipelines.detail.delete")); + expect(handlers.onDelete).toHaveBeenCalled(); + }); + + it("blocks saving until the edits are valid", () => { + renderHeader({ canSave: false }); + expect( + screen.getByText("portal.pipelines.composer.save").closest("button"), + ).toBeDisabled(); + }); + + it("cannot pause while a save is committing", () => { + renderHeader({ saving: true }); + expect( + screen.getByText("portal.pipelines.builder.pause").closest("button"), + ).toBeDisabled(); + }); + + it("cannot save while a pause is committing", () => { + renderHeader({ togglingEnabled: true }); + expect( + screen.getByText("portal.pipelines.composer.save").closest("button"), + ).toBeDisabled(); + }); + + it("explains, on hover, why Save is disabled", async () => { + renderHeader({ canSave: false, blockers: ["Choose a destination"] }); + const save = document.querySelector( + ".portal-pipeline-edit-header__save", + ) as HTMLElement; + fireEvent.pointerEnter(save); + fireEvent.mouseEnter(save); + expect(await screen.findByText("Choose a destination")).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.tsx b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.tsx new file mode 100644 index 0000000000..7ee2fbd173 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineEditHeader.tsx @@ -0,0 +1,235 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; +import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded"; +import PauseRoundedIcon from "@mui/icons-material/PauseRounded"; +import PowerSettingsNewRoundedIcon from "@mui/icons-material/PowerSettingsNewRounded"; +import ReplayRoundedIcon from "@mui/icons-material/ReplayRounded"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import MoreHorizRoundedIcon from "@mui/icons-material/MoreHorizRounded"; +import { ActionIcon, Button, Dropdown, Input } from "@app/ui"; +import { PipelineBlockerTooltip } from "@portal/components/pipelines/PipelineBlockerTooltip"; +import "@portal/components/pipelines/PipelineEditHeader.css"; + +export interface PipelineEditHeaderProps { + name: string; + onNameChange: (name: string) => void; + + /** The pipeline's live state. Toggling it takes effect immediately, not on save. */ + enabled: boolean; + onTogglePause: () => void; + togglingEnabled: boolean; + + onBack: () => void; + + canSave: boolean; + /** Everything still owed before the edits can be saved, shown on the disabled Save button. */ + blockers: string[]; + saving: boolean; + onSave: () => void; + + /** Run the saved pipeline against its real input, delivering to its real destination. */ + onRun: () => void; + running: boolean; + /** Reprocess everything in the sources: clears the processed record, then runs at once. */ + onReprocess: () => void; + reprocessing: boolean; + onDelete: () => void; +} + +/** + * Edit mode's toolbar over an existing, live pipeline. The left is what it *is* - a back arrow, its + * name as the page title, a pencil to rename in place. The right is what you can *do to it*: pause + * or activate it (an operational toggle that acts at once, matching the Policies vocabulary), run it + * now, and - behind an overflow, since they are rare or destructive - reprocess its sources or delete + * it. Saving the chain edits is the primary action, on the far right. (Reading the definition is an + * inspect action, so it lives in the graph toolbar beside Test, not here.) + */ +export function PipelineEditHeader({ + name, + onNameChange, + enabled, + onTogglePause, + togglingEnabled, + onBack, + canSave, + blockers, + saving, + onSave, + onRun, + running, + onReprocess, + reprocessing, + onDelete, +}: PipelineEditHeaderProps) { + const { t } = useTranslation(); + const [renaming, setRenaming] = useState(false); + const [draft, setDraft] = useState(name); + const inputRef = useRef(null); + // Enter and Escape both end the rename, which unmounts the input - and unmounting a focused input + // fires blur in a real browser (jsdom does not). Without this guard that blur would re-run the + // commit, so Escape would save the very draft it was meant to discard. The key handler sets this so + // the trailing blur is ignored; a plain click-away leaves it false and blur commits as normal. + const keyHandledRef = useRef(false); + + useEffect(() => { + if (renaming) inputRef.current?.select(); + }, [renaming]); + + function startRename() { + keyHandledRef.current = false; + setDraft(name); + setRenaming(true); + } + + // End the rename, committing the draft only when asked and only if non-empty (an all-whitespace + // rename would leave the pipeline titleless). + function finishRename(commit: boolean) { + keyHandledRef.current = true; + if (commit) { + const next = draft.trim(); + if (next) onNameChange(next); + } + setRenaming(false); + } + + // Clicking away commits; the unmount-triggered blur that follows a key press does not (the key + // already decided the outcome). + function handleBlur() { + if (keyHandledRef.current) { + keyHandledRef.current = false; + return; + } + finishRename(true); + } + + return ( +
      +
      + + + + + {renaming ? ( + setDraft(e.target.value)} + onBlur={handleBlur} + onKeyDown={(e) => { + if (e.key === "Enter") finishRename(true); + if (e.key === "Escape") finishRename(false); + }} + /> + ) : ( + <> +

      {name}

      + + + + + )} +
      + +
      + {/* Pause and Save both write the whole policy, so they are mutually exclusive: neither can + start while the other is committing, or the two writes race and the loser's version wins. */} + + + {/* Run and Reprocess both start a run, so only one at a time: each is disabled while the + other is in flight, matching the handler guards (a click otherwise silently no-ops). */} + + + {/* Rare and destructive actions kept off the row so they do not compete with running. */} + + + + + + + + } + > + {t("portal.pipelines.detail.clearHistory")} + + + + } + > + {t("portal.pipelines.detail.delete")} + + + + + {/* Wrapped in a span so the disabled button's hover still reaches the tooltip. */} + + + + + +
      +
      + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.css b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.css new file mode 100644 index 0000000000..9dc2d53bec --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.css @@ -0,0 +1,48 @@ +/** + * The test control and the last run's outcome, directly above the graph. + */ + +.portal-pipeline-toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Reading the definition sits at the far end of the bar, opposite Test. */ +.portal-pipeline-toolbar__definition { + margin-left: auto; +} + +/* The last test run's outcome, beside the button that started it. Whole-pipeline, because the + backend reports one flat file list plus the step it stopped at - nothing per node to attach. */ +.portal-pipeline-toolbar__result { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.portal-pipeline-toolbar__result-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: var(--c-text); +} + +.portal-pipeline-toolbar__result-icon.is-ok { + color: var(--c-success); +} + +.portal-pipeline-toolbar__result-icon.is-bad { + color: var(--c-danger); +} + +/* Why the run failed, shown inline in the strip. Neutral text (the icon already carries the tone); + it may wrap to keep a long backend message readable rather than clipping it. */ +.portal-pipeline-toolbar__result-error { + font-size: 0.8125rem; + color: var(--c-text-muted); + min-width: 0; +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.stories.tsx new file mode 100644 index 0000000000..359aa9bfa1 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { PipelineGraphToolbar } from "@portal/components/pipelines/PipelineGraphToolbar"; + +const meta: Meta = { + title: "Portal/Pipelines/PipelineGraphToolbar", + component: PipelineGraphToolbar, + parameters: { layout: "padded" }, + args: { + stepCount: 2, + testing: false, + runResult: null, + onTest: () => {}, + onDownloadOutput: () => {}, + onViewDefinition: () => {}, + }, +}; +export default meta; +type Story = StoryObj; + +/** Idle: just the test control. */ +export const Idle: Story = {}; + +/** A chain with no steps cannot be tested. */ +export const NoSteps: Story = { args: { stepCount: 0 } }; + +/** Mid test-run. */ +export const Testing: Story = { args: { testing: true } }; + +/** After a completed run: the outcome and its files sit beside the button. */ +export const Completed: Story = { + args: { + runResult: { + status: "completed", + completedSteps: 3, + stepCount: 3, + outputs: [ + { fileId: "f1", fileName: "claim-redacted.pdf" }, + { fileId: "f2", fileName: null }, + ], + }, + }, +}; + +/** A failed run: the summary and the failure reason. */ +export const Failed: Story = { + args: { + runResult: { + status: "failed", + completedSteps: 1, + stepCount: 3, + error: "OCR failed: unreadable page", + }, + }, +}; diff --git a/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.test.tsx new file mode 100644 index 0000000000..4aafb27766 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.test.tsx @@ -0,0 +1,103 @@ +import { describe, expect, it, vi } from "vitest"; +import { + fireEvent, + render as baseRender, + screen, +} from "@testing-library/react"; +import { PortalTestProviders } from "@portal/test/TestQueryProvider"; +import { + PipelineGraphToolbar, + type PipelineGraphToolbarProps, +} from "@portal/components/pipelines/PipelineGraphToolbar"; + +const render = (ui: Parameters[0]) => + baseRender(ui, { wrapper: PortalTestProviders }); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +function renderToolbar(overrides: Partial = {}) { + const handlers = { + onTest: vi.fn(), + onDownloadOutput: vi.fn(), + onViewDefinition: vi.fn(), + }; + render( + , + ); + return handlers; +} + +describe("PipelineGraphToolbar", () => { + it("hands the chosen file to the test run", () => { + const handlers = renderToolbar(); + const file = new File(["x"], "claim.pdf", { type: "application/pdf" }); + const input = + document.querySelector('input[type="file"]'); + expect(input).not.toBeNull(); + fireEvent.change(input as HTMLInputElement, { target: { files: [file] } }); + expect(handlers.onTest).toHaveBeenCalledWith(file); + }); + + it("will not offer a test run on a chain with no steps", () => { + renderToolbar({ stepCount: 0 }); + expect( + screen.getByText("portal.pipelines.builder.testRun").closest("button"), + ).toBeDisabled(); + }); + + it("opens the definition from its icon", () => { + const handlers = renderToolbar(); + fireEvent.click( + screen.getByLabelText("portal.pipelines.builder.viewDefinition"), + ); + expect(handlers.onViewDefinition).toHaveBeenCalled(); + }); + + it("shows no result strip until a test has been run", () => { + renderToolbar(); + expect( + screen.queryByText(/portal.pipelines.inspector.status/), + ).not.toBeInTheDocument(); + }); + + it("shows why a test run failed, not only that it did", () => { + renderToolbar({ + runResult: { + status: "failed", + completedSteps: 1, + stepCount: 3, + error: "OCR failed: unreadable page", + }, + }); + expect(screen.getByText("OCR failed: unreadable page")).toBeInTheDocument(); + }); + + it("reports a finished run and downloads the file clicked", () => { + const handlers = renderToolbar({ + runResult: { + status: "completed", + completedSteps: 2, + stepCount: 2, + outputs: [ + { fileId: "f1", fileName: "claim.pdf" }, + { fileId: "f2", fileName: null }, + ], + }, + }); + fireEvent.click(screen.getByText("claim.pdf")); + expect(handlers.onDownloadOutput).toHaveBeenCalledWith({ + fileId: "f1", + fileName: "claim.pdf", + }); + // A file the backend did not name still has to be reachable. + expect(screen.getByText("f2")).toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.tsx b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.tsx new file mode 100644 index 0000000000..30513f6a83 --- /dev/null +++ b/frontend/editor/src/portal/components/pipelines/PipelineGraphToolbar.tsx @@ -0,0 +1,147 @@ +import { useTranslation } from "react-i18next"; +import { Tooltip } from "@mantine/core"; +import ScienceOutlinedIcon from "@mui/icons-material/ScienceOutlined"; +import CheckCircleOutlineRoundedIcon from "@mui/icons-material/CheckCircleOutlineRounded"; +import DownloadRoundedIcon from "@mui/icons-material/DownloadRounded"; +import ErrorOutlineRoundedIcon from "@mui/icons-material/ErrorOutlineRounded"; +import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; +import { ActionIcon, Button, FilePicker, Spinner } from "@app/ui"; +import { type RunOutputFile } from "@portal/api/pipelines"; +import "@portal/components/pipelines/PipelineGraphToolbar.css"; + +/** + * A test run's outcome. Whole-pipeline, not per-node: the backend reports one flat list of files + * plus the step it stopped at, so there is no per-node output to attach to a node. + */ +export interface RunResultSummary { + status: "running" | "completed" | "failed"; + completedSteps: number; + stepCount: number; + error?: string | null; + outputs?: RunOutputFile[]; +} + +export interface PipelineGraphToolbarProps { + /** How many steps the chain has, so an empty pipeline cannot offer a test that does nothing. */ + stepCount: number; + /** Run the steps as they stand against one uploaded file, without saving or delivering. */ + onTest: (file: File) => void; + testing: boolean; + /** The last test run in this session, or null if there has not been one. */ + runResult: RunResultSummary | null; + onDownloadOutput: (output: RunOutputFile) => void; + /** Opens the definition (JSON + cURL) - an inspect action, sibling to Test, hence its home here. */ + onViewDefinition: () => void; +} + +/** + * The graph's own toolbar, above the canvas in both create and edit. It gathers the two ways to + * *inspect* what you are building - testing the chain against one file, and reading its definition - + * as opposed to committing (Save/Create) or operating on the live pipeline (Run now). A test run's + * progress shows on the graph's nodes, so the strip that summarises it belongs next to the graph too. + */ +export function PipelineGraphToolbar({ + stepCount, + onTest, + testing, + runResult, + onDownloadOutput, + onViewDefinition, +}: PipelineGraphToolbarProps) { + const { t } = useTranslation(); + + return ( +
      + file && onTest(file)} + leftSection={} + > + {t("portal.pipelines.builder.testRun")} + + + {runResult && ( + + )} + + {/* The graph is the visual definition; reading it as JSON/cURL sits at the far end of its bar. */} + + + + + +
      + ); +} + +interface RunResultStripProps { + result: RunResultSummary; + onDownload: (output: RunOutputFile) => void; +} + +/** What the last test run did, beside the button that started it. */ +function RunResultStrip({ result, onDownload }: RunResultStripProps) { + const { t } = useTranslation(); + const outputs = result.outputs ?? []; + + return ( +
      +
      + {result.status === "running" && } + {result.status === "completed" && ( + + )} + {result.status === "failed" && ( + + )} + + {t(`portal.pipelines.inspector.status.${result.status}`, { + done: result.completedSteps, + count: result.stepCount, + })} + +
      + + {/* The reason it failed, where the failure is announced - not only on the node, which the user + has to know to click. */} + {result.status === "failed" && result.error && ( + + {result.error} + + )} + + {outputs.map((output) => ( + + ))} +
      + ); +} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.css b/frontend/editor/src/portal/components/pipelines/PipelineHeader.css deleted file mode 100644 index f559e42795..0000000000 --- a/frontend/editor/src/portal/components/pipelines/PipelineHeader.css +++ /dev/null @@ -1,133 +0,0 @@ -/** - * The builder's opening section: identity above the rule, actions below it. - */ - -.portal-pipeline-header { - display: flex; - flex-direction: column; - gap: 0.875rem; - padding: 1.125rem; - background: var(--c-surface); - border: 1px solid var(--c-border-subtle); - border-radius: var(--radius-lg); -} - -/* Leaving the page and saving it are the same kind of decision, so they share a row - and the back - link is short, so the save pair always has room beside it. */ -.portal-pipeline-header__top { - display: flex; - align-items: center; - gap: 1rem; - flex-wrap: wrap; -} - -/* The back link is the shared Button restyled to a plain link, so re-assert that over the - design-system base (which imposes a fixed height, its own padding and an accent colour). */ -.portal-pipeline-header__back.sui-btn { - height: auto; - min-height: 0; - padding: 0; - font-size: 0.8125rem; - font-weight: 400; - color: var(--c-text-muted); -} - -.portal-pipeline-header__back.sui-btn:hover { - background: none; - color: var(--c-text); -} - -.portal-pipeline-header__identity { - display: flex; - align-items: center; - gap: 1.25rem; - flex-wrap: wrap; -} - -/* The shared Checkbox aligns its box to the top of the first text line, with a nudge tuned for its - own font size - that is for the label-plus-description case. This one is a single line, so centre - the box on it and leave the component's sizing alone (overriding the font size shifts the line - box and leaves the tick floating high). */ -.portal-pipeline-header__enabled.sui-check { - flex: none; - align-items: center; -} - -.portal-pipeline-header__enabled.sui-check .sui-check__box { - margin-top: 0; -} - -/* The name is the page's title, so it takes the room and reads at title size. */ -.portal-pipeline-header__name { - flex: 1 1 16rem; - min-width: 12rem; -} - -.portal-pipeline-header__name input { - font-size: 1rem; - font-weight: 500; -} - -/* Never let the labels squash: buttons hold their width and the row wraps instead of clipping. */ -.portal-pipeline-header__save { - display: flex; - align-items: center; - gap: 0.5rem; - margin-left: auto; - flex: none; -} - -.portal-pipeline-header__save .sui-btn { - flex: none; - white-space: nowrap; -} - -/* Operational actions: what you can do to this pipeline, kept off the identity row. */ -.portal-pipeline-header__actions { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - padding-top: 0.875rem; - border-top: 1px solid var(--c-border-subtle); -} - -/* Destructive, so it sits away from the rest rather than next in line. */ -.portal-pipeline-header__delete.sui-btn { - margin-left: auto; -} - -/* The last test run's outcome, beside the button that started it. Whole-pipeline, because the - backend reports one flat file list plus the step it stopped at - nothing per node to attach. */ -.portal-pipeline-header__result { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - padding-top: 0.875rem; - border-top: 1px solid var(--c-border-subtle); -} - -.portal-pipeline-header__result-status { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.8125rem; - color: var(--c-text); -} - -.portal-pipeline-header__result-icon.is-ok { - color: var(--c-success); -} - -.portal-pipeline-header__result-icon.is-bad { - color: var(--c-danger); -} - -/* Why the run failed, shown inline in the strip. Neutral text (the icon already carries the tone); - it may wrap to keep a long backend message readable rather than clipping it. */ -.portal-pipeline-header__result-error { - font-size: 0.8125rem; - color: var(--c-text-muted); - min-width: 0; -} diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx b/frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx deleted file mode 100644 index 41c73a5ade..0000000000 --- a/frontend/editor/src/portal/components/pipelines/PipelineHeader.stories.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState } from "react"; -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { - PipelineHeader, - type RunResultSummary, -} from "@portal/components/pipelines/PipelineHeader"; - -const meta: Meta = { - title: "Portal/Pipelines/PipelineHeader", - component: PipelineHeader, - parameters: { layout: "padded" }, -}; -export default meta; -type Story = StoryObj; - -const noop = () => {}; - -/** The name and the enabled switch are live, so the section can be seen in both states. */ -function Playground({ - initialName, - isEdit, - initialEnabled = true, - runResult = null, - ...rest -}: { - initialName: string; - isEdit: boolean; - initialEnabled?: boolean; - runResult?: RunResultSummary | null; - saving?: boolean; - testing?: boolean; - running?: boolean; - canSave?: boolean; - stepCount?: number; -}) { - const [name, setName] = useState(initialName); - const [enabled, setEnabled] = useState(initialEnabled); - return ( - - ); -} - -/** An existing pipeline: everything is available. */ -export const Editing: Story = { - render: () => , -}; - -/** - * A pipeline that has never been saved. It can still be tested against a file, but there is - * nothing yet to run on a schedule, clear history for, or delete. - */ -export const New: Story = { - render: () => , -}; - -/** Paused: the pipeline exists but its trigger will not fire. */ -export const Paused: Story = { - render: () => ( - - ), -}; - -/** Mid test-run: the picker shows its own progress while the graph shows the steps. */ -export const Testing: Story = { - render: () => , -}; - -/** After a test run: the outcome and its files sit beside the button that started them. */ -export const WithRunResult: Story = { - render: () => ( - - ), -}; - -/** A failed run: the summary is here, the failing step's own message is on its node. */ -export const WithFailedRun: Story = { - render: () => ( - - ), -}; diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx deleted file mode 100644 index 2c5552be07..0000000000 --- a/frontend/editor/src/portal/components/pipelines/PipelineHeader.test.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - fireEvent, - render as baseRender, - screen, -} from "@testing-library/react"; -import { PortalTestProviders } from "@portal/test/TestQueryProvider"; -import { - PipelineHeader, - type PipelineHeaderProps, -} from "@portal/components/pipelines/PipelineHeader"; - -const render = (ui: Parameters[0]) => - baseRender(ui, { wrapper: PortalTestProviders }); - -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ t: (key: string) => key }), -})); - -function renderHeader(overrides: Partial = {}) { - const handlers = { - onNameChange: vi.fn(), - onEnabledChange: vi.fn(), - onSave: vi.fn(), - onCancel: vi.fn(), - onBack: vi.fn(), - onTest: vi.fn(), - onRun: vi.fn(), - onClearHistory: vi.fn(), - onDelete: vi.fn(), - onViewDefinition: vi.fn(), - onDownloadOutput: vi.fn(), - }; - render( - , - ); - return handlers; -} - -describe("PipelineHeader", () => { - it("edits the pipeline's name and enabled state", () => { - const handlers = renderHeader(); - fireEvent.change( - screen.getByRole("textbox", { name: "portal.pipelines.composer.name" }), - { target: { value: "Renamed" } }, - ); - expect(handlers.onNameChange).toHaveBeenCalledWith("Renamed"); - - fireEvent.click(screen.getByRole("checkbox")); - expect(handlers.onEnabledChange).toHaveBeenCalledWith(false); - }); - - it("offers run, clear history and delete only once the pipeline exists", () => { - renderHeader({ isEdit: false }); - expect( - screen.queryByText("portal.pipelines.detail.run"), - ).not.toBeInTheDocument(); - expect( - screen.queryByText("portal.pipelines.detail.delete"), - ).not.toBeInTheDocument(); - // A test run needs no saved record, so it stays: it is how you check the steps as you build. - expect( - screen.getByText("portal.pipelines.builder.testRun"), - ).toBeInTheDocument(); - }); - - it("labels the save action for what it will do", () => { - renderHeader({ isEdit: false }); - expect( - screen.getByText("portal.pipelines.composer.create"), - ).toBeInTheDocument(); - expect( - screen.queryByText("portal.pipelines.composer.save"), - ).not.toBeInTheDocument(); - }); - - it("blocks saving until the pipeline is valid", () => { - renderHeader({ canSave: false }); - expect( - screen.getByText("portal.pipelines.composer.save").closest("button"), - ).toBeDisabled(); - }); - - it("hands the chosen file to the test run", () => { - const handlers = renderHeader(); - const file = new File(["x"], "claim.pdf", { type: "application/pdf" }); - const input = - document.querySelector('input[type="file"]'); - expect(input).not.toBeNull(); - fireEvent.change(input as HTMLInputElement, { target: { files: [file] } }); - expect(handlers.onTest).toHaveBeenCalledWith(file); - }); - - it("will not offer a test run on a chain with no steps", () => { - renderHeader({ stepCount: 0 }); - expect( - screen.getByText("portal.pipelines.builder.testRun").closest("button"), - ).toBeDisabled(); - }); - - it("shows why a test run failed, not only that it did", () => { - renderHeader({ - runResult: { - status: "failed", - completedSteps: 1, - stepCount: 3, - error: "OCR failed: unreadable page", - }, - }); - expect(screen.getByText("OCR failed: unreadable page")).toBeInTheDocument(); - }); - - it("runs and deletes from the row, clears history from the tray", () => { - const handlers = renderHeader(); - fireEvent.click(screen.getByText("portal.pipelines.detail.run")); - expect(handlers.onRun).toHaveBeenCalled(); - fireEvent.click(screen.getByText("portal.pipelines.detail.delete")); - expect(handlers.onDelete).toHaveBeenCalled(); - - fireEvent.click( - screen.getByLabelText("portal.pipelines.builder.moreActions"), - ); - fireEvent.click(screen.getByText("portal.pipelines.detail.clearHistory")); - expect(handlers.onClearHistory).toHaveBeenCalled(); - }); - - it("leaves the page through cancel and back", () => { - const handlers = renderHeader(); - fireEvent.click(screen.getByText("portal.pipelines.composer.cancel")); - expect(handlers.onCancel).toHaveBeenCalled(); - fireEvent.click(screen.getByText("portal.pipelines.builder.back")); - expect(handlers.onBack).toHaveBeenCalled(); - }); - - it("keeps the occasional actions out of the row, behind a tray", () => { - renderHeader(); - // Running and testing earn a button each; reading the definition and wiping history do not. - expect( - screen.queryByText("portal.pipelines.builder.viewDefinition"), - ).not.toBeInTheDocument(); - expect( - screen.queryByText("portal.pipelines.detail.clearHistory"), - ).not.toBeInTheDocument(); - expect( - screen.getByLabelText("portal.pipelines.builder.moreActions"), - ).toBeInTheDocument(); - }); - - it("opens the definition from the tray", () => { - const handlers = renderHeader(); - fireEvent.click( - screen.getByLabelText("portal.pipelines.builder.moreActions"), - ); - fireEvent.click( - screen.getByText("portal.pipelines.builder.viewDefinition"), - ); - expect(handlers.onViewDefinition).toHaveBeenCalled(); - }); - - it("shows no run strip until a test has been run", () => { - renderHeader(); - expect( - screen.queryByText(/portal.pipelines.inspector.status/), - ).not.toBeInTheDocument(); - }); - - it("reports a finished run and downloads the file clicked", () => { - const handlers = renderHeader({ - runResult: { - status: "completed", - completedSteps: 2, - stepCount: 2, - outputs: [ - { fileId: "f1", fileName: "claim.pdf" }, - { fileId: "f2", fileName: null }, - ], - }, - }); - fireEvent.click(screen.getByText("claim.pdf")); - expect(handlers.onDownloadOutput).toHaveBeenCalledWith({ - fileId: "f1", - fileName: "claim.pdf", - }); - // A file the backend did not name still has to be reachable. - expect(screen.getByText("f2")).toBeInTheDocument(); - }); -}); diff --git a/frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx b/frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx deleted file mode 100644 index 25bfbd044f..0000000000 --- a/frontend/editor/src/portal/components/pipelines/PipelineHeader.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import { useTranslation } from "react-i18next"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; -import HistoryRoundedIcon from "@mui/icons-material/HistoryRounded"; -import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded"; -import ScienceOutlinedIcon from "@mui/icons-material/ScienceOutlined"; -import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; -import MoreHorizRoundedIcon from "@mui/icons-material/MoreHorizRounded"; -import CheckCircleOutlineRoundedIcon from "@mui/icons-material/CheckCircleOutlineRounded"; -import DownloadRoundedIcon from "@mui/icons-material/DownloadRounded"; -import ErrorOutlineRoundedIcon from "@mui/icons-material/ErrorOutlineRounded"; -import { - ActionIcon, - Button, - Checkbox, - Dropdown, - FilePicker, - Input, - Spinner, -} from "@app/ui"; -import "@portal/components/pipelines/PipelineHeader.css"; - -/** One file a test run produced, downloadable from the result strip. */ -export interface RunOutputFile { - fileId: string; - fileName: string | null; -} - -/** - * A test run's outcome. Whole-pipeline, not per-node: the backend reports one flat list of files - * plus the step it stopped at, so there is no per-node output to attach to a node. - */ -export interface RunResultSummary { - status: "running" | "completed" | "failed"; - completedSteps: number; - stepCount: number; - error?: string | null; - outputs?: RunOutputFile[]; -} - -export interface PipelineHeaderProps { - name: string; - onNameChange: (name: string) => void; - enabled: boolean; - onEnabledChange: (enabled: boolean) => void; - /** False for a pipeline that has never been saved: it cannot yet be run, cleared or deleted. */ - isEdit: boolean; - /** How many steps the chain has, so an empty pipeline cannot offer a test that does nothing. */ - stepCount: number; - - canSave: boolean; - saving: boolean; - onSave: () => void; - onCancel: () => void; - onBack: () => void; - - /** Run the steps as they stand against one uploaded file, without saving or delivering. */ - onTest: (file: File) => void; - testing: boolean; - /** Run the saved pipeline against its real input, delivering to its real destination. */ - onRun: () => void; - running: boolean; - onClearHistory: () => void; - clearingHistory: boolean; - onDelete: () => void; - - /** Opens the definition (JSON + cURL), which is pipeline-scoped like the rest of this row. */ - onViewDefinition: () => void; - /** The last test run in this session, or null if there has not been one. */ - runResult: RunResultSummary | null; - onDownloadOutput: (output: RunOutputFile) => void; -} - -/** - * The pipeline's identity and its whole-pipeline actions, at the top of the builder. - * - * Split in two so neither half gets lost in a single crowded row: what the pipeline *is* (name, - * whether it is live) sits with the actions that leave the page, and what you can *do to it* sits - * below the rule. A test run is part of building, so it lives here rather than off in a corner - - * its progress shows on the graph's nodes and its results in the inspector. - */ -export function PipelineHeader({ - name, - onNameChange, - enabled, - onEnabledChange, - isEdit, - stepCount, - canSave, - saving, - onSave, - onCancel, - onBack, - onTest, - testing, - onRun, - running, - onClearHistory, - clearingHistory, - onDelete, - onViewDefinition, - runResult, - onDownloadOutput, -}: PipelineHeaderProps) { - const { t } = useTranslation(); - - return ( -
      -
      - -
      - - -
      -
      - -
      - onNameChange(e.target.value)} - /> - {/* A checkbox, not a switch: this is a form value that takes effect on save, and a switch - would imply it applies the moment it is flipped. No description - a second line beside - the single-line name field leaves the row ragged. */} - onEnabledChange(e.target.checked)} - label={t("portal.pipelines.builder.enabled")} - /> -
      - -
      - file && onTest(file)} - leftSection={} - > - {t("portal.pipelines.builder.testRun")} - - - {isEdit && ( - - )} - - {/* Occasional things - reading the definition, wiping the processed history - kept behind a - tray so they do not compete with running and testing, which is what this row is for. */} - - - - - - - - } - > - {t("portal.pipelines.builder.viewDefinition")} - - {isEdit && ( - - } - > - {t("portal.pipelines.detail.clearHistory")} - - )} - - - - {isEdit && ( - - )} -
      - - {runResult && ( - - )} -
      - ); -} - -interface RunResultStripProps { - result: RunResultSummary; - onDownload: (output: RunOutputFile) => void; -} - -/** What the last test run did, beside the button that started it. */ -function RunResultStrip({ result, onDownload }: RunResultStripProps) { - const { t } = useTranslation(); - const outputs = result.outputs ?? []; - - return ( -
      -
      - {result.status === "running" && } - {result.status === "completed" && ( - - )} - {result.status === "failed" && ( - - )} - - {t(`portal.pipelines.inspector.status.${result.status}`, { - done: result.completedSteps, - count: result.stepCount, - })} - -
      - - {/* The reason it failed, where the failure is announced - not only on the node, which the user - has to know to click. */} - {result.status === "failed" && result.error && ( - - {result.error} - - )} - - {outputs.map((output) => ( - - ))} -
      - ); -} diff --git a/frontend/editor/src/portal/views/PipelineBuilder.css b/frontend/editor/src/portal/views/PipelineBuilder.css index 464d3fad55..343aea95f6 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.css +++ b/frontend/editor/src/portal/views/PipelineBuilder.css @@ -225,3 +225,43 @@ gap: 0.5rem; width: 100%; } + +/* The graph column IS the card: the test control is its header and the graph its body, so the test + control reads as the graph's toolbar and - crucially - the card's top lines up with the inspector + beside it (a toolbar sitting *above* the card pushed the graph down out of alignment). Caps at the + row height and clips its rounded corners; the body scrolls inside while the header stays put. */ +.portal-builder__canvas { + display: flex; + flex-direction: column; + min-height: 0; + max-height: 100%; + overflow: hidden; + border: 1px solid var(--c-border-subtle); + border-radius: var(--radius-lg); + background: var(--c-surface-sunken); +} + +/* The test control as the card's header, ruled off from the graph below it. */ +.portal-builder__canvas > .portal-pipeline-toolbar { + flex: none; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--c-border-subtle); +} + +/* The graph as the card's body: it drops its own frame (the canvas provides it now) and scrolls + inside while the header stays put. */ +.portal-builder__canvas > .portal-graph { + flex: 1 1 auto; + min-height: 0; + max-height: none; + border: none; + border-radius: 0; + background: transparent; +} + +/* Stacked (short viewport): the page scrolls, so the column must not cap or nest its own scroll. */ +@media (max-width: 60rem) { + .portal-builder__canvas { + max-height: none; + } +} diff --git a/frontend/editor/src/portal/views/PipelineBuilder.test.tsx b/frontend/editor/src/portal/views/PipelineBuilder.test.tsx index b1ff80682d..52fb5d516c 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.test.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.test.tsx @@ -456,7 +456,7 @@ describe("PipelineBuilder", () => { expect( await screen.findByText("portal.pipelines.builder.needsSource"), ).toBeInTheDocument(); - // Still nothing chosen, so the pipeline cannot be saved. + // Still nothing chosen, so the pipeline cannot be created. expect( screen.getByText("portal.pipelines.composer.create").closest("button"), ).toBeDisabled(); @@ -601,15 +601,15 @@ describe("PipelineBuilder", () => { target: { value: "Needs both" }, }, ); - const saveButton = () => + const createButton = () => screen.getByText("portal.pipelines.composer.create").closest("button"); // Name only: blocked (no source, no destination). - expect(saveButton()).toBeDisabled(); + expect(createButton()).toBeDisabled(); // An input with a source but still no destination: blocked. await pickInputSource("Claims intake"); - expect(saveButton()).toBeDisabled(); + expect(createButton()).toBeDisabled(); // Both chosen: allowed, and both are sent. await pickDestination(); @@ -782,17 +782,19 @@ describe("PipelineBuilder", () => { ).toBeInTheDocument(); }); - it("clears processed history from the header and confirms", async () => { + it("reprocesses the source: clears the processed record, runs, and reports", async () => { renderBuilder("/processor/pipelines/plc-1"); await openTray(); fireEvent.click(screen.getByText("portal.pipelines.detail.clearHistory")); + // It forgets what was processed, then triggers a run so those files go through now. await waitFor(() => expect(clearProcessedHistory).toHaveBeenCalledWith("plc-1"), ); + await waitFor(() => expect(triggerPipeline).toHaveBeenCalledWith("plc-1")); expect( - await screen.findByText("portal.pipelines.run.historyCleared"), + await screen.findByText("portal.pipelines.run.completed"), ).toBeInTheDocument(); }); @@ -846,6 +848,8 @@ describe("PipelineBuilder", () => { it("deletes an existing pipeline after confirmation", async () => { renderBuilder("/processor/pipelines/plc-1"); + // Delete is a rare, destructive action, so it lives behind the overflow tray. + await openTray(); fireEvent.click(await screen.findByText("portal.pipelines.detail.delete")); fireEvent.click(await screen.findByText("portal.pipelines.delete.confirm")); @@ -853,6 +857,45 @@ describe("PipelineBuilder", () => { expect(await screen.findByText("pipelines list")).toBeInTheDocument(); }); + it("pauses a live pipeline at once, in place, and re-saves the persisted policy", async () => { + renderBuilder("/processor/pipelines/plc-1"); + + // POLICY.enabled is true, so the toggle offers to pause it. + fireEvent.click(await screen.findByText("portal.pipelines.builder.pause")); + + await waitFor(() => expect(savePipeline).toHaveBeenCalledTimes(1)); + expect(savePipeline).toHaveBeenCalledWith( + expect.objectContaining({ id: "plc-1", enabled: false }), + ); + // It acts in place: the builder stays open and the control now offers to activate again. + expect( + await screen.findByText("portal.pipelines.builder.activate"), + ).toBeInTheDocument(); + expect(screen.queryByText("pipelines list")).not.toBeInTheDocument(); + }); + + it("creates a paused pipeline when Create paused is chosen", async () => { + renderBuilder("/processor/pipelines/new"); + + fireEvent.change( + await screen.findByRole("textbox", { + name: "portal.pipelines.composer.name", + }), + { target: { value: "Paused draft" } }, + ); + await addTool("Compress"); + await pickInputSource("Claims intake"); + await pickDestination(); + + fireEvent.click(screen.getByText("portal.pipelines.composer.createPaused")); + + await waitFor(() => expect(savePipeline).toHaveBeenCalledTimes(1)); + expect(savePipeline).toHaveBeenCalledWith( + expect.objectContaining({ name: "Paused draft", enabled: false }), + ); + expect(await screen.findByText("pipelines list")).toBeInTheDocument(); + }); + it("prompts to save or discard when leaving with unsaved edits", async () => { renderBuilder("/processor/pipelines/new"); @@ -864,7 +907,7 @@ describe("PipelineBuilder", () => { target: { value: "Draft" }, }, ); - fireEvent.click(screen.getByText("portal.pipelines.composer.cancel")); + fireEvent.click(screen.getByLabelText("portal.pipelines.builder.back")); expect( await screen.findByText("portal.pipelines.builder.unsavedTitle"), @@ -928,7 +971,7 @@ describe("PipelineBuilder", () => { await screen.findByRole("textbox", { name: "portal.pipelines.composer.name", }); - fireEvent.click(screen.getByText("portal.pipelines.composer.cancel")); + fireEvent.click(screen.getByLabelText("portal.pipelines.builder.back")); expect(await screen.findByText("pipelines list")).toBeInTheDocument(); }); diff --git a/frontend/editor/src/portal/views/PipelineBuilder.tsx b/frontend/editor/src/portal/views/PipelineBuilder.tsx index 746fd61ab0..d5092f5c88 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.tsx @@ -67,7 +67,9 @@ import { useQueryClient } from "@tanstack/react-query"; import { qk } from "@portal/queries/keys"; import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; import { humanizeOperation } from "@portal/components/pipelines/pipelineOperations"; -import { PipelineHeader } from "@portal/components/pipelines/PipelineHeader"; +import { PipelineCreateHeader } from "@portal/components/pipelines/PipelineCreateHeader"; +import { PipelineEditHeader } from "@portal/components/pipelines/PipelineEditHeader"; +import { PipelineGraphToolbar } from "@portal/components/pipelines/PipelineGraphToolbar"; import { PipelineInspector } from "@portal/components/pipelines/PipelineInspector"; import { PipelineDefinitionModal } from "@portal/components/pipelines/PipelineDefinitionModal"; import { @@ -258,10 +260,19 @@ export function PipelineBuilder() { const [inputAsked, setInputAsked] = useState(false); const [outputAsked, setOutputAsked] = useState(false); const [submitting, setSubmitting] = useState(false); + // Which create action is in flight, so only the button that was clicked (Create / Create paused) + // shows its spinner. Null in edit and while idle. + const [pendingCreateEnabled, setPendingCreateEnabled] = useState< + boolean | null + >(null); const [error, setError] = useState(null); const [seeded, setSeeded] = useState(false); const [running, setRunning] = useState(false); - const [clearingHistory, setClearingHistory] = useState(false); + // Pausing/activating an existing pipeline acts immediately (a separate save), not on the next + // "Save changes"; this tracks that in-flight toggle. + const [togglingEnabled, setTogglingEnabled] = useState(false); + // Clearing the processed record then running, so already-handled files go through again. + const [reprocessing, setReprocessing] = useState(false); const [runResult, setRunResult] = useState(null); const [pendingDelete, setPendingDelete] = useState(false); const [deleting, setDeleting] = useState(false); @@ -587,10 +598,11 @@ export function PipelineBuilder() { } // Track unsaved edits: snapshot the form and compare against the state captured just after - // seeding, so leaving the builder can prompt to save or discard. + // seeding, so leaving the builder can prompt to save or discard. `enabled` is deliberately left + // out: in edit it is toggled and persisted at once (never an unsaved edit), and in create it is + // chosen at submit - so it can never be the thing that makes the form dirty. const snapshot = JSON.stringify({ name: name.trim(), - enabled, input, steps: steps.map((step) => serializeToolStep(step, allTools)), uploads: steps.map(stepRequiresUpload), @@ -602,20 +614,46 @@ export function PipelineBuilder() { }, [seeded, snapshot]); const dirty = baseline.current !== null && baseline.current !== snapshot; - // The input needs a source, and a scheduled input needs a positive interval; the pipeline - // needs exactly one output destination. - const inputValid = - input.sourceId !== "" && - (input.triggerType !== "schedule" || Number(input.scheduleCount) > 0); + // Each validity condition is defined exactly once here, then consumed both by the graph (which + // flags each end) and by the blocker list below. + const sourceChosen = input.sourceId !== ""; + const scheduleValid = + input.triggerType !== "schedule" || Number(input.scheduleCount) > 0; + const inputValid = sourceChosen && scheduleValid; const outputValid = outputIds.length === 1; - const canSave = - name.trim() !== "" && - inputValid && - outputValid && - !hasUploadSteps && - !hasUnconfiguredSteps && - !hasIncompatibleSteps && - !submitting; + + // The single source of truth for "can this be committed": every reason it can't be, in the order + // they appear down the form, so a disabled Create / Save button can say exactly what is still owed. + const blockers: string[] = []; + if (name.trim() === "") + blockers.push(t("portal.pipelines.builder.blocker.name")); + if (!sourceChosen) + blockers.push(t("portal.pipelines.builder.blocker.source")); + else if (!scheduleValid) + blockers.push(t("portal.pipelines.builder.blocker.schedule")); + if (!outputValid) + blockers.push(t("portal.pipelines.builder.blocker.destination")); + if (hasUnconfiguredSteps) + blockers.push( + t("portal.pipelines.builder.blocker.setup", { + tools: unconfiguredStepLabels.join(", "), + }), + ); + if (hasUploadSteps) + blockers.push( + t("portal.pipelines.builder.blocker.upload", { + tools: uploadStepLabels.join(", "), + }), + ); + if (hasIncompatibleSteps) + blockers.push( + t("portal.pipelines.builder.blocker.incompatible", { + tools: blockingSteps.join(", "), + }), + ); + + // Nothing left to fix, and not already committing. + const canSave = blockers.length === 0 && !submitting; const listPath = toPortalPath(VIEW_PATHS.pipelines); @@ -629,14 +667,14 @@ export function PipelineBuilder() { else navigate(destination); } - async function save(destination: string) { + async function save(destination: string, enabledOverride?: boolean) { if (!canSave) return; setSubmitting(true); setError(null); const policy: Policy = { id: policyState.data?.id ?? undefined, name: name.trim(), - enabled, + enabled: enabledOverride ?? enabled, // The wire shape stays a list; canSave guarantees the one input has a source. inputs: [{ sourceId: input.sourceId, trigger: buildTriggerFor(input) }], steps: steps.map((step) => serializeToolStep(step, allTools)), @@ -652,6 +690,39 @@ export function PipelineBuilder() { } catch (e) { setError(errorMessage(e)); setSubmitting(false); + setPendingCreateEnabled(null); + } + } + + // Create live or paused. The buttons disable until the pipeline is valid, so this only fires on a + // saveable pipeline; the flag records which button spins and whether it starts live or paused. + function submitCreate(enabledValue: boolean) { + setPendingCreateEnabled(enabledValue); + void save(listPath, enabledValue); + } + + /** + * Pause or activate the saved pipeline now, without leaving the builder. It re-saves the + * persisted policy with the flag flipped - deliberately NOT the working form - so a pending chain + * edit is not silently committed by a pause. The dirty tracker ignores `enabled`, so this never + * looks like an unsaved change. + */ + async function handleTogglePause() { + // Never run alongside a Save: both write the whole policy, and a concurrent pair would race + // (the pause carries the persisted steps, so it could clobber the edits Save is committing). + if (togglingEnabled || submitting || !policyState.data) return; + const next = !enabled; + setTogglingEnabled(true); + setError(null); + try { + await savePipeline({ ...policyState.data, enabled: next }); + if (!mounted.current) return; + setEnabled(next); + await invalidatePipelines(); + } catch (e) { + if (mounted.current) setError(errorMessage(e)); + } finally { + if (mounted.current) setTogglingEnabled(false); } } @@ -741,39 +812,45 @@ export function PipelineBuilder() { return { tone: "info", text: t("portal.pipelines.run.empty") }; } + // Trigger the saved pipeline and report the outcome: what the sweep started (or why it started + // nothing), then each run's terminal state. Shared by Run now and the reprocess action. + async function reportRun(policyId: string) { + const outcome = await triggerPipeline(policyId); + const runIds = outcome.runIds; + if (runIds.length === 0) { + if (mounted.current) setRunResult(emptySweepResult(outcome)); + return; + } + const finals = await Promise.all(runIds.map((runId) => awaitRun(runId))); + if (!mounted.current) return; + const failed = finals.find((r) => r?.status === "FAILED"); + if (failed) { + setRunResult({ + tone: "danger", + text: t("portal.pipelines.run.failed", { error: failed.error ?? "" }), + }); + } else if (finals.some((r) => r === null)) { + // Gave up polling before a terminal status; the run may still finish server-side. + setRunResult({ + tone: "warning", + text: t("portal.pipelines.run.timeout"), + }); + } else if (finals.every((r) => r?.status === "COMPLETED")) { + setRunResult({ + tone: "success", + text: t("portal.pipelines.run.completed", { count: finals.length }), + }); + } else { + setRunResult({ tone: "info", text: t("portal.pipelines.run.running") }); + } + } + async function handleRun() { - if (running || !id) return; + if (running || reprocessing || !id) return; setRunning(true); setRunResult(null); try { - const outcome = await triggerPipeline(id); - const runIds = outcome.runIds; - if (runIds.length === 0) { - if (mounted.current) setRunResult(emptySweepResult(outcome)); - return; - } - const finals = await Promise.all(runIds.map((runId) => awaitRun(runId))); - if (!mounted.current) return; - const failed = finals.find((r) => r?.status === "FAILED"); - if (failed) { - setRunResult({ - tone: "danger", - text: t("portal.pipelines.run.failed", { error: failed.error ?? "" }), - }); - } else if (finals.some((r) => r === null)) { - // Gave up polling before a terminal status; the run may still finish server-side. - setRunResult({ - tone: "warning", - text: t("portal.pipelines.run.timeout"), - }); - } else if (finals.every((r) => r?.status === "COMPLETED")) { - setRunResult({ - tone: "success", - text: t("portal.pipelines.run.completed", { count: finals.length }), - }); - } else { - setRunResult({ tone: "info", text: t("portal.pipelines.run.running") }); - } + await reportRun(id); } catch (e) { if (mounted.current) setRunResult({ tone: "danger", text: errorMessage(e) }); @@ -783,26 +860,22 @@ export function PipelineBuilder() { } /** - * Forget which source files this pipeline has processed, so the next sweep - * reprocesses everything currently in its sources (the standard retry for a - * parked-by-failure file). Does not touch the files themselves. + * Reprocess everything currently in the sources: forget which files the pipeline already handled, + * then run at once so those files - which a normal run skips - go through now. Reports the run's + * outcome exactly like Run now; does not touch the files themselves. */ - async function handleClearHistory() { - if (clearingHistory || !id) return; - setClearingHistory(true); + async function handleReprocessAll() { + if (running || reprocessing || !id) return; + setReprocessing(true); setRunResult(null); try { await clearProcessedHistory(id); - if (mounted.current) - setRunResult({ - tone: "success", - text: t("portal.pipelines.run.historyCleared"), - }); + await reportRun(id); } catch (e) { if (mounted.current) setRunResult({ tone: "danger", text: errorMessage(e) }); } finally { - if (mounted.current) setClearingHistory(false); + if (mounted.current) setReprocessing(false); } } @@ -1056,29 +1129,37 @@ export function PipelineBuilder() { return (
      - save(listPath)} - onCancel={() => attemptLeave(listPath)} - onBack={() => attemptLeave(listPath)} - onTest={handleTest} - testing={testing} - onRun={handleRun} - running={running} - onClearHistory={handleClearHistory} - clearingHistory={clearingHistory} - onDelete={() => setPendingDelete(true)} - onViewDefinition={() => setDefinitionOpen(true)} - runResult={testSummary} - onDownloadOutput={downloadOutput} - /> + {isEdit ? ( + attemptLeave(listPath)} + canSave={canSave} + blockers={blockers} + saving={submitting} + onSave={() => save(listPath)} + onRun={handleRun} + running={running} + onReprocess={handleReprocessAll} + reprocessing={reprocessing} + onDelete={() => setPendingDelete(true)} + /> + ) : ( + submitCreate(true)} + onCreatePaused={() => submitCreate(false)} + onBack={() => attemptLeave(listPath)} + /> + )} {error && } {runResult && ( @@ -1110,44 +1191,54 @@ export function PipelineBuilder() { )}
      - setSelected({ steps: [index] })} - /> +
      + setDefinitionOpen(true)} + /> + setSelected({ steps: [index] })} + /> +
      Date: Fri, 14 Aug 2026 10:55:15 +0000 Subject: [PATCH 119/154] Processor UI snags: fat CTAs, real Infrastructure tabs, one surface style (#7497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five unrelated snags in the processor (portal) UI, plus fixes they turned up. No backend changes. `84 files changed, +892 / −3364` ## Fat CTA buttons - New `fat` prop on the SUI `Button`: 2.75rem tall, 1.25rem side padding, 0.75rem corners, semibold. Composes with all four variants/accents. - Applied to the page-header CTA on Sources, Documents, Pipelines, Users (both), Usage, Integrations, Infrastructure — 8 buttons, all in line with a page title. Nothing else. - `LandingActions` migrated onto the prop; `.landing-btn-primary` / `.landing-btn-secondary` and their four `!important`s deleted. The editor landing CTAs come down 4px with everything else. - Infrastructure's header CTA is now primary; its "Create key" dropped to secondary so they stop competing. ## Documents empty state - "Connect a source" opened the Sources *page*; it now opens the `SourceModal` connect flow in place, no route change. - No extra cache wiring: `SourceModal` already invalidates the sources query. ## Infrastructure tabs - Only API Keys and Audit Logs hit real endpoints. Deployments, Security, Models and Storage read mock-only `/v1/infrastructure/*` that no backend serves. - Those four are now disabled: native `disabled`, out of the keyboard tab order, `aria-disabled`, with the view refusing non-enabled keys as a second guard. - Real tabs moved leftmost; API Keys is the default; `?tab=` deep links validated against the enabled set (the home flow's audit link still works). - Deleted: 4 tab components, their fetch fns and ~25 dead types, MSW handlers, fixtures (908 → 253 lines), dead CSS, unused formatters, 240 lines of `en-US` strings. Most of the −3364. - Page subtitle no longer advertises the disabled tabs. ## Surface consolidation - New `Surface` primitive (`sui-surface`): fill, hairline, radius, no shadow. Kept separate from `sui-nav-surface` so nav chrome can diverge later. - `Card` composes it and no longer draws its own shadow — this changes editor Card usages too, by design. - SUI primitives that are surfaces adopt it: `MetricCard`, `MetricStrip`, `NodeCard`, `Table`, `Collapsible`, `CodeBlock`. - The portal gets its own `.portal-surface` with the same three declarations, applied to 19 elements. A `sui-` class belongs to the component that emits it, so feature markup doesn't wear one. - `raised` variant = one subtle shadow for a surface in front of another surface (the flow diagram's tiles). Same fill as its parent, so nesting never shifts a region's colour. Dark has its own value. - Floating chrome (modals, drawers, dropdowns, assistant, sidebar) keeps its elevation; sunken wells stay sunken. ## Sources list - Centred "No sources connected yet" empty state removed — it duplicated the header CTA and pushed the table down the page. The header's "Connect source" is the single way in. ## Drive-by fixes - The connect flow rendered unstyled outside the Sources view: `.portal-conn-picker__*` / `.portal-sources__connection-*` lived in `views/Sources.css`, which none of the five components rendering them imported. Moved to `components/sources/connections.css`. - Three inert custom properties (`--surface-input`, `--color-border-2`, `--text-default`) are defined nowhere in the codebase — `.portal-conn-picker__card` had no fill at all as a result. - Dead CSS removed from `Sources.css` (grep-verified unused): old expanded-row panel + its keyframes, type-card block. ## Testing - `task frontend:check` — typecheck, lint (oxlint + 4 theme-lint passes + stylelint), format, 238 files / 2063 tests. - `frontend:typecheck:all` across all 9 tsconfigs. - `frontend:storybook:a11y:changed` — 119 stories, light and dark, zero violations, no regressions vs baseline. - New tests: `Infrastructure.test.tsx` (tab order, default, disabled behaviour, deep-link filtering) and a Documents test that the empty-state CTA opens the modal without navigating. - Merged `origin/main` (#7438 replaced `PipelineHeader` with the new Create/Edit headers); full suite green at 240 files / 2072 tests after the merge. --- .../public/locales/en-US/translation.toml | 246 +------ .../core/components/shared/LandingActions.tsx | 8 +- .../core/components/shared/LandingPage.css | 10 - frontend/editor/src/core/ui/Button.css | 5 + .../editor/src/core/ui/Button.stories.tsx | 30 + frontend/editor/src/core/ui/Button.tsx | 16 +- frontend/editor/src/core/ui/Card.css | 8 +- frontend/editor/src/core/ui/Card.tsx | 4 +- frontend/editor/src/core/ui/CodeBlock.css | 3 +- frontend/editor/src/core/ui/Collapsible.css | 2 - frontend/editor/src/core/ui/Collapsible.tsx | 5 +- frontend/editor/src/core/ui/MetricCard.css | 6 - frontend/editor/src/core/ui/MetricCard.tsx | 2 + frontend/editor/src/core/ui/MetricStrip.css | 4 - frontend/editor/src/core/ui/MetricStrip.tsx | 2 + frontend/editor/src/core/ui/NodeCard.css | 4 - frontend/editor/src/core/ui/NodeCard.tsx | 2 + frontend/editor/src/core/ui/Surface.css | 20 + .../editor/src/core/ui/Surface.stories.tsx | 46 ++ frontend/editor/src/core/ui/Surface.tsx | 33 + frontend/editor/src/core/ui/Table.css | 3 - frontend/editor/src/core/ui/Table.tsx | 5 +- frontend/editor/src/core/ui/Tabs.tsx | 8 +- frontend/editor/src/core/ui/index.ts | 1 + frontend/editor/src/portal/MOCKS.md | 9 +- .../editor/src/portal/api/infrastructure.ts | 229 ------- .../portal/components/EditorStatusCard.css | 4 - .../portal/components/EditorStatusCard.tsx | 3 +- .../editor/src/portal/components/HomeHero.tsx | 2 +- .../src/portal/components/ProcessorFlow.css | 11 +- .../src/portal/components/ProcessorFlow.tsx | 1 + .../billing/BundleCheckoutModal.tsx | 3 +- .../components/billing/CardPlaceholder.tsx | 3 +- .../src/portal/components/billing/billing.css | 5 - .../docs/EndpointReferenceSection.tsx | 6 +- .../components/documents/ReviewQueue.test.tsx | 46 +- .../components/documents/ReviewQueue.tsx | 12 +- .../components/failures/FileRunEventList.tsx | 3 +- .../portal/components/failures/failures.css | 3 - .../components/infrastructure/ApiKeysTab.tsx | 1 + .../infrastructure/DeploymentsTab.stories.tsx | 46 -- .../infrastructure/DeploymentsTab.tsx | 233 ------- .../infrastructure/ModelsTab.stories.tsx | 30 - .../components/infrastructure/ModelsTab.tsx | 272 -------- .../infrastructure/SecurityTab.stories.tsx | 58 -- .../components/infrastructure/SecurityTab.tsx | 342 ---------- .../infrastructure/StorageTab.stories.tsx | 80 --- .../components/infrastructure/StorageTab.tsx | 244 ------- .../components/infrastructure/infraFormat.ts | 135 +--- .../pipelines/PipelineInspector.css | 3 - .../pipelines/PipelineInspector.tsx | 7 +- .../pipelines/graph/PipelineGraph.css | 2 +- .../policies/PolicyExternalApiConfig.tsx | 4 +- .../components/procurement/DealStatusHero.tsx | 3 +- .../procurement/ProcurementAgreement.tsx | 3 +- .../components/procurement/QuoteBuilder.tsx | 3 +- .../components/sources/ConnectionForm.tsx | 1 + .../components/sources/ConnectionModal.tsx | 1 + .../components/sources/ConnectionPicker.tsx | 1 + .../sources/ConnectionTypePicker.tsx | 4 +- .../sources/SourcesTable.stories.tsx | 19 + .../portal/components/sources/connections.css | 241 +++++++ .../components/users/PendingInvitations.tsx | 3 +- .../components/users/UsersDirectory.tsx | 7 +- .../portal/mocks/handlers/infrastructure.ts | 34 +- .../editor/src/portal/mocks/infrastructure.ts | 617 +----------------- frontend/editor/src/portal/theme/surface.css | 7 + .../editor/src/portal/views/DeveloperDocs.css | 2 - .../editor/src/portal/views/Documents.tsx | 2 +- .../src/portal/views/Infrastructure.css | 255 -------- .../src/portal/views/Infrastructure.test.tsx | 94 +++ .../src/portal/views/Infrastructure.tsx | 70 +- .../editor/src/portal/views/Integrations.css | 3 - .../editor/src/portal/views/Integrations.tsx | 4 +- .../editor/src/portal/views/Pipelines.tsx | 1 + .../editor/src/portal/views/Procurement.css | 15 +- frontend/editor/src/portal/views/Sources.css | 356 ---------- .../editor/src/portal/views/Sources.test.tsx | 4 +- frontend/editor/src/portal/views/Sources.tsx | 30 +- frontend/editor/src/portal/views/Usage.tsx | 2 +- frontend/editor/src/portal/views/Users.css | 3 - frontend/editor/src/portal/views/Users.tsx | 8 +- 82 files changed, 702 insertions(+), 3361 deletions(-) create mode 100644 frontend/editor/src/core/ui/Surface.css create mode 100644 frontend/editor/src/core/ui/Surface.stories.tsx create mode 100644 frontend/editor/src/core/ui/Surface.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/DeploymentsTab.stories.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/ModelsTab.stories.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/SecurityTab.stories.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/SecurityTab.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/StorageTab.stories.tsx delete mode 100644 frontend/editor/src/portal/components/infrastructure/StorageTab.tsx create mode 100644 frontend/editor/src/portal/components/sources/connections.css create mode 100644 frontend/editor/src/portal/theme/surface.css create mode 100644 frontend/editor/src/portal/views/Infrastructure.test.tsx diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 1948b71bf4..4ddc2497ee 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7444,7 +7444,7 @@ morning = "Good morning" [portal.infrastructure] manageEditorDeployment = "Manage Editor deployment" sectionsAriaLabel = "Infrastructure sections" -subtitle = "Deployments, credentials, security posture, storage, and the audit trail for your Stirling workspace." +subtitle = "API credentials and the audit trail for your Stirling workspace." title = "Infrastructure" [portal.infrastructure.apiKeys] @@ -7472,11 +7472,6 @@ cancel = "Cancel" confirm = "Revoke key" title = "Revoke API key" -[portal.infrastructure.attestationLabel] -attested = "Attested" -inScope = "In scope" -notApplicable = "N/A" - [portal.infrastructure.audit] filterAriaLabel = "Filter audit events by category" heading = "Audit logs" @@ -7548,11 +7543,6 @@ info = "Info" success = "Success" warning = "Warning" -[portal.infrastructure.certLabel] -certified = "Certified" -inProgress = "In progress" -notStarted = "Not started" - [portal.infrastructure.createKey] cancel = "Cancel" createKey = "Create key" @@ -7566,240 +7556,10 @@ subtitleCreated = "Copy this secret now — it won't be shown again." title = "Create API key" titleCreated = "Key created" -[portal.infrastructure.deployLabel] -live = "Live" -queued = "Queued" -rolledBack = "Rolled back" -rolling = "Rolling out" - -[portal.infrastructure.deployments] -loadAria = "Load for {{name}}" -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Deployed by" -environment = "Environment" -product = "Product" -status = "Status" -version = "Version" -when = "When" - -[portal.infrastructure.deployments.recent] -heading = "Recent deployments" -subheading = "The latest rollouts across products and environments." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instances" -latency = "Latency" -load = "Load" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Throughput" -uptime = "Uptime" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regions" -subheading = "Live health for every deployed Stirling region — latency, load, and rollout version." - -[portal.infrastructure.deployments.regions.empty] -description = "Deployed regions appear here once your workspace is provisioned." -title = "No regions deployed" - [portal.infrastructure.keyLabel] active = "Active" revoked = "Revoked" -[portal.infrastructure.modelLabel] -active = "Active" -degraded = "Degraded" -disabled = "Disabled" - -[portal.infrastructure.models] -heading = "Models" -loadAria = "Load for {{name}}" -msValue = "{{value}} ms" -subheading = "The model catalogue and routing that powers document processing across your workspace." - -[portal.infrastructure.models.byom] -description = "Register an on-prem or self-hosted model and pin it to a region for data-residency-bound processing." -title = "Bring your own model" - -[portal.infrastructure.models.catalogue] -heading = "Catalogue" -sub = "Managed models available to your workspace, with live latency and cost." -subEnterprise = "Managed, bring-your-own, and on-prem models — with per-region pinning available." - -[portal.infrastructure.models.catalogue.empty] -description = "Models in your workspace's catalogue appear here." -title = "No models available" - -[portal.infrastructure.models.columns] -cost = "Cost" -latency = "Latency" -load = "Load" -model = "Model" -status = "Status" -type = "Type" -version = "Version" - -[portal.infrastructure.models.cost] -perCall = "{{price}}/call" -perThousand = "{{price}}/1k" - -[portal.infrastructure.models.metrics] -activeModels = "Active models" -avgLatency = "Avg latency" -included = "Included" -monthlySpend = "Monthly model spend" - -[portal.infrastructure.models.routing] -empty = "No routing rules configured." -heading = "Routing rules" -sub = "Which model handles each operation. The default applies when no narrower rule matches." -subLocked = "Route operations to specific models — available on paid plans." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Upgrade to Pro to control which model handles each operation and document type." -title = "Model routing is a paid feature" - -[portal.infrastructure.models.routingColumns] -default = "Default" -docType = "Document type" -modelForAria = "Model for {{operation}}" -operation = "Operation" -routedTo = "Routed to" - -[portal.infrastructure.modelTypeLabel] -classification = "Classification" -extraction = "Extraction" -llm = "LLM" -ocr = "OCR" - -[portal.infrastructure.regionLabel] -degraded = "Degraded" -down = "Down" -healthy = "Healthy" - -[portal.infrastructure.security.access.byok] -description = "Supply a key from your own KMS. Stirling encrypts with it but can still read." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Keys never leave your KMS. Stirling holds only ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling manages encryption keys. Simplest — zero key ops on your side." -label = "Stirling-held keys" - -[portal.infrastructure.security.accessPolicy] -heading = "Document access policy" -subheading = "Controls who can decrypt processed documents at rest." - -[portal.infrastructure.security.attestations] -heading = "Compliance attestations" -noReport = "No report available" -subheading = "Framework-by-framework audit posture, with reports available on attested controls." -viewReport = "View report →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Attestations and certifications covering the Stirling platform." - -[portal.infrastructure.security.empty] -description = "Your workspace's security configuration will appear here." -title = "Security posture unavailable" - -[portal.infrastructure.security.hyokBanner] -description = "With HYOK, encryption keys never leave your KMS. Stirling stores and processes only ciphertext you can revoke at any time." -title = "Stirling cannot decrypt your documents" - -[portal.infrastructure.security.ipAllowlist] -empty = "No IP ranges configured — all IPs allowed." -heading = "IP allowlist" -sub = "API access is restricted to these CIDR ranges." -subLocked = "Restrict API access to known IP ranges — available on paid plans." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Upgrade to Pro to restrict API access to specific networks." -title = "IP allowlisting is a paid feature" - -[portal.infrastructure.security.ipColumns] -added = "Added" -addedBy = "Added by" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Encryption key management" -keyId = "Key identifier" -lastRotated = "Last rotated" -rotateKey = "Rotate key" -rotationPolicy = "Rotation policy" -subheading = "Custody of the keys that encrypt documents at rest — who can decrypt, and how keys rotate." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) and hold-your-own-key (HYOK) custody are available on Enterprise. Upgrade to supply keys from your own KMS." -title = "Keys are managed by Stirling on your plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "European Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "United States" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "Where documents are stored and processed." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} used" - -[portal.infrastructure.storage.empty] -description = "Connected storage and usage appear here." -title = "No storage configured" - -[portal.infrastructure.storage.lifecycle] -active = "Active" -activeRange = "0–{{value}}d" -archived = "Archived" -coldStorage = "cold storage" -deleted = "Deleted" -never = "never" -purged = "purged" - -[portal.infrastructure.storage.providers] -connect = "Connect" -connected = "Connected" -heading = "Connected providers" -subheading = "Where processed artifacts are written." - -[portal.infrastructure.storage.retention] -heading = "Retention" -subheading = "How long artifacts are kept before lifecycle deletion." -windowLabel = "Default retention window" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} day" -days_other = "{{count}} days" -never = "Never delete" - -[portal.infrastructure.storage.totalUsage] -heading = "Total usage" -progressLabel = "Storage used" -subheading = "Storage consumed across all connected providers." - [portal.infrastructure.tabs] apiKeys = "API Keys" audit = "Audit Logs" @@ -8875,10 +8635,6 @@ cancel = "Cancel" confirm = "Delete" title = "Delete source?" -[portal.sources.empty] -description = "Connect a storage location so your policies have somewhere to pull data from." -title = "No sources connected yet" - [portal.sources.kpi] inUse = "In use" total = "Connections" diff --git a/frontend/editor/src/core/components/shared/LandingActions.tsx b/frontend/editor/src/core/components/shared/LandingActions.tsx index d69f90a1ef..750848f3c7 100644 --- a/frontend/editor/src/core/components/shared/LandingActions.tsx +++ b/frontend/editor/src/core/components/shared/LandingActions.tsx @@ -32,8 +32,7 @@ export function LandingActions({ <>
      + ), +}; + /** Icons are optional and positional: `leftSection`, `rightSection`, or both. */ export const WithIcons: Story = { render: (args) => ( diff --git a/frontend/editor/src/core/ui/Button.tsx b/frontend/editor/src/core/ui/Button.tsx index 2f46d5ab75..4b6bf2db85 100644 --- a/frontend/editor/src/core/ui/Button.tsx +++ b/frontend/editor/src/core/ui/Button.tsx @@ -42,6 +42,7 @@ type ButtonOwnProps = { variant?: ButtonVariant; accent?: ButtonAccent; size?: ButtonSize; + fat?: boolean; /** Label size relative to `size`. Defaults to the `size`-derived value. */ fontSize?: ButtonFontSize; /** Padding override for both axes */ @@ -103,6 +104,9 @@ function ButtonGroup({ ); } +const FAT_HEIGHT = "2.75rem"; +const FAT_PADDING_X = "lg" satisfies ControlPadding; + const MANTINE_VARIANT: Record = { primary: "filled", secondary: "outline", @@ -123,6 +127,7 @@ const ButtonRoot = forwardRef( variant = "primary", accent = "default", size = "sm", + fat = false, fontSize, p, px, @@ -161,7 +166,7 @@ const ButtonRoot = forwardRef( : undefined; // px/py override p for their axis; each stays undefined (= size default) if unset. - const padX = px ?? p; + const padX = px ?? p ?? (fat ? FAT_PADDING_X : undefined); const padY = py ?? p; // Sections flank a label → spread them without requiring justify="between". @@ -175,6 +180,7 @@ const ButtonRoot = forwardRef( `sui-acc-${accent}`, `sui-btn--${variant}`, iconOnly ? "sui-btn--icon" : "", + fat ? "sui-btn--fat" : "", shape !== "default" ? `sui-btn--${shape}` : "", overflow === "wrap" ? "sui-btn--wrap" : "", !hover ? "sui-btn--no-hover" : "", @@ -238,7 +244,9 @@ const ButtonRoot = forwardRef( className={classes} style={{ ...(accentVars as CSSProperties), - ...({ "--button-height": CONTROL_HEIGHT[size] } as CSSProperties), + ...({ + "--button-height": fat ? FAT_HEIGHT : CONTROL_HEIGHT[size], + } as CSSProperties), // Relative label size, scaled off the `size` base (unset → Mantine default). ...(fontSize ? ({ @@ -253,6 +261,10 @@ const ButtonRoot = forwardRef( ...(padY ? ({ "--sui-btn-py": CONTROL_PADDING[padY] } as CSSProperties) : {}), + // mantineTheme writes font-weight inline on every button root, so this must be inline too. + ...(fat + ? ({ fontWeight: "var(--font-weight-semibold)" } as CSSProperties) + : {}), // Icon-only: zero the size padding inline so the lone icon centres. ...(iconOnly ? ({ "--button-padding-x": "0" } as CSSProperties) : {}), ...style, diff --git a/frontend/editor/src/core/ui/Card.css b/frontend/editor/src/core/ui/Card.css index fcbefdab5b..62deaa0cf0 100644 --- a/frontend/editor/src/core/ui/Card.css +++ b/frontend/editor/src/core/ui/Card.css @@ -1,11 +1,6 @@ .sui-card { position: relative; - background: var(--c-surface); - border: 1px solid var(--c-border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-md); transition: - box-shadow var(--motion-fast), border-color var(--motion-fast), transform var(--motion-fast); } @@ -28,7 +23,6 @@ } .sui-card--interactive:hover { border-color: var(--c-border-strong); - box-shadow: var(--shadow-lg); transform: translateY(-0.0625rem); } @@ -43,7 +37,7 @@ left: 0; bottom: 0; width: 0.25rem; - border-radius: var(--radius-lg) 0 0 var(--radius-lg); + border-radius: var(--radius-nav) 0 0 var(--radius-nav); } .sui-card--accent-default::before { background: var(--c-primary); diff --git a/frontend/editor/src/core/ui/Card.tsx b/frontend/editor/src/core/ui/Card.tsx index dbdb1a393a..42d4c2d93a 100644 --- a/frontend/editor/src/core/ui/Card.tsx +++ b/frontend/editor/src/core/ui/Card.tsx @@ -1,4 +1,5 @@ import type { HTMLAttributes, ReactNode } from "react"; +import "@app/ui/Surface.css"; import "@app/ui/Card.css"; /** Subset of the shared accent dial that has a styled strip (see Card.css). */ @@ -18,7 +19,7 @@ export interface CardProps extends HTMLAttributes { * (e.g. a list with row dividers). */ padding?: "none" | "tight" | "default" | "loose"; - /** Use the lifted surface treatment (taller shadow, hover affordance). */ + /** Adds the clickable affordance (pointer cursor, hover lift). */ interactive?: boolean; children?: ReactNode; } @@ -40,6 +41,7 @@ export function Card({
      +
      + ); +} + +function cellClass( + align: "left" | "right", + nowrap: boolean, + fit: boolean, +): string { + return [ + "sui-datatable__td", + `sui-datatable__td--${align}`, + nowrap ? "sui-datatable__td--nowrap" : "", + fit ? "sui-datatable__td--fit" : "", + ] + .filter(Boolean) + .join(" "); +} + +function headerClass(align: "left" | "right", fit: boolean): string { + return [ + "sui-datatable__th", + `sui-datatable__th--${align}`, + fit ? "sui-datatable__th--fit" : "", + ] + .filter(Boolean) + .join(" "); +} diff --git a/frontend/editor/src/core/ui/Dropdown.css b/frontend/editor/src/core/ui/Dropdown.css index 8b84af29f1..d71026d881 100644 --- a/frontend/editor/src/core/ui/Dropdown.css +++ b/frontend/editor/src/core/ui/Dropdown.css @@ -4,28 +4,20 @@ } .sui-dd__menu { - position: absolute; - top: calc(100% + var(--space-1)); + /* Positioned (fixed, portaled to ) entirely by the Menu component. */ min-width: 12rem; padding: var(--space-1); background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); - z-index: var(--z-dropdown); + z-index: var(--z-popover); animation: fadeInUp var(--motion-enter) both; display: flex; flex-direction: column; gap: 0.0625rem; } -.sui-dd__menu--start { - left: 0; -} -.sui-dd__menu--end { - right: 0; -} - .sui-dd__item { display: flex; align-items: center; diff --git a/frontend/editor/src/core/ui/Dropdown.tsx b/frontend/editor/src/core/ui/Dropdown.tsx index bf481c9ea5..25019d7c3c 100644 --- a/frontend/editor/src/core/ui/Dropdown.tsx +++ b/frontend/editor/src/core/ui/Dropdown.tsx @@ -6,12 +6,14 @@ import { useContext, useEffect, useId, + useLayoutEffect, useMemo, useRef, useState, type ReactElement, type ReactNode, } from "react"; +import { createPortal } from "react-dom"; import "@app/ui/Dropdown.css"; type Alignment = "start" | "end"; @@ -20,6 +22,8 @@ interface DropdownContextValue { open: boolean; setOpen: (open: boolean) => void; triggerRef: React.RefObject; + /** The portaled menu element, so click-outside can exclude it. */ + menuRef: React.RefObject; menuId: string; align: Alignment; } @@ -68,15 +72,20 @@ function Root({ const triggerRef = useRef(null); const containerRef = useRef(null); + const menuRef = useRef(null); const menuId = useId(); - // Click-outside + Escape close. + // Click-outside + Escape close. The menu is portaled to , so it is not + // inside containerRef - check it separately or a click on it would close the + // menu before the item's handler runs. useEffect(() => { if (!open) return; function onDocClick(e: MouseEvent) { + const target = e.target as Node; if ( containerRef.current && - !containerRef.current.contains(e.target as Node) + !containerRef.current.contains(target) && + !(menuRef.current && menuRef.current.contains(target)) ) { setOpen(false); } @@ -96,7 +105,7 @@ function Root({ }, [open, setOpen]); const value = useMemo( - () => ({ open, setOpen, triggerRef, menuId, align }), + () => ({ open, setOpen, triggerRef, menuRef, menuId, align }), [open, setOpen, menuId, align], ); @@ -150,23 +159,79 @@ export interface DropdownMenuProps { } function Menu({ children, className, width }: DropdownMenuProps) { - const { open, menuId, align } = useDropdownCtx(); - if (!open) return null; - const style = - width !== undefined + const { open, menuId, align, triggerRef, menuRef } = useDropdownCtx(); + // Fixed position tracked to the trigger. Portaling to keeps the menu + // out of any `overflow` ancestor (e.g. a table's horizontal scroll area), + // which would otherwise clip it and add a scrollbar. + const [pos, setPos] = useState<{ + top?: number; + bottom?: number; + left?: number; + right?: number; + maxHeight: number; + } | null>(null); + + useLayoutEffect(() => { + if (!open) return; + const place = () => { + const el = triggerRef.current; + if (!el) return; + const r = el.getBoundingClientRect(); + const gap = 4; + const margin = 8; + const spaceBelow = window.innerHeight - r.bottom - margin; + const spaceAbove = r.top - margin; + // Flip above when there's more room there, so a trigger near the viewport + // bottom doesn't open a fixed menu that runs off-screen and can't scroll. + const below = spaceBelow >= spaceAbove; + const horizontal = + align === "end" + ? { right: window.innerWidth - r.right } + : { left: r.left }; + setPos({ + ...horizontal, + ...(below + ? { top: r.bottom + gap } + : { bottom: window.innerHeight - r.top + gap }), + maxHeight: Math.max(0, (below ? spaceBelow : spaceAbove) - gap), + }); + }; + place(); + // Track the trigger while scrolling/resizing (capture catches inner scrollers). + window.addEventListener("scroll", place, true); + window.addEventListener("resize", place); + return () => { + window.removeEventListener("scroll", place, true); + window.removeEventListener("resize", place); + }; + }, [open, align, triggerRef]); + + if (!open || !pos) return null; + const style: React.CSSProperties = { + position: "fixed", + // Explicit auto (not undefined) so the CSS fallback `top`/`left` can't leak + // in on the axis this placement isn't pinning. + top: pos.top ?? "auto", + bottom: pos.bottom ?? "auto", + left: pos.left ?? "auto", + right: pos.right ?? "auto", + maxHeight: pos.maxHeight, + overflowY: "auto", + ...(width !== undefined ? { minWidth: typeof width === "number" ? `${width}px` : width } - : undefined; - return ( + : {}), + }; + return createPortal( + , + document.body, ); } diff --git a/frontend/editor/src/core/ui/Table.css b/frontend/editor/src/core/ui/Table.css deleted file mode 100644 index fa89fd04d6..0000000000 --- a/frontend/editor/src/core/ui/Table.css +++ /dev/null @@ -1,73 +0,0 @@ -.sui-table-wrap { - width: 100%; - overflow-x: auto; -} - -.sui-table { - width: 100%; - border-collapse: collapse; - font-size: 0.8125rem; -} - -/* Header text kept for assistive tech only, so a column of controls can be named - without putting a heading above it. Defined here rather than borrowing a global - utility, since the portal loads its own stylesheet. */ -.sui-table__th-sr { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; - border: 0; -} - -.sui-table__th { - text-align: left; - font-weight: 600; - color: var(--c-text-subtle); - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.04em; - padding: 0.625rem 0.875rem; - border-bottom: 1px solid var(--c-border); - white-space: nowrap; -} -.sui-table__th--right, -.sui-table__td--right { - text-align: right; -} -.sui-table__th--center, -.sui-table__td--center { - text-align: center; -} - -.sui-table__td { - padding: 0.625rem 0.875rem; - color: var(--c-text-muted); - border-bottom: 1px solid var(--c-border-subtle); - vertical-align: middle; -} -.sui-table tbody tr:last-child .sui-table__td { - border-bottom: none; -} - -.sui-table__row--interactive { - cursor: pointer; - transition: background var(--motion-fast); -} -.sui-table__row--interactive:hover { - background: var(--c-hover); -} -.sui-table__row--interactive:focus-visible { - outline: 0.125rem solid var(--c-primary); - outline-offset: -0.125rem; -} - -.sui-table__empty { - padding: 2rem; - text-align: center; - color: var(--c-text-subtle); -} diff --git a/frontend/editor/src/core/ui/Table.stories.tsx b/frontend/editor/src/core/ui/Table.stories.tsx deleted file mode 100644 index 2e7e0f64f1..0000000000 --- a/frontend/editor/src/core/ui/Table.stories.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Table, type TableColumn } from "@app/ui/Table"; -import { StatusBadge } from "@app/ui/StatusBadge"; - -interface Region { - id: string; - name: string; - code: string; - status: "healthy" | "degraded"; - docs: number; - latency: string; -} - -const REGIONS: Region[] = [ - { - id: "1", - name: "US East", - code: "us-east-1", - status: "healthy", - docs: 12481, - latency: "41 ms", - }, - { - id: "2", - name: "US West", - code: "us-west-2", - status: "healthy", - docs: 8210, - latency: "63 ms", - }, - { - id: "3", - name: "EU West", - code: "eu-west-1", - status: "degraded", - docs: 3044, - latency: "190 ms", - }, -]; - -const COLUMNS: TableColumn[] = [ - { key: "name", header: "Region", render: (r) => r.name }, - { - key: "code", - header: "Code", - render: (r) => ( - {r.code} - ), - }, - { - key: "status", - header: "Status", - render: (r) => ( - - {r.status} - - ), - }, - { - key: "docs", - header: "Docs 24h", - align: "right", - render: (r) => r.docs.toLocaleString(), - }, - { key: "latency", header: "P95", align: "right", render: (r) => r.latency }, -]; - -const meta: Meta = { - title: "Compound/Table", - component: Table, - tags: ["autodocs"], - parameters: { layout: "padded" }, -}; -export default meta; -type Story = StoryObj; - -/** Presentational table — columns own their cell renderers; pass pre-sorted rows. */ -export const Basic: Story = { - render: () => ( - columns={COLUMNS} rows={REGIONS} rowKey={(r) => r.id} /> - ), -}; - -/** With `onRowClick`, rows become focusable + hoverable (keyboard: Enter/Space). */ -export const Interactive: Story = { - render: () => ( - - columns={COLUMNS} - rows={REGIONS} - rowKey={(r) => r.id} - onRowClick={() => {}} - /> - ), -}; - -/** Empty body slot. */ -export const Empty: Story = { - render: () => ( - - columns={COLUMNS} - rows={[]} - rowKey={(r) => r.id} - empty="No regions deployed yet." - /> - ), -}; diff --git a/frontend/editor/src/core/ui/Table.tsx b/frontend/editor/src/core/ui/Table.tsx deleted file mode 100644 index 417df3d6c1..0000000000 --- a/frontend/editor/src/core/ui/Table.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import type { ReactNode } from "react"; -import "@app/ui/Surface.css"; -import "@app/ui/Table.css"; - -export interface TableColumn { - /** Stable column id. */ - key: string; - header: ReactNode; - /** - * Hides the header visually but keeps it for assistive tech. For a trailing column of controls - * or chevrons, where a visible heading would be noise but a blank one leaves the cells below it - * unlabelled. - */ - headerHidden?: boolean; - /** Cell renderer for a row. */ - render: (row: T) => ReactNode; - align?: "left" | "right" | "center"; - /** Optional fixed/min width (any CSS length). */ - width?: string; -} - -export interface TableProps { - columns: TableColumn[]; - rows: T[]; - /** Stable key per row. */ - rowKey: (row: T) => string; - /** Makes rows interactive (hover + click + keyboard). */ - onRowClick?: (row: T) => void; - /** - * Per-row gate for interactivity, checked only when {@link onRowClick} is set. A row for which - * this returns false is inert: no click/keyboard, and not announced as a button. Defaults to - * all rows interactive. - */ - isRowInteractive?: (row: T) => boolean; - /** - * Set when rows render controls of their own. The row keeps its click as a mouse shortcut but - * stops announcing itself as a button, because a button may not contain other controls and a - * {@code

      aK^R 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 literal 102114 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 diff --git a/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png b/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png index f12133f4f70b11f534d26eab1b50a886acea02b4..dab3d43aa399e597bfb2442da32d432020643875 100644 GIT binary patch literal 173332 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℘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 z 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≫L4_Rg_lvI#-cY>-;_SVw%ia+@hJHcnmTYd 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%0A+uPX*?MrAM#WIQ@iftH z(hf0-+HWE$SVD#DQPrh3Sd)h>oZ%!58?!BkT2l6g;O%71j3AaW5gS$7!N3%V+R*xG zYE)KQQY~V>Uny4?XyyJYWXljH68%MT0fMD~8YtJCe7EM>__#IF=lKcph>H4A@3cfL z$U+2Xf>f;)Qiy?j`_t#rdz8XLA0nSgaX4BjkoE)`9pXkwOd(lgYFE~5(j#o3QV~)@ zR8y1M&dEz!C$1s!Z>G?=S=8|J2NV54PZen-MHgzmu>aik3e+|PzNqiuTB~`R9OW=BjNnGVh^(|&XON{3C%`!Y zoiH4Mm=Yw;waREJFod`Eg1)tlK>`hg;Q)b%#Mj%8k}Cr>j_W{0#snzLm&<6|(SUKi zaLA&k+-&IPvq#_khc|xfw@*%=zV?l8I(hpYmp=EY*<^C^uDi;{+Rn32U3&P-m%s4& z^Iy34_-pRI;;y@ozxp*#zW3cXz41*OH{2j17ry+^rTBO!t;69v(HcD68 zib*S_?(PhG`zofT>(|#6x=GQlX)eRwPMfZ7HtR~y%psyh%WC^_UG8_Xp?%-YW>$x4 zRtG;)rQ+Cb6{$t)tbgFwf9=$p-ZVdTdf3~0!;k#f$=hD>!WZr_wPUy3{?c#wrpfW+ z+IRcsUiiW<|Ho2?($8dAstxP&=@)Gaa$k9_Wze&OoZ-~HJ8-?Q`F^V9XUq1L`D zX10EKQ&y`-e*bqL{=M2yCbP{$!~SA#>#~`4(`moHS;dyaAfmNcQSG#=yL*$#bY*6P zd1HfjpNWdqVGuDXs*}#Vd868}z(OiWAOP@Germ$Up5SpVghx7+mTWC4*b$>38!{6U z>0rYW@+^i#L%>6AFXCAOg`|P-YKnx2vt$q6>SC$o!QGTu#S;K3B~C^p@-DM@ySH!W zkJMq<|4zlhq7w#+Oo$;V4hXayY5v<7gZDfspUMM>Ltd71B)TAs!P%6xn06B`7eHau zr1b-8#2`_|rKFc8K@c)35JyTD(EDLTxqBrIU#PomA|z9B#2%$0H1J@HRMs~lDWbW^ zf7>o!ErPxWaL>rTk)DzLNLYeDqjzgHq~e38Cdo1hRyoM5r~qiLsDt7~m~m z!2tGZYxER&Vw;YZlW~b);y(F>0lGB_7Q9cRF;16T4q^LiXbll98@Km}Efj2`m&6e5 zQ=Ep6Q2q;Pin%$NZ@V7&F@$3a=R{g>4ryY3`c7)uC$Dux;I4(;%Nk|yK*XTM41q6p-Y$mdwpH7BBjx?L?LH1}qD~6q1xn-$Km*Ne$pq7=r+`>rf}_ z>zAK+{F(Q^|E9nA7yHdaw|?7qedLp$*u3VdW4GL@BIm#K#hquKABOt)@Bi+xJ73kW zt-a*Wf8As6dDrPzzowthMeOYT_tc#oJ9cPTF1k*K)pGyBh4#$dbY`k`e_8u(Fd26D zY=7UZ_S4DQ@#Fh@yOS$U$YQAbd);)_Rk7XOW>WopO}eh`FG{bqN--;4+1t9b+S{9o zNY{6>Id=WAI#?VcBU(i2VEyKyi;q0~iJ$&Qul=!~Sik;;VK#gD54`P`y}ep%SzEJe zr7JtnKJ}UZ<7c+-yLWo%(6CrqQCkh&bhbG2+#mnrzxRsof7`KJZ=Y;#%COuy^Xzl? z-~Z+JzIXlD6;FQX!yo?pKXL5(>mUAu-*5TVLtm{{N)h?;Z~f*Yzx&%o>U6%=6pH4) zb*Nom){YzzGplvAx6rPvpEy>l)LLy=c|k#{`FXXfeK!n4oBkjiVX?@i?q=|;I5V2mE8j{Y7j=8)x zDy1;VS*}Gj;GZbR04f&-skR%FVo5f$B^pLN+c#3JNEyDCsWLM96yg2N7w>VIpmiTpBeWv~@fo0e2FhyaMil{6@_fK`=)NCVg^gCkc2%cWJbQ`AFLGjuMn5QI zrS;$o;9QbEfcquu0ji}PgrOu@&7!M_Vh2oXP8>D@mQoF7P57&1b_xVwf@LdBC`Zk` zjXEj}knY`CP^zNvh=LZJs3jX8PU2tIIuP1hS%XV0o5-f6E4~$@)wHtGQ;eSz23k!r zOa$TRt|AYYRE3iGJB%dHopBD`yId44h+-wA@#gtJtnfxcQ%{!Fd~rVqjsL8%MFVe& zdQBqMK=XzH%`F2;Yw^ioqcEjpf(4BMKx$`jVYyv>YkWjW&W+K}6a2#BH4F5l1<+c2 zbsVr`hE%za!Yu*a=RCK^m_*XPE|A6A-<$ifkKEL>Iwr1WqNT9^*_?l?a0B&_-Z12|iXN!F8IMArU3mwSIMkJdUdImVQj|plx)#5C9l4J9_3DQ+_P0|$DGpnaa+vpEcq+<`p5ICc@&Y)gF zwLyVEp;bKn(RG9vm&uAN6ENjO#G;DHaU@Me$j}9dI6xkVxo?MRpxuJwPNI7?p>)w8d?ai-jWOktGXu&8|PYXHOK){trSTCP*a1q(fRY*G*K)y z!)Pm;#^^Yb8oj(=MsAtdF7Y3{FeNd6nA~0AAWjtxA8kKP5eXqH#l7A{O2HxXtq53P zHxwE~y>zZXm(&mOgVE@)-lk|C1@J91+Erk7Z)=e}_7{>4kPlP4el(0gzC?(aK#+pVWxbN9(tymo&5 zOGIS3vwiNq&rgb&)xoM%8y3sG=bmYAS^9|#Lmh^dOlsFHwk|C$p5N4$nCeTu@mrpF z&--_tdc2#?=f{q;Z2;?Hug&9h^H4XR?(goX712QkttNG`SC`9bRYfP8n}gNmst!Zt za#n#$mtryu^CO3z{=`S$^SA%!mw(Uqo_@m{H?FC0&IqHu@&jJvR<;44Ak&`?cWkyJ9+s8XJb`jk>o2$W!2n_yWf@#d(`G>m`^xuc3H z4vQ|SR0H|~EJI040m2yQDWLdC7jEi~CzZ~HJl5W_8__Tq{8tX!FRk zz?*#tlN<|(j4BK9SkSppbe^6N52Md0z^bHBkD5!%ei5sa zIvluP5rjOfC1E}|JD_m00mZ3TY}x?6GbOk6poDXbNn$V{B`p_g(mCEUa}q!VuV*O+ zlU*_(cu98ya!yObl!B&X7Mvf_9>DW{C5l3(#MK-U2NZ@vUSo<4NfIvxY!xbrLiI*u zZ;uJEZwcHG7%E%3fN#l%Q)*G9U(>oW>z$6XzsS6?ry&KNWX(<*0do@+#1*}Me6Pgw z$RSI7&qYrdt5WZUk%z>}PE@2+W-L%piq9!P@h0Sitg~S%d0%t#nU@2p%l8CnM1T_G z%I69>Dv^@-wNRG=9x0yl@gW0y@vcK5TPBM@+_$)qL8rjVM$LGF@s4N?hXl3Dq5l~I zAuLhR#$ioI1XaYYlPsUL-aIe4yF^HA?fqJ85M!4E1_r&)gz{m<0@}rOjQ(#Qg@XRT zCdfszvLrKMF7nN*LNOLn70vDSkN zfh`^6DJ6U^0&06^w3WkhTqk5W>08ilxVWwb6N|DEpQ1s!$N(6^wod1ZZFFEd~xyO*?aDhuG6)RopWa%f9E@n z-gevU_=#8l&<`ED{yGuadh+p$UwlwYSuK{URjo4Ea=G{1nYvubr0crAhz_e&?K|!J z#f9x>|M;U&h5W0hi*FUC)4HmvuB@uMx^wU*~a>M z6X8EoodE~ zU#XtU3n1u=vUi<&g^GH!O>p%%*%Im&w&a!+T`j_`Uk%j-Pj%GJyg&{p#;zkp*Ih#( zrJ5hv;`UaQ#~=eYP**Y1M#?W1Y=+DJM6zno20&w}JL_K2@`_+?K2;8ykmH$smBWkq zW3M%x1*|Y+U&}7nzF(#GMZF%2qlc2q1TJeJ5D0;_=Fxo!s7pquBH~9}_ye0sY3qzf zg+vL^mBND-tc!9iMb=M^0aP8RHZRM7l^Y?S7x@YFF#K*F>g-Th6Xl4y*;F8cn7F_J zjB}|%7v(}pXeV(G3wuWhMbuO&z9*Ws{vOI(DCXm)jCirby++_5Wi{AwM9@eph#ksN z$2B3YAXK9D6FGq6M_0tA#sC@CLPz^SO~94WDF6&%4FOonXo+GMU;tQE2 zzpei@Mn8m#)YXmhP)d*n9@d)MUE)Ci@XX3J@P?V2ej;eEkc;1LqmgLxib_8}`&DlOk0+pl7H1Bb9O=iY0K zE$+>+EHkUJ{()La8J!MD*+Yg+H$z$=bHUZEXlhuICB`gi){#Ec*T^olfo$&n*@S)6l_>5-UODN-xPQbnxLl-hz+k(g|;Fa(i44S~|R zqm3iktG@!_gvJMWKV%F{j>4}rdnj~97w0VEh>ED%Fc`WmS_^}lr&TZ7W4bJvaSMuk zV19rq3^}~|f|b}WfzVdNa4gIn*iqD*OgN(ga#_4XMClp|{9d$R*?@oElXfJ2k*?G_ zJoZQLy!xxZN+y$=zV+LO-Mvm^>&$Z(AA4-NIhQ)jW@}Hq|Gh8$w(nlQ@%qzmdVMt& zk!Szp6Z>agm~O5w7elSpbQlI(ZEX#!rFAylSf8!0Z*On)Wv!ac4j+2(9lv+#t#3Vf z*IldC>cngAKJl8n+siDrw;uS9Z{NLe!D{U$lhv>qmaDe&o0-&Mkkak#Y~T0){_3sY z^Zgedy#L|%zi+a(R)?V_N3TG)@S&}lMO4-5Fk4^mM3-ltf9B~YtyXPdciK-kH$`N% zTA}MWlDMe0M$IyvPN%cR!G>DJq$^@3RtJ+RCZ(vMWlxEmi1Z8#ijd)}M;C6xkMaSO zCyzynTDzOAL%CE=@jQusRmeW%3=Q^JAW;#GrZ*;kWQKw)=4%76(2HTuUB$4*RRNzE z^vI(GHqE$<3kmOTaC8YT5%HsyL5WqqH|{coFjBqX+Cl6As^wE@T7dFwTpM~Jy1-YlM6 z)7g=sl{%rTD7QOPWs(gj5B_1Xr9cz8+DaP$7i36(qco+NWdRm2dKwuWC?5F`CBGQjW6h33=hot_&Uli}~GftZ!%#Dxz)9SA&uq~bRrbBAvR z!Yh-$o6U#){i2z5N?bNjO{>R6p&Xuo98UP?6XzbMP;(U6z^|j)Ku@TkSTCar^z0*~ z(Yk=KiddVF0_yC72-0p>!VOeKq)cbq&pdtd6|da9=9To)15QVtsOaf^3|`|zjV>6*|7M+fB5CgPd=`*S@kGUhQ0mGt50A5_1|cvbhdi( zo$uH^f4N(qi&)=JWPksePknN-wsGk6DJfz@9rpLPo_*?p-}%k`r=MPKUEDhJ{OaPx zFaFxEU3}=Femb>U#iS{yrR!gK;J(M-``#x$@PXCNZkbNlhrn@y;k36>Rjt)(t=jkf zY}QX_Wo@RjnRK1h!G_8eDTKKy;fvPZs_hM;Ot4y7FCQ2vl$N+DE+i$=Xja6O(;fF# zGD_8UZ8uG&fZ}x1R?Jj#LtGB)230#KBuViU+%gzrgo6PH_LMq0x$b1nDqD@jkjm zb^M%ZF5N0Yw=uMMH5d;g5+(^PNrE7VBzu5rMMab0n#o5-Lv3li5e`&}TcG3^5}X3Q z#1BASf`54u11Z2g_~6RGf~yjYAxIW~C{s=mT~Qm6;`kdKMHvD4KtWTn6f(@L(C!el zDGo~t!Nig0Y)%}4qOO89rEr~R%V?~*8!p&3Y>Y7= zK7>GGD&1sS_x5Zx`1xn;PbsQl-*iJ7rNZzOo|HnXrBBL>ynqS5z=Aay4t{T#Ub3|q zOFk9+$ZJ{$iXeqohu|O6P1mA1q+Ds?Rq#zl1ohdC;TFal5%_8ZHx}N6cei|Z6_o;{ z!gnaRD?(Q=eXm^7btI6D&nZA6?f|guV?dXSu%a}*nj#^rAIvGdk7R~sA5&xF2|`0k z8jP4C7!1iOhhJkXqZFVVJ}7dMToc%oU?!!y1ApiY%BK&um3)+;LDTE=s=(>#HyBnT zO8hGY160C2t3<&?Ip%=dks2*hRY3TbA_H6tYysG{g`sTx4DqeaLzSCAhL3Z$@FpH^ z8F5?JA?FsJlJT#$^$dt(^l`@7P>sdCZ4`;_L6Mt3e_VeIHoSfq88_vdO$8B(?re; z0}LaNV2#V^_yZDGqf;hSO7))^z8Sj~`pUsXKvpK{nxdk9HeRAE=+#MBqenSg8nG`# zr$U`BEG}dp!T43z<&O-g~dqm5EfVT16)PWAA?VP2cui(-X(Ku0Q*Qd(Pi~ zpHBK^9n^ONsLXMXa4tp`lVmH z`p}IxmdR}Y!o~AXJW?;6Ut60>wWr?kyHEYs|F7>Rv)Ob&`*qts&QxdfwYt4E>3W^? zL-lse()NewNQHSZX9j9us~=J6%c)C1`G6n$tn<79#fb#k3uwGK zCoX}9(^9yatX;~_qHteiE-HBpYdj4`n^G|^X#fa_U>2d&D_!l zH*YjUiZ|{|lTZ{SX<}{mWTp>$jA1@m$*|gK>q!%|2q9OT;jNoGmh5 z=p7I+imz$Z0qp5UGsI8NXBEP{bDqvb#XziiKV*;NGyytS_~U zR!X~_27L-+Ago#AB8u(3=srxeOpP8?l~gd97Js59FM2w~!B7d;yF-S6 z-e^cHUQ&SHkV2r_M%WPFqKhN8_|tD$G5+H3q1>c#6Xi8&HNO?i2tYD%k!l z7E_Wjq6E>Lx3tnaUm-~x=qd7Q3#UT}7%)$oqyK1|L`~&#L{-EsYUg9~!Dk`f7H!)) z(8gO~Tt=;m;h4~&RlQ)H=h5xk%49Mu_H0kR2`u+*95$OZJP1Bzo1oSV3J13PNB#F75b-2ut2r8 zzCp<(;wli`1hPUxg!%bJ>2Lrlfzb-P7Nd!g#j8kA$+u}`Y>{OlD9W7?)U3qgM6J;{ zexotrh=QXqG`baG&}j`t8PI&Fm06ouUXnFnP`virkS4_hXbK`o#el=3x5B$A=PS>y zM$2kgwyJNu44s*Jn6y*dV5w<*o0ciAKbpjQ(kg^xF zX(OSw*DcVz1dn$>c_p$dT;f5RfSC-75!??-eRO64^hkQ+c)-{&KoWOl1mPzXacYFD zl|5TH<|T&?KWqe9yR`OcFjPT;i6F%98Uj9R8%)4>kxZ8*s9^gq^x)*4DvO&k2ND&Q z^wza=cTMK=!D=aYzfrdE=ZI2xZBLa2jsp&FF3HpuK3S zB2t>*0qT*mAtXsKCYBiEe?HCx0J#AYaFR)ciHB+qb7+VzD61$P9Jd;yu83$sY?KyB z>o|~@n5#59P&^&%E?aa`qO#6UmI8xCc5~WcjB3V`5Zyr28;q!eMUJMwP+SfCqT0^H zbupMh#I;!(W2}HgVyFR7h(6_DbdsR=*pr4v8B^wlReYxwgIz+zAAN_Tn@Gr5(~%5* z56&?n=IB|4)yV+S_y!aWPL-xOYY>%Lkw}76XjOjWk)E4Nh=3-{iR@sGvJpQDP}W5nuUp3ES1G*x2#n0LMN)5dItasiMFvO$US~5 zihY@_i8U5SysjB-=hZBn?R7_aI6foKRq!BJ%<@GL4=9&qwbX@Tquw_QBwERDTgoc( zZJ1Fq6<0Hs{mX~VkGIyw&*RZ{LAYLhh7>=tbti!>zzOoe5xjujQ8YMb4~es8jP=7G z?38I2)8huwn>FlYMk>u;^P;9V^+_Q0x#W(FaW=BqYuz|+px%L8Y`1pT>o{V5LVaw( zg*-yID~B{3E^)wdEy_gDV=xo)LU7HutCndctTN0oy!ydYgS~8B9F7=s^W!@N`=%>4 z8=CJ-V1>hQBpRr%#AuIoEN8@6cFpP;o!5D_p;WC^!X(hoG`)+Z(aAqPyyNtbbWt(% zWk9Kws3K<9u}rnV8R5*V$~}f3kIz3IQLBsF1{?)`z~n#w{PX*N{2%K-{^P&>U;lUi z{>RKcE4esx@8*OA0b=7)YRY4_!xH#o=O#~Sw75ln9=%bh*zHe?F<3lX7HQt_b5|Qb z?G$@(MTbjpVsu!=i50J80V{LdTwb^YuCSQZK&gzswyH3oGjU}cehh!bQg)mj>Ll(p;5}Eef|21RwVx#- ziGJlnWE}AD9vGF!C4{&4Xu5l>tZ%eE zlMj>;;1h62!sSnNe}e0Z$e!<{dV*fZOA{;72<@m8v!4~X?0(Zg=4gf~=B&!MeIi#l zeH<$dQe8}3DSl_KuY|fH5GB}%Sj%w|#&)mua(9eA{R%P6j#I9XAZu7~t}On1s{EgX z6De)ydt=-+3CrCtG13TGu03Mck2Axe|k6j)&1xG;awUmHPwCo)4#oy%aT}ne0J=; zgy%nF(u~Kl0K-(Ft8d@Ba5)7fa+97>&m934`?ZnxN)=eesiBuJ-L=3PmTgtPWhs{x zdvk~V%3ZAQ7k%II&w1zYIMUFC7`#94peM5X7&>e2^m#H*Jr&WwQP00z6u(;G%*7|6 z=ne=$-5QjDq=^eYaYIk=#geQb*2+(gDT}ch`3fAZrU!2qCspUoqLi}h8I&hcIMr2W z*d!Wdx2LM#NfFiW1ivPYTFQGjQ{m`dY-i?K4Vuonl>$cF)r^^5w7aYhM8GEG_T7)$~(8W6HsB*eQo@a$#K7f?2 zlzKb>Gw;k^E3}oGN5AcM*)~wV`E1~AF@3LI`L%-yYoeVWdE5!tEHyUuxut=?xyn8(yE;a^*O6mlH|C7zC6t^}N&(VnQ&5n^{qsU)(Zq3eJi&X8$dc5Wz>Utm zt(2?gegge9r)uRB=iyIqntFYSXUXs8m!C!}t0zpQCA32MyRZTPRi!JH5RRU2fv52d zfY!qKwj$|qk|sO8PkQs!_qb4(D%asVSK~v-PB`DopA67i zmzj2zenZ!O$E-LRkp{zuwXSomxM+N{#8YlP>D{g&UyJ0?w%=}T>%u^6gaLHoJDbU( zIKrBuKRb4Ly{xsm$Pw*58>=%ijAmDz*RJh#rbbjmX=^rnVy zUKfR_E}9K+Xku&r1(17lH}5>{Ual4bQ=Wh#a-4IP&+l%r1qF|*57_6~6HtU^-I7^b z4~u5!A@Ta~1@A@gnP!+BWGbob<+f=p+0015MDU@Pdd0*(?cLqlluBduX{zJ7Gdtom zZLNWv=LZ6oe1<-*{zI~99@g8uywOjIq6v?TFrWZZPzf}xfaNE!6>%v8#fW+U_KKrq z%Juhi)~_7UHC52n>mCR%g)Z96vA zU8hfJ-`fhkBFZI1qpU|~A~4t#)6iLcx`_IR?%8Ee_`-mommmE}NpG7o_Tl=n);TD_JR3gw*4X{^AesMl$A6OBjJ!L>d= z#s#%F$Z+3QJw>OrZG)+Sbi>Q0V(WG>oIO*Y+~){fJ&M2pA;T zt_Nc1(&(Aku5R2YO=zgGG{^3vg!WX1KD%B6y0VRAdz2Qo5j5wrVL}>;qn;IpPVuW0*M*)6h!F)1tQ`I04j!ObfF zp=x<%09E)xumK{_8@tC$PB)YsgfFLa@IQ|K4-On*fm3b;OQld*%w`n;7f-0fH%WD; z-=>dGr)WTmCKoQ47x!J!C4_-v(gefEnIyx;p2x%OgtfC9Nf?k;HC;KF9zj?EDf&_F z_Q_>0O<&FyHIeV?bm$KG|C3r58?(G&yVr5Ww+J3zV4mv+yQ@nPie=HEhGV#gHw zsitLN#llRs9@ox_IMTDC_TR$AUPrjY2p}+4U#T!D!z0INE11}WWWaT!6g%xMXt+fi zsLO5{Yd3k*uwBkQ!Ahx6?fIUS7vE2g^+IuC2lPe0vCXcjcP5BS^Kb_n z({|sW!3x29r9l=N8so0lb~OL2bF@I|xh1Vt!lLyc-TW?&)ypJ?sX|1~#!8vs)#VkN zaBK|W-EGNB9%>sq(Q;{jf<=xLo2WW*yG227>{s|4sT>AW2~j&Cy7)op&>(^HY_;~y z4`P>bfO}R2+pQ}G7QV@#mMu~hV6gw;sIt$GC$_&_BJohsF7qTRJ~6vBgDWD8jXgTf zdbE`Yw?Tk`S2e|tC)E#kCG+B3=+ zSiG?T7yv}~(02!%m2|3)y!v|RTd9 z;KeKVMgL;v;Gl%aojKo`!P0BoGVoXCe}!@1&6bGW*z&dVqFFyQD|NcM))ibD0LruW z4}Mau#@c1#&dyN;Nq5I-+6k@pP%WZVAj5>UvEDT^5<9E3qyc)acfWhCNrjh2j^#r5 z!Lo_^+_ue^jZHq2wP1kLe#RdM2esCkkImy&A+53@hv${4P@40gIvln`BbIPItTZ-M zRrT+B_2See-N>_@2MnpGJKcTXJ6uOUJ>=c((|+5Ed&h-QB~}m{A<(Y=)%QP0Rbc1k z9zy3$5H;4}?U_Io6x3Vrz^v;~b~$%7dXgOXou7sTK+-kt#9lW(O%z*tn3Xdg2YScN z@1(8BXsg`3u;RKgLV10WXKq)9#+iEO%rPHG+5tbU=I_!1-rAxPI6IyljBwe!$Ttso zNo%~-;G#OX)Tr{D#>LjU7M{v7#h!GpmLsR@QvQRTb>nJblQb6kvnXY7n-4SdhlfU3 z2a+WStQo5hn&KX89bV%m3mN^CsyzFjq&@^*?M5uCyQe}}ZXGZ%ymuGFb@|Ekrz7Y%F zJoe|I@!`)m4RMtl9jsT5-a&j}o6cjUDjojyxlfQZfOu)GL`+jMy+%-*fT zA(Z9}Z7_fD08*{CTip1w24P%kh0kn%Uo=0`>ZsC6zJBi%x z^5LGmn01WT3v9fpsq6jRBFr zH&tLB)jn)DYk5sV0};fdE9X_wjtQ6){fw1ycefg(0EZ>L;ccMW@#vnw4&)@i<6Z^K zs*6AehWkUPKOSZ~a$NYp*JLs95nu%@ih0}4c-hT%$0={QjKTkc-DTkqw4DORsKT;r zVZm_7LS~`d>8jTE53#oGgig-H`E;-Q4KAAXsjlDks(eZjfrgEGPL@%|-FR%5lr=i- zywxAK8RVq`9<1=U(l{@4tLsR#kQH0YD^>y7O)T|2#?rovadb z%vv5oE47zDg#Ii{9X>0Wne~u`Oxj(6ONKP<>=@jT3jG2tc^Yto^O$24%XnQmGqwG! zn7>NO>%LC{yWO3|Bo0GLt4bx)U2}yzJyCb&WHnZ0EqxHo_Wz$skOK_gb#V>pXRyu; zs8d&2(SWu(;98FekXYTHQMmae0j2%|nR{}RXN$Z$>*L!f1_v3g0jXRfGqVmP_ndqY zMlC=2_^90W83C=uOTBpLr52+57fYDOIh&`&&DV`F4aez)(PurD-#tP>u{n}f-LESf zj1{*fCKzz8F(Qna9=ki=aWYUIEY*3RmAuKBZl`D)Ykg)lx#p7zyikKl=Di+-5wD;1 zUa!!$qEXg)6bzzhP$ajw3V)2aURA=zk z`w#TS!wP5;r>>C@nI0k8gv&u9w+`=1CR$jaKIU)7dEfSrwiRi}Wi8mZOEj~dch(&? z*)`zFk4HaRtp=jJztqDrgfb_{nYZ{`YvS!&8E150HHPhc$Ndjco{h_$<1-_@y|h=Q zO{($=D^s`MQu?u6*L`#h??Umb`G&A5K9ax@P1t!WKqqb##cQ)GZ4Rt2-dPFgeSON?+>Uy2N`!Db-D$v1Hkeb(AyUjSXDr>Aa^pS2H7M-WJQ3w@%D zvEXJ*P*oT4m;^j$jdnB)f?YZlH`0OV{4IBNK2oYC1tC6PFeEIBBM8fd}7P*YQ+3 z^Yt>AL~hYRmD|6N&K+fS|K8w!J3YYEXZ=%1J~@| z1za%~Hmp!4$~uE`PJi|B_vY1a;bMCgs{uu@S1`sBFZIpLK_J|pKj+s!-tVJjI?OFS zHqdosgUoY608s8Dv$-oe!Z)_`K87|#4EXdRCf~}|n}oCS6Xjha{+)w(-qm@~egX&G zEbx7a?k=Aqm>NyK>~Y7vB%to@b~Z>sttAz|Er>UXwS-(V*YITqoyL4A!dI6!dSdq~ z8{9#z)W1%xlA0$z@{C{! zFVZuBwet+q%ha%yT+?&3LF$%S8dAFA#Nx19>DrgJ@Q6PAP>6PaHvT2#6oS>%^{wFr zd(ceVU#t$E);YXZ^`Muf-J7blVdaAU38N`q5ZqMwsvM-Jr@6`?Un4wncPvhq#z%PJ z&d4{p+2-B?B}NuV1|H0$u1rF?F9Pg&=WSe}5V01!gLW=)v8j@vVNiFKZrz@k=)3C; zFu^g{P#;L|KimeBZL@}=?Y`W^Ad1hs0<~7*w3*QrgJIfKg2fJbU~~%x5igdLmD-q@fZ{ z@&2EHQe>>vs6N$YCH*!&FVmOX^t|4*dCbB@ze?+3k}u9V$|XEKx5nH({9wqb{hXvs zbC8t|mKdWEv6Zv1IJs+PwTo>eF7*O>O50d)Lg?6g5<%C>pjlEB>E)#sfqh-Qh##g! zd&lDi?;~nq=>CRfGajW70+hbY*oe0f!ME1Sz=Auq^8E*$A$XaHK7eBY45{a+2GjWN zY#U-k^QyjvM#kXyh2)>56IFV6gHes0gk!JMz_KOuZYeVYgO{imDqrf2Ci`fBu79iX-R&tb_(M+CZ&37x4!MnhURj7c*F&*iEfH>#I z42m_|Gf3EBVd=4LZpk{6)jR@h8p2%GgpDUjq-_Lw>F=@sJfIyC}lyr)G0pVQWz!soqRKhanSZsy(r$tY`EVeK^=7Nt)2+{r4{ z_vqR=;Ji`3m#dQ-rrSYYy`xaA8f}TjP`|HUJ%b-c!`M}Qv=wBZ^EQz<&eq!6q(D(s zb55H4Y=cEpNR6UL@AsVwr&|W;8QQ;Pu=KcSYJkM?9B+)8a07q`3hpMK1&nuCcwsWF zSCJ!*qx8c=)pgFg|J2l(11K>+o~V|3k~@9tMN`7rIblqevFj?3U&3*hJI{}@uDoNq zwKv|EqmL2H-h>GWTvc{{*ylLuEI04S?*-DNBwbt+6K7s6v(*01k_a=$;UAteLYVa6 z)e`9=V(D6xo!Vcjwb7)(GO_jaF$d`BQ{8f_?wF|88F~H@Wr@mdrgnjKK(0_gGka92H|^S<<@xIi>w1e-*KdjwDxag{GBH9ue=6 zhdg5Jnp%bbco3&kZj;drDar%zp{lF$yo_9SRq2#z5SOLuLduM*w1#uCJT{Y%fRy2M zj_F&fYUa}&=<&kRXH>h-+sM;_A`M%3oIw`1+UGbQv_8g}YKZpjIZsZV_kkyz-nT2| z0jIRtCAtJfSLXtxLZa3;Dn+n7=ToXUj*7!Taj$bkO3Yxl^|fOKH$mi2Em#Tghmm6L zoeG3IEP^}=$w=w?A&qb9qK4ORKM%}CIo`WML|T=uqrYD$3lBpUcpw1aU~$YbF;LFj zXtJNlqu=)y| z=e`5~#>*1?nvCO{Jv7s2Yi!m;d{Dy2`5 zS^u2A_s_37mNCmxHR4XEF6;?MV8}YiC_|}+c*f|%uvjuNMpw}k(F*zGbxx9WcK3(% zl|WB)xn2u(JjNl3tKlNO5m7`|Rc_#y{Jd!BZ*6t8I-s6n^$6S@6+fO~+<`%kqk{~J z^E{q9to@?B7{5Pzrz$8vDe+GU+&ig>kmEl_ojc08HN6=Fj>x>SQcfkkzav zp<%4>R5x05k~Ct~(0(OP0114pF8m2PKTkDu66}tgOB9jbVlmhASf zuAH|JWA4>nfk{`*yd-w4)wZ9In@z3=3y^;DPXG3iRI~K&Hb45HwCd`*cMTo^4J;{Z zkY3oGsEp~!cGFe}c|$U2uK=%C>Xbt2E*HeDB*c*%Tq_qU@%UVataLrOm5B3 zQMxlVZ>_qKfJO8Wv#DYac*3JV%^2#f;V?`lB?XKk#9B&PWE8(f5`L(q5RRTxvUBYa zeQ@(D_-E4FQx~$9RNHP;QLK)rBC~%R-VfI7{Xv>;kba|au%Aa5xz5ckVyim6MXA3@ z?W;2eqN$qLl#j-wkB2#0nQ@$YKCMP+{8`-k;F|_x78h$=gjttz-`e~8s|(<_oha&j zesQ@GXnc=%@2wu%dIOgmJwrE}AK<3nD#ZjRN?_<~nT` zaJ;E>={G2Wlbosy`Y@aJjZ&9DT~8f`r<#pR^fNqeosdjD<~cK0L?D=RRLPubucmuV z)&(nJW>Py8X_gqXwb83YU+MiQGe%eUaeQ5gh z9k&|A2skur(INPLI38txCqyFLEn9UrqrKaYUNlzWH3(+OCq{=t7xQpp()1hc-wUGV zYw7_52DcUy7=bI*rE+O@io>Ced6r1{4D6RWMby$9+cPg_(E@k~>wfilM?xeG_evxvhja%ZUW!(^v$F}%j0~v}*oi!Sv z1Enn|2QXr^=}d*V@X_GbKfuCNw|V%DYwi9~7~EEo2=abGW*dy+yATF>U>Vf1z^-){ zIF~tw2Nr!b_7VvBqQPn;OmWrTIV-EclMb3stQ-qxnGn->&|Q4pPE%I|fLTxbtR~t` z>HNWf#MHiKy|ZPPhM1EeWQXfOw%s8dh+KISGaqn!xBVS^`SU`UDa@B zec3P7$aIa>a*Uk3z;VwI)pMQDNp!&h=)-sZNbO?9^8P$deH|6$zoNHocVFhS^xPFS zc;t!++CvlNgH?+4M1l%sj+cl7JWcS}k5q<#AA_=Fl&2e)n~0w@OUjQuTcIr>;sG;D zOS6xzEl5I%gHF(*tl=`p_$BWry27JYE){r_$BJkk@hY-{0)mrw2GUKadMwMR4iZD? zaH6rYn{<^@>P{dF6xQq0!}30cc?aN9G=vKh!IKYBYR;hO#w>zfY85@#--sp3tqm}R#QiQ;9ZifZu zl~UqM8)1E?5;;#fV|gtfNPh;#OC;Ig%<&4Per&GCE0+e0QeEY)PR}VRs4?S^+Q1XX zP`u652fxh*aq`PB)pd@2QbvDNl5!2*s;q~y~dDCk%3l~@MPBBso;S~d>a=|E`~6Rkag zdNC;jgzBmKZ59)znKAF`OBn|wf#nb7Jxfn3=o%5cpPdA&gsgG|mGH766Bg;3V!~Tpa7J9(b38QJ#j{yhDXF6&H7)o{4lBQ}$^<@CYh^gbq zmKo4q*N&cIi)OjD;%|K(W~F{pB;4!)@SD!mp+=?zrdveXS$?{BiqL$+GW zR)p~YzREB$sz zz;`@=_~nFeYFAyX=vuNujT$|gn88Tsu0?DLn$f7f7Lx7u_Td~#fokHXi|<4oe0EEi z>6oxK9}LkB?@?chvCYh!bI|7ARZobw$)zyAk{ny&2}5t{w{cyOkQ^H|EvM!_+7Ige`O(fc5kQtT=dECM4n&P9Gk%Bg5AGny#ofZ#!*GEqEWH<4Z@iR3WSll$#n&mJ(Sg9A1w-q=)4_E0eNHrq|Ewxwxhfm^2}dR2YSK)A9MZX~>j<^3rWKUf~R$IDj;q>_N7Y zqlW5@V4JdLHJCAek7dyhQtm|Rl$Eq8d52vSCkr>Kv)fltuB7q#lk*!;&)(yAzhn5dcHAz+1&uGc6S0Ejk zlinbija#PJ8!y=e=!aW6M+x|wdF3PPg6R*Ii~Oh>_VVDU2K&yRn-*w(my$k7m?v0q zq5jGS*d{eS;`w7RLET{P^`bYSN50v#4wOszYOH`fQ?ZOnxdpI8aZ`EuMOcH;yX02lIKA`FBsjGrVg%tmBi%$aS2Yq3`IR1!A{{Ujh|wIHTq0bmDVB^@5yT zbvVOp*+xOVHJX6Dg$18pIkst}QYgq%-Dl5;;@DP+ZK6N)?haE^m2|-n*@A?7Iz9~M zb`$DxM^$MLh$Wka&s(*aG!KSJ{5&(;%oE!`-(8=|{e!1_B|>9)UU9Mrp)GP4=k$6^ zDrpH&F3Bq!(A`2Jv;~esgA*S2VXu7>x)+djdUirg1AbPx<^vY~da3t2rNHw2z2_Fz zlQ6p0X^07@Zf03$i29(2sp;D~`0YLBbp#-$<8lV8p(74ppJt73B4jX^5I7rBSEX&c ze9!vYG8FgeVY6ObGe4G)3Bg9N5j>VewQ6in{5ab`UmQL%(?_e1FV(^kVGPB(F2BZo z`&K-!&`GREsxD-KR3>Zf?s}_Q)uuo0l^%aUOER?>xb&qc6Dk+J_qar3%4E++5mNfz z>(r|Uv!e>5FE}EfB2(NapWIh`P>_f5NKot!#k5JxTyXW!@OkR6R{jl=)1XegdV}ue zf73o<*Re;CY4Y!GEsoN>Js)HM`nuZR=RLJD^Vp19B@PqlfE9V!D;!=w;rXTNN%dPr zA)e8mHVcrg7<;TQzoFKK6;1Y3C@6%>kpUhM+Mj*Hy}cgP z7dTpmZa`3^qc~+fEsL!V)1P)T{4?5C{w)!sJ9bKurfsA7N<-h@u;8}CV=OF>p`KEg znX&HloTJTYB3eD94OmM>i+Lsvyt2wI;{O|ngD4lQ@pL79Vrp*J5jU?!2!LpsH2<8q z?;lsY8qyyVyzq4y{8VOdz7O0#q#OmI51skA=3k!;x)1|~xw7t#Tvh(qEMqjY{zQiL znsaN;FNlF$t(1C_L!ZHEcY%jP-_srDuKB~_dJ0G_E%R!5#e%8@J7Mh3N361dWV*86 znVED~Akx<++RXM7w^|yZ=)TgKBb|5`*@rsXRNX7nL?{ys#{k;S7Lmu{sfS%oth^&S zizW8^fK)fP3M*2eZ8DeBRa%27ng>gpwA5fPo-Z#hGj!`?GS7fGP3dJOq0X^!vnF&4 zipMW2Qnu#<M*b~Zt z`7MEFDFn=EJvWtbJAt6DZ1)1O3u#O%hbHO?Y0JaW! z1^Yod(;?@?H4OP>i0|d!{P^)2LCK?eh$+uue3LaHj!e*Cvkcy&yM8;)AX$PDLozk{ z07+nKsi#Vj&^7nDe$w-zp&G-~*{q!+!8Ca4g~q-|zf3z4J>@8t47fZX5LBe@)sTK( zup}WM@2=@bKhwW;@@`jGpXvB1R9%YYUQ#-{L3=FZ$#*bMWfREoo80eO+;sr_(ghIx zHkX^Vj^(r;E_uf5GAQj$RbAq>ZO-d{UQW3$T4c>}_RW8-|E9Y}sv1t3?(1_?A9f6E z#>ZZl#&8lw&D1mengEq>L;=D%>P*`LVO{boXf)?W4P6XJC zf!{b0&CGt-PwZt?)oBs(m zha978o`bbHurVKtz7sr*tt^tH)S!N$4*qvsqC0JS>5ymYNyW+z<@ zPa|S~S=Fg$-v`yTW7jmWJ+f)5um%rurdnSU2xZ4Z;nDtih!GDBkT@M1Ykf07X{vgy z2e)hxfm)TD0;?41J3@h@Z~Daa9!{jh7-J5~(@yz`d9#Mkn> z%J1yfW%$h>ga)2

    - - - {columns.map((c) => ( - - ))} - - - - {rows.length === 0 ? ( - - - - ) : ( - rows.map((row) => { - const rowInteractive = - interactive && (isRowInteractive?.(row) ?? true); - // Only a row that owns the whole interaction takes the button role and the keyboard - // handling that goes with it; see rowsContainControls. - const rowIsControl = rowInteractive && !rowsContainControls; - return ( - onRowClick?.(row) : undefined} - tabIndex={rowIsControl ? 0 : undefined} - role={rowIsControl ? "button" : undefined} - onKeyDown={ - rowIsControl - ? (e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onRowClick?.(row); - } - } - : undefined - } - > - {columns.map((c) => ( - - ))} - - ); - }) - )} - -
    - {c.headerHidden ? ( - {c.header} - ) : ( - c.header - )} -
    - {empty ?? "No data"} -
    - {c.render(row)} -
    -
    - ); -} diff --git a/frontend/editor/src/core/ui/dataTableColumns.tsx b/frontend/editor/src/core/ui/dataTableColumns.tsx new file mode 100644 index 0000000000..df31dc384a --- /dev/null +++ b/frontend/editor/src/core/ui/dataTableColumns.tsx @@ -0,0 +1,569 @@ +import { Fragment, type ReactNode } from "react"; +import { StatusBadge, type StatusTone } from "@app/ui/StatusBadge"; +import { Chip, type ChipAccent } from "@app/ui/Chip"; +import { Button } from "@app/ui/Button"; +import { Dropdown } from "@app/ui/Dropdown"; +import { ProgressBar } from "@app/ui/ProgressBar"; +import { Select, type SelectOption } from "@app/ui/Select"; + +/** + * The column vocabulary for {@link DataTable}. Call-sites pick a cell KIND and + * supply the data + semantics; the component owns 100% of the appearance. There + * is no raw-JSX / className escape hatch by design; a cell can only look the way + * the design system draws its kind, so every table looks and behaves the same. + */ + +type Align = "left" | "right"; +type SortValue = string | number | boolean | null | undefined; + +/** + * Which built-in comparator sorts a column. Set by the builder from the cell's + * data type - `alphanumeric` (case-insensitive, natural: `v2` before `v10`) for + * text, `basic` (raw numeric) for numbers. Call-sites never choose this. + */ +export type DataTableSortFn = "alphanumeric" | "basic"; + +/** Opaque, fully-resolved column. Produced only by the {@link column} builders. */ +export interface DataTableColumn { + key: string; + header: ReactNode; + align: Align; + /** Prevent wrapping (mono/number values). */ + nowrap: boolean; + /** Shrink the column to its content (actions / affordances). */ + fit: boolean; + sortable: boolean; + sortValue?: (row: T) => SortValue; + /** Comparator kind, derived from the cell type. Only set when sortable. */ + sortFn?: DataTableSortFn; + /** Cell renders its own interactive control (button/link/select/chip). Rows + * containing one drop their `role="button"` so a button never nests inside a + * button - the control is the keyboard path instead. */ + interactive?: boolean; + /** Internal, design-system-owned renderer. Call-sites never supply this. */ + renderCell: (row: T) => ReactNode; +} + +/** The only design-system glyph a cell may use (icon-only actions). */ +export type CellGlyph = "kebab"; + +function KebabGlyph() { + return ( + + + + + + ); +} + +/** An item in a kebab action menu. */ +export interface CellMenuItem { + label: string; + tone?: "default" | "danger"; + disabled?: boolean; + onClick: () => void; + /** Draw a divider above this item. */ + dividerBefore?: boolean; +} + +/** A row/group action. A locked button, or a kebab menu when `menu` is set. */ +export interface CellAction { + label: string; + glyph?: CellGlyph; + /** Icon-only (uses `label` as the accessible name). */ + iconOnly?: boolean; + tone?: "default" | "danger"; + onClick?: () => void; + loading?: boolean; + disabled?: boolean; + /** When set, the button opens this menu instead of firing `onClick`. */ + menu?: CellMenuItem[]; +} + +/** Renders a row of locked action buttons / kebab menus. Shared by the + * `actions` cell kind and grouped-table headers. */ +export function renderCellActions(actions: CellAction[]): ReactNode { + return ( +
    e.stopPropagation()}> + {actions.map((a) => + a.menu ? ( + + + + + + {a.menu.map((m) => ( + + {m.dividerBefore && } + + {m.label} + + + ))} + + + ) : ( + + ), + )} +
    + ); +} + +/** An external link inside a cell. */ +export interface CellLink { + label: string; + href: string; + ariaLabel?: string; +} + +interface Common { + key: string; + header: ReactNode; + sortable?: boolean; +} + +function base( + o: Common, + extra: Pick, "align" | "nowrap" | "fit" | "renderCell"> & { + sortValue?: (row: T) => SortValue; + sortFn?: DataTableSortFn; + interactive?: boolean; + }, +): DataTableColumn { + return { + key: o.key, + header: o.header, + align: extra.align, + nowrap: extra.nowrap, + fit: extra.fit, + sortable: !!o.sortable, + sortValue: o.sortable ? extra.sortValue : undefined, + sortFn: o.sortable ? extra.sortFn : undefined, + interactive: extra.interactive, + renderCell: extra.renderCell, + }; +} + +function text( + o: Common & { + get: (row: T) => string; + /** Optional bold label rendered before the value as "Label: value". */ + label?: (row: T) => string | null | undefined; + sortBy?: (row: T) => SortValue; + }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: false, + fit: false, + sortValue: o.sortBy ?? ((r) => o.get(r)), + sortFn: "alphanumeric", + renderCell: (r) => { + const label = o.label?.(r); + return label ? ( + + {label}: {o.get(r)} + + ) : ( + {o.get(r)} + ); + }, + }); +} + +function mono( + o: Common & { get: (row: T) => string; sortBy?: (row: T) => SortValue }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: true, + fit: false, + sortValue: o.sortBy ?? ((r) => o.get(r)), + sortFn: "alphanumeric", + renderCell: (r) => {o.get(r)}, + }); +} + +function muted( + o: Common & { + get: (row: T) => string | null | undefined; + placeholder?: string; + /** Override the sort key (e.g. an ISO date behind a "3 days ago" label). */ + sortBy?: (row: T) => SortValue; + }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: false, + fit: false, + sortValue: o.sortBy ?? ((r) => o.get(r) ?? undefined), + sortFn: "alphanumeric", + renderCell: (r) => ( + + {o.get(r) || (o.placeholder ?? "-")} + + ), + }); +} + +function number( + o: Common & { + get: (row: T) => number | null | undefined; + format?: (n: number, row: T) => string; + placeholder?: string; + /** Override the sort key (e.g. a raw count behind a formatted label). */ + sortBy?: (row: T) => SortValue; + }, +): DataTableColumn { + return base(o, { + align: "right", + nowrap: true, + fit: false, + sortValue: o.sortBy ?? ((r) => o.get(r) ?? undefined), + sortFn: "basic", + renderCell: (r) => { + const n = o.get(r); + if (n == null) { + return ( + + {o.placeholder ?? "-"} + + ); + } + return ( + + {o.format ? o.format(n, r) : String(n)} + + ); + }, + }); +} + +function badge( + o: Common & { + get: (row: T) => { tone: StatusTone; label: string }; + sortBy?: (row: T) => SortValue; + }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: true, + fit: false, + sortValue: o.sortBy ?? ((r) => o.get(r).label), + sortFn: "alphanumeric", + renderCell: (r) => { + const b = o.get(r); + return ( + + {b.label} + + ); + }, + }); +} + +/** + * A user-defined label, rendered as a dot-less pill. Use this ONLY for labels + * that come from data / the user (e.g. a document's classification). Values from + * a fixed set we define (types, environments, providers) are `text`, not pills. + */ +export interface CellLabel { + label: string; + accent?: ChipAccent; +} + +function labels( + o: Common & { get: (row: T) => CellLabel[] }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: false, + fit: false, + sortValue: (r) => o.get(r)[0]?.label ?? undefined, + sortFn: "alphanumeric", + renderCell: (r) => ( +
    + {o.get(r).map((l) => ( + + {l.label} + + ))} +
    + ), + }); +} + +/** + * An interactive capability chip: click to grant, remove to revoke, dashed to + * offer adding. A functional cell (it toggles state), distinct from static + * `labels`. + */ +export interface CellCap { + label: string; + accent?: ChipAccent; + onClick?: () => void; + onRemove?: () => void; + dashed?: boolean; +} + +function caps( + o: Common & { get: (row: T) => CellCap[] }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: false, + fit: false, + interactive: true, + renderCell: (r) => ( +
    + {o.get(r).map((c) => ( + + {c.label} + + ))} +
    + ), + }); +} + +function entity( + o: Common & { + /** Semantic leading icon (component owns its size + colour container). */ + icon?: (row: T) => ReactNode; + primary: (row: T) => string; + /** Muted inline suffix after the name, its own node (e.g. "(you)"). */ + suffix?: (row: T) => string | null | undefined; + /** Secondary muted line under the name. */ + note?: (row: T) => string | null | undefined; + sortBy?: (row: T) => SortValue; + }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: false, + fit: false, + sortValue: o.sortBy ?? ((r) => o.primary(r)), + sortFn: "alphanumeric", + renderCell: (r) => { + const icon = o.icon?.(r); + const suffix = o.suffix?.(r); + const note = o.note?.(r); + return ( +
    + {icon != null && ( + + {icon} + + )} +
    + + {o.primary(r)} + {suffix && ( + {suffix} + )} + + {note && {note}} +
    +
    + ); + }, + }); +} + +function actions(o: { + key: string; + header?: ReactNode; + get: (row: T) => CellAction[]; +}): DataTableColumn { + return { + key: o.key, + header: o.header ?? "", + align: "right", + nowrap: true, + fit: true, + sortable: false, + interactive: true, + renderCell: (r) => renderCellActions(o.get(r)), + }; +} + +function progress( + o: Common & { + get: (row: T) => { value: number; label?: string }; + /** Accessible name for the bar (it has no visible text). Defaults to the + * shown percent; pass a description like "Load for us-east-1" when useful. */ + ariaLabel?: (row: T) => string; + }, +): DataTableColumn { + return base(o, { + align: "left", + nowrap: true, + fit: false, + sortValue: (r) => o.get(r).value, + sortFn: "basic", + renderCell: (r) => { + const p = o.get(r); + const shown = p.label ?? `${Math.round(p.value * 100)}%`; + return ( +
    + + + + {shown} +
    + ); + }, + }); +} + +function links(o: { + key: string; + header?: ReactNode; + get: (row: T) => CellLink[]; +}): DataTableColumn { + return { + key: o.key, + header: o.header ?? "", + align: "right", + nowrap: true, + fit: true, + sortable: false, + interactive: true, + renderCell: (r) => ( +
    + ), + }; +} + +function select(o: { + key: string; + header: ReactNode; + get: (row: T) => { + value?: string | null; + defaultValue?: string; + options: SelectOption[]; + ariaLabel?: string; + disabled?: boolean; + }; + /** Omit for an uncontrolled select (local UI state only). */ + onChange?: (row: T, value: string | null) => void; +}): DataTableColumn { + return { + key: o.key, + header: o.header, + align: "left", + nowrap: true, + fit: false, + sortable: false, + interactive: true, + renderCell: (r) => { + const s = o.get(r); + const change = o.onChange; + return ( +
    + onChangeRole(m, (value ?? m.role) as RoleId)} - /> -
    - )} + const groups = useMemo[]>(() => { + function ownerNames(owners: string[]): string { + return owners.map((u) => nameByUsername.get(u) ?? u).join(", "); + } + // A team whose name/membership is system-managed - no rename/delete. + function isManagedTeam(team: TeamGroup): boolean { + return SYSTEM_TEAMS.has(team.name) || team.isPersonal === true; + } + function teamKebabHasItems(team: TeamGroup): boolean { + return ( + capabilities.manageGrants || + (!isManagedTeam(team) && + (capabilities.renameTeam || capabilities.deleteTeam)) + ); + } + function teamActions(team: TeamGroup): CellAction[] { + const acts: CellAction[] = [ + { + label: t("users.group.addToTeam", "Add to team"), + onClick: () => onAddToTeam(team), + }, + ]; + if (teamKebabHasItems(team)) { + const items: CellMenuItem[] = []; + if (capabilities.manageGrants) { + items.push( + processorTeamIds.has(team.id) + ? { + label: t( + "users.team.revokeProcessor", + "Revoke Processor from team", + ), + onClick: () => onRevokeTeamProcessor(team), + } + : { + label: t( + "users.team.grantProcessor", + "Grant Processor to team", + ), + onClick: () => onGrantTeamProcessor(team), + }, + ); + } + if (!isManagedTeam(team)) { + const divider = capabilities.manageGrants; + if (capabilities.renameTeam) { + items.push({ + label: t("users.action.rename", "Rename team"), + onClick: () => onRenameTeam(team), + dividerBefore: divider, + }); + } + if (capabilities.deleteTeam) { + items.push({ + label: t("users.action.deleteTeam", "Delete team"), + tone: "danger", + onClick: () => onDeleteTeam(team), + dividerBefore: divider && !capabilities.renameTeam, + }); + } + } + acts.push({ + label: t("users.teamActions", "Team actions"), + glyph: "kebab", + iconOnly: true, + menu: items, + }); + } + return acts; + } - {rowKebab(m)} -
    - ); - } - - /** Rows for a group, collapsing past COLLAPSED_LIMIT behind a toggle. */ - function renderMembers(list: Member[], key: string) { - const isOpen = expanded.has(key); - const overflow = list.length > COLLAPSED_LIMIT; - const shown = overflow && !isOpen ? list.slice(0, COLLAPSED_LIMIT) : list; - return ( - <> - {shown.map(renderRow)} - {overflow && ( - - )} - - ); - } + const gs: DataTableGroup[] = []; + if (capabilities.orgGroup && dir.organization.length > 0) { + gs.push({ + key: "org", + title: t("users.group.org", "Organization"), + meta: t("users.group.owners", "{{count}} owner", { + count: dir.organization.length, + }), + rows: dir.organization, + collapseAfter: COLLAPSED_LIMIT, + }); + } + for (const team of dir.teams) { + const led = + team.owners.length > 0 + ? ` · ${t("users.group.ledBy", "led by {{owner}}", { + owner: ownerNames(team.owners), + })}` + : ""; + gs.push({ + key: `team-${team.id}`, + title: t("users.group.team", "{{name}} team", { name: team.name }), + meta: + t("users.group.teamMeta", "{{count}} people", { + count: team.members.length, + }) + led, + actions: teamActions(team), + rows: team.members, + collapseAfter: COLLAPSED_LIMIT, + }); + } + if (showGuests && dir.guests.length > 0) { + gs.push({ + key: "guests", + title: t("users.group.guests", "Guests"), + meta: t("users.group.guestCount", "{{count}} guest", { + count: dir.guests.length, + }), + rows: dir.guests, + collapseAfter: COLLAPSED_LIMIT, + }); + } + return gs; + }, [ + t, + dir, + nameByUsername, + capabilities, + showGuests, + processorTeamIds, + onAddToTeam, + onGrantTeamProcessor, + onRevokeTeamProcessor, + onRenameTeam, + onDeleteTeam, + ]); return ( -
    - {/* Organization (a single-org deployment only; SaaS has no org). */} - {capabilities.orgGroup && dir.organization.length > 0 && ( -
    -
    -
    - {t("users.group.org", "Organization")} - - {t( - "users.group.orgDesc", - "Owners with org-wide authority and policy approval", - )} - -
    - - {t("users.group.owners", "{{count}} owner", { - count: dir.organization.length, - })} - -
    - {renderMembers(dir.organization, "org")} -
    - )} - - {/* Teams */} - {dir.teams.map((team) => ( -
    -
    -
    - - {t("users.group.team", "{{name}} team", { name: team.name })} - - - {t("users.group.teamMeta", "{{count}} people", { - count: team.members.length, - })} - {team.owners.length > 0 && - ` · ${t("users.group.ledBy", "led by {{owner}}", { - owner: ownerNames(team.owners), - })}`} - -
    -
    - - {teamKebabHasItems(team) && ( - - - - - - {capabilities.manageGrants && - (processorTeamIds.has(team.id) ? ( - onRevokeTeamProcessor(team)}> - {t( - "users.team.revokeProcessor", - "Revoke Processor from team", - )} - - ) : ( - onGrantTeamProcessor(team)}> - {t( - "users.team.grantProcessor", - "Grant Processor to team", - )} - - ))} - {!isManagedTeam(team) && - (capabilities.renameTeam || capabilities.deleteTeam) && ( - <> - {capabilities.manageGrants && } - {capabilities.renameTeam && ( - onRenameTeam(team)}> - {t("users.action.rename", "Rename team")} - - )} - {capabilities.deleteTeam && ( - onDeleteTeam(team)} - > - {t("users.action.deleteTeam", "Delete team")} - - )} - - )} - - - )} -
    -
    - {renderMembers(team.members, `team-${team.id}`)} -
    - ))} - - {/* Guests (parked in the live app; shown when showGuests is set). */} - {showGuests && dir.guests.length > 0 && ( -
    -
    -
    - {t("users.group.guests", "Guests")} - - {t( - "users.group.guestsDesc", - "External collaborators, scoped to what you shared. Editor only.", - )} - -
    - - {t("users.group.guestCount", "{{count}} guest", { - count: dir.guests.length, - })} - -
    - {renderMembers(dir.guests, "guests")} -
    - )} -
    + + columns={columns} + groups={groups} + rowKey={(m) => String(m.id)} + collapseLabels={{ + showAll: (count) => t("users.showAll", "Show all {{count}}", { count }), + showLess: t("users.showLess", "Show less"), + }} + /> ); } diff --git a/frontend/editor/src/portal/views/Integrations.test.tsx b/frontend/editor/src/portal/views/Integrations.test.tsx index c5e96d8666..8c4e684bfa 100644 --- a/frontend/editor/src/portal/views/Integrations.test.tsx +++ b/frontend/editor/src/portal/views/Integrations.test.tsx @@ -68,39 +68,35 @@ describe("Integrations view", () => { ).toBeInTheDocument(); }); - it("groups connections of the same type and expands to the instances", async () => { + it("groups connections of the same type, instances shown as rows (no expand)", async () => { fetchIntegrations.mockResolvedValue([ bucket(1, "Claims"), bucket(2, "Archive"), ]); render(); - // One connected group row for S3 with the instance count, not two rows. - const group = await screen.findByText( - "portal.integrations.connectionCount", - ); - expect(group).toBeInTheDocument(); - - fireEvent.click(screen.getByText("portal.connections.types.s3.label")); + // Instances are rows directly under the S3 vendor group - no expand click. expect(await screen.findByText("Claims")).toBeInTheDocument(); expect(screen.getByText("Archive")).toBeInTheDocument(); + // Vendor group header shows the instance count and the "add another" action. expect( - screen.getByText("portal.integrations.addAnother"), + screen.getByText("portal.integrations.connectionCount"), ).toBeInTheDocument(); + // Each connected vendor group offers a Connect action (to add another). + expect( + screen.getAllByText("portal.integrations.connect").length, + ).toBeGreaterThan(0); // The available band remains for the other, unconnected vendors. expect( screen.getByText(/portal\.integrations\.availableHeading/), ).toBeInTheDocument(); }); - it("deletes an instance from the expanded group", async () => { + it("deletes an instance directly from its row", async () => { fetchIntegrations.mockResolvedValueOnce([bucket(5, "Claims")]); fetchIntegrations.mockResolvedValueOnce([]); render(); - fireEvent.click( - await screen.findByText("portal.connections.types.s3.label"), - ); fireEvent.click(await screen.findByText("portal.connections.delete")); await waitFor(() => expect(deleteIntegration).toHaveBeenCalledWith(5)); @@ -115,9 +111,6 @@ describe("Integrations view", () => { ); render(); - fireEvent.click( - await screen.findByText("portal.connections.types.s3.label"), - ); fireEvent.click(await screen.findByText("portal.connections.delete")); expect( diff --git a/frontend/editor/src/portal/views/Integrations.tsx b/frontend/editor/src/portal/views/Integrations.tsx index 993583a364..793c21f880 100644 --- a/frontend/editor/src/portal/views/Integrations.tsx +++ b/frontend/editor/src/portal/views/Integrations.tsx @@ -2,8 +2,15 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import AddRoundedIcon from "@mui/icons-material/AddRounded"; import SearchRoundedIcon from "@mui/icons-material/SearchRounded"; -import ExpandMoreRoundedIcon from "@mui/icons-material/ExpandMoreRounded"; -import { Banner, Button, Skeleton } from "@app/ui"; +import { + Banner, + Button, + column, + DataTable, + type DataTableColumn, + type DataTableGroup, + EmptyState, +} from "@app/ui"; import { errorMessage } from "@portal/api/http"; import { deleteIntegration, @@ -30,12 +37,11 @@ import "@portal/views/Integrations.css"; /** * The integrations catalogue: everything Stirling can talk to, in one place. * - * Three bands in one list. Connected first — stored connections grouped by - * vendor, expandable when a vendor has several (two S3 buckets is normal, not - * an error), each instance editable and one click from "add another". Then - * Available — the supported vendors, each saying what it works with (sources, - * policies, pipelines) so it's obvious whether a vendor feeds documents in or - * receives them. Coming-soon source connectors close the list greyed out, so + * Three bands, one grouped table. Connected first - stored connections grouped + * by vendor (two S3 buckets is normal, not an error), every instance a row you + * can edit or remove, with "add another" on the vendor's group header. Then + * Available - the supported vendors, each saying what it works with (sources, + * policies, pipelines). Coming-soon source connectors close the list so * "do you support X?" is answered honestly instead of hidden. * * Setup itself stays in the shared {@link ConnectionModal}; every entry point @@ -72,6 +78,20 @@ interface TypeGroup { connections: IntegrationConfig[]; } +/** One normalized row across the three bands, so a single grouped table renders + * connected instances, available vendors, and coming-soon vendors alike. */ +type IntegrationRow = { + key: string; + brandId: string; + title: string; + subtitle: string; + worksWith: WorksWith[]; +} & ( + | { kind: "instance"; connection: IntegrationConfig; canManage: boolean } + | { kind: "available"; typeId: string } + | { kind: "soon" } +); + export function Integrations() { const { t } = useTranslation(); const [connections, setConnections] = useState( @@ -82,13 +102,13 @@ export function Integrations() { >(undefined); const [filter, setFilter] = useState("all"); const [query, setQuery] = useState(""); - const [expanded, setExpanded] = useState>(new Set()); const [modal, setModal] = useState<{ open: boolean; editing: IntegrationConfig | null; fixedTypeId?: string; }>({ open: false, editing: null }); const [busy, setBusy] = useState(false); + const [deletingId, setDeletingId] = useState(null); const [error, setError] = useState(null); const refresh = useCallback(async () => { @@ -192,45 +212,152 @@ export function Integrations() { return counts; }, [catalogue]); - function toggleExpand(typeId: string) { - setExpanded((current) => { - const next = new Set(current); - if (next.has(typeId)) next.delete(typeId); - else next.add(typeId); - return next; - }); - } - - function openCreate(typeId: string) { + const openCreate = useCallback((typeId: string) => { setModal({ open: true, editing: null, fixedTypeId: typeId }); - } + }, []); - function openEdit(connection: IntegrationConfig) { + const openEdit = useCallback((connection: IntegrationConfig) => { setModal({ open: true, editing: connection }); - } + }, []); - async function remove(connection: IntegrationConfig) { - if (busy) return; - setBusy(true); - setError(null); - try { - await deleteIntegration(connection.id); - await refresh(); - } catch (e) { - setError(errorMessage(e)); - } finally { - setBusy(false); - } - } + const remove = useCallback( + async (connection: IntegrationConfig) => { + if (busy) return; + setBusy(true); + setDeletingId(connection.id); + setError(null); + try { + await deleteIntegration(connection.id); + await refresh(); + } catch (e) { + setError(errorMessage(e)); + } finally { + setBusy(false); + setDeletingId(null); + } + }, + [busy, refresh], + ); const isLoading = connections === null; - const chip = (kind: WorksWith) => ( - - {t(`portal.integrations.worksWith.${kind}`)} - + const worksWithText = useCallback( + (list: WorksWith[]) => + list.map((w) => t(`portal.integrations.worksWith.${w}`)).join(", "), + [t], ); + const columns = useMemo[]>( + () => [ + column.entity({ + key: "integration", + header: t("portal.integrations.table.integration"), + icon: (r) => , + primary: (r) => r.title, + note: (r) => r.subtitle || undefined, + }), + column.text({ + key: "worksWith", + header: t("portal.integrations.table.worksWith"), + get: (r) => worksWithText(r.worksWith), + }), + column.actions({ + key: "actions", + get: (r) => { + if (r.kind === "instance") { + return r.canManage + ? [ + { + label: t("portal.connections.edit"), + disabled: busy, + onClick: () => openEdit(r.connection), + }, + { + label: t("portal.connections.delete"), + tone: "danger", + loading: busy && deletingId === r.connection.id, + disabled: busy, + onClick: () => void remove(r.connection), + }, + ] + : []; + } + if (r.kind === "available") { + return [ + { + label: t("portal.integrations.connect"), + onClick: () => openCreate(r.typeId), + }, + ]; + } + return []; + }, + }), + ], + [t, busy, deletingId, remove, openEdit, openCreate, worksWithText], + ); + + const tableGroups = useMemo[]>(() => { + const gs: DataTableGroup[] = []; + for (const { type, connections: list } of connectedGroups) { + gs.push({ + key: `connected-${type.id}`, + title: t(type.labelKey), + meta: + list.length > 1 + ? t("portal.integrations.connectionCount", { count: list.length }) + : t("portal.integrations.status.connected"), + actions: [ + { + label: t("portal.integrations.connect"), + onClick: () => openCreate(type.id), + }, + ], + rows: list.map((c) => ({ + kind: "instance" as const, + key: `i-${c.id}`, + brandId: type.id, + title: c.name, + subtitle: connectionDetail(c), + worksWith: worksWith(type), + connection: c, + canManage: !!c.canManage, + })), + }); + } + if (availableTypes.length > 0) { + gs.push({ + key: "available", + title: t("portal.integrations.availableHeading"), + rows: availableTypes.map((type) => ({ + kind: "available" as const, + key: `a-${type.id}`, + brandId: type.id, + title: t(type.labelKey), + subtitle: t(type.descriptionKey), + worksWith: worksWith(type), + typeId: type.id, + })), + }); + } + if (comingSoon.length > 0) { + gs.push({ + key: "soon", + title: t("portal.integrations.comingSoonHeading"), + muted: true, + rows: comingSoon.map((entry) => ({ + kind: "soon" as const, + key: `s-${entry.type}`, + brandId: entry.type, + title: t(entry.labelKey), + subtitle: t(entry.descriptionKey), + worksWith: ["sources"], + })), + }); + } + return gs; + }, [connectedGroups, availableTypes, comingSoon, t, openCreate]); + return (
    @@ -301,187 +428,23 @@ export function Integrations() { {error && } - {isLoading ? ( -
    - {Array.from({ length: 4 }).map((_, i) => ( - - ))} -
    + {!isLoading && tableGroups.length === 0 ? ( + ) : ( -
    -
    - {t("portal.integrations.table.integration")} - {t("portal.integrations.table.worksWith")} - -
    - - {connectedGroups.length > 0 && ( -
    - {t("portal.integrations.connectedHeading")} ·{" "} - {connectedGroups.length} -
    - )} - {connectedGroups.map(({ type, connections: list }) => { - const open = expanded.has(type.id); - return ( -
    - - {open && ( -
    - {list.map((connection) => ( -
    - - {connection.name} - - - {connectionDetail(connection)} - - {connection.canManage && ( - - - - - )} -
    - ))} -
    - -
    -
    - )} -
    - ); - })} - - {availableTypes.length > 0 && ( -
    - {t("portal.integrations.availableHeading")} ·{" "} - {availableTypes.length} -
    - )} - {availableTypes.map((type) => ( -
    - - - - - {t(type.labelKey)} - - - {t(type.descriptionKey)} - - - - - {worksWith(type).map(chip)} - - - - -
    - ))} - - {comingSoon.length > 0 && ( -
    - {t("portal.integrations.comingSoonHeading")} · {comingSoon.length} -
    - )} - {comingSoon.map((entry) => ( -
    - - - - - {t(entry.labelKey)} - - - {t(entry.descriptionKey)} - - - - - {chip("sources")} - - - - {t("portal.sources.builder.comingSoon")} - - -
    - ))} -
    + + columns={columns} + groups={tableGroups} + rowKey={(r) => r.key} + loading={isLoading} + skeletonRows={5} + /> )} DELETE /invitations/{id} -> refetch drops the invite. fireEvent.click( diff --git a/frontend/editor/src/portal/views/Users.tsx b/frontend/editor/src/portal/views/Users.tsx index 535bf3e627..79c6c11611 100644 --- a/frontend/editor/src/portal/views/Users.tsx +++ b/frontend/editor/src/portal/views/Users.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Button, EmptyState, Skeleton } from "@app/ui"; @@ -71,12 +71,14 @@ export function Users() { }, [searchParams, setSearchParams]); // Scroll to and flash the row for ?member= (deep link from the super - // search), once the roster has rendered; then strip the param. + // search), once the roster has rendered; then strip the param. Scoped to the + // roster so a pending-invitation row sharing the id can't match first. + const rosterRef = useRef(null); useEffect(() => { const memberId = searchParams.get("member"); if (memberId === null || usersState.loading) return; - const row = document.querySelector( - `[data-member-id="${CSS.escape(memberId)}"]`, + const row = rosterRef.current?.querySelector( + `[data-row-key="${CSS.escape(memberId)}"]`, ); if (row) { row.scrollIntoView({ block: "center" }); @@ -343,28 +345,30 @@ export function Users() { )} {!loading && members.length > 0 && ( - openInvite(team.id)} - onResetPassword={setResetPwMember} - onMoveToTeam={setMoveMember} - onToggleEnabled={toggleEnabled} - onUnlock={unlock} - onDisableMfa={disableMfa} - onRemove={removeUser} - onRenameTeam={(team) => - setRenameTarget({ id: team.id, name: team.name }) - } - onDeleteTeam={deleteTeamAction} - /> +
    + openInvite(team.id)} + onResetPassword={setResetPwMember} + onMoveToTeam={setMoveMember} + onToggleEnabled={toggleEnabled} + onUnlock={unlock} + onDisableMfa={disableMfa} + onRemove={removeUser} + onRenameTeam={(team) => + setRenameTarget({ id: team.id, name: team.name }) + } + onDeleteTeam={deleteTeamAction} + /> +
    )} =20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.23", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", @@ -4953,6 +4992,32 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/store": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.1.tgz", + "integrity": "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/table-core": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-9.1.2.tgz", + "integrity": "sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.11.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-core": { "version": "3.13.23", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3fd1f8e106..90a0e10b05 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "@supabase/supabase-js": "^2.47.13", "@tailwindcss/postcss": "^4.1.13", "@tanstack/react-query": "^5.101.4", + "@tanstack/react-table": "^9.1.2", "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "2.7.0", From 526bb85e17e5224f264af8d891399675291268f1 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:00:35 +0000 Subject: [PATCH 143/154] Translate the failures debug panel strings (#7500) Follow-up to #7296, addressing a missing translation. --- .../public/locales/en-US/translation.toml | 8 ++++++++ .../components/failures/FileRunEventList.tsx | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 99d68c35de..70d3d03bf4 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7372,6 +7372,14 @@ confirm = "Are you sure?" dismiss = "Dismiss" dismissSkipFile = "Skip this file" +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + [portal.failures.disabled] closed = "This failure is already closed." unavailable = "Not available for this failure." diff --git a/frontend/editor/src/portal/components/failures/FileRunEventList.tsx b/frontend/editor/src/portal/components/failures/FileRunEventList.tsx index 477af32e50..2330546655 100644 --- a/frontend/editor/src/portal/components/failures/FileRunEventList.tsx +++ b/frontend/editor/src/portal/components/failures/FileRunEventList.tsx @@ -69,7 +69,7 @@ export function FileRunEventList() { const debugPanel = !import.meta.env.DEV ? null : (
    {showJson && (
    
    From 89d8ffec5d1266251b7feeb05a3d998a6d5c747f Mon Sep 17 00:00:00 2001
    From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
    Date: Mon, 17 Aug 2026 17:49:13 +0000
    Subject: [PATCH 144/154] Ci/environments cleanups, new envs and master to
     release naming (#7511)
    
    # Description of Changes
    
    Ci/environments cleanups, new envs and master to release naming
    
    ---
    
    ## 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/DeveloperGuide.md)
    (if applicable)
    - [ ] I have read the [How to add new languages to
    Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
    (if applicable)
    - [ ] I have performed a self-review of my own code
    - [ ] My changes generate no new warnings
    
    ### Documentation
    
    - [ ] I have updated relevant docs on [Stirling-PDF's doc
    repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
    (if functionality has heavily changed)
    - [ ] I have read the section [Add New Translation
    Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
    (for new translation tags only)
    
    ### Translations (if applicable)
    
    - [ ] I ran
    [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
    
    ### UI Changes (if applicable)
    
    - [ ] Screenshots or videos demonstrating the UI changes are attached
    (e.g., as comments or direct attachments in the PR)
    
    ### Testing (if applicable)
    
    - [ ] I have run `task check` to verify linters, typechecks, and tests
    pass
    - [ ] I have tested my changes locally. Refer to the [Testing
    Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
    for more details.
    ---
     .github/config/.files.yaml                    |   1 -
     .github/workflows/PR-Demo-cleanup.yml         |   4 -
     .github/workflows/ai_pr_title_review.yml      | 221 ----------------
     .github/workflows/backend-build.yml           |   1 +
     .github/workflows/build-enterprise.yml        |   2 +
     .github/workflows/build.yml                   |   1 +
     .github/workflows/check-licence.yml           |   1 +
     .github/workflows/check-openapi.yml           |   1 +
     .github/workflows/db-migration-test.yml       |   1 +
     .github/workflows/deploy-on-v2-commit.yml     | 209 ----------------
     .github/workflows/docker-compose-tests.yml    |   1 +
     .github/workflows/e2e-live.yml                |   1 +
     .../frontend-backend-licenses-update.yml      |   4 +
     .github/workflows/multiOSReleases.yml         |  63 ++---
     .github/workflows/nightly.yml                 |   1 +
     .github/workflows/push-docker-base.yml        |   3 +
     .github/workflows/push-docker.yml             |  29 ++-
     .github/workflows/swagger.yml                 |   5 +-
     .github/workflows/tauri-build.yml             |  42 +---
     .github/workflows/test-build-docker.yml       |   1 +
     .github/workflows/testdriver.yml              | 235 ------------------
     WINDOWS_SIGNING.md                            |  71 +++---
     .../editor/src/core/services/updateService.ts |   2 +-
     23 files changed, 95 insertions(+), 805 deletions(-)
     delete mode 100644 .github/workflows/ai_pr_title_review.yml
     delete mode 100644 .github/workflows/deploy-on-v2-commit.yml
     delete mode 100644 .github/workflows/testdriver.yml
    
    diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml
    index 70a964b020..b5cc0527b0 100644
    --- a/.github/config/.files.yaml
    +++ b/.github/config/.files.yaml
    @@ -68,7 +68,6 @@ project: &project
     frontend: &frontend
       - *ci
       - frontend/**
    -  - .github/workflows/testdriver.yml
       - testing/**
       - docker/**
       - scripts/translations/*.py
    diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml
    index e0032955e3..1407939994 100644
    --- a/.github/workflows/PR-Demo-cleanup.yml
    +++ b/.github/workflows/PR-Demo-cleanup.yml
    @@ -7,10 +7,6 @@ on:
     permissions:
       contents: read
     
    -env:
    -  SERVER_IP: ${{ secrets.NEW_VPS_IP }} # Add this to your GitHub secrets
    -  CLEANUP_PERFORMED: "false" # Add flag to track if cleanup occurred
    -
     jobs:
       cleanup:
         environment: pr-preview
    diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml
    deleted file mode 100644
    index b9b391af0e..0000000000
    --- a/.github/workflows/ai_pr_title_review.yml
    +++ /dev/null
    @@ -1,221 +0,0 @@
    -name: AI - PR Title Review
    -
    -on:
    -  pull_request:
    -    types: [opened, edited]
    -    branches: [main]
    -
    -permissions: # required for secure-repo hardening
    -  contents: read
    -
    -jobs:
    -  ai-title-review:
    -    # GITHUB_TOKEN obeys this block, so it must cover every API call made below.
    -    permissions:
    -      contents: read # actions/checkout, git fetch/diff
    -      issues: write # issues.listComments / createComment / updateComment on the PR
    -      pull-requests: write # same endpoints when the target is a pull request
    -      models: read # actions/ai-inference
    -
    -    runs-on: ubuntu-latest
    -
    -    steps:
    -      - name: Harden Runner
    -        uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
    -        with:
    -          egress-policy: audit
    -
    -      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    -        with:
    -          fetch-depth: 0
    -
    -      - name: Configure Git to suppress detached HEAD warning
    -        run: git config --global advice.detachedHead false
    -
    -      - name: Check if actor is repo developer
    -        id: actor
    -        run: |
    -          if [[ "${{ github.actor }}" == *"[bot]" ]]; then
    -            echo "PR opened by a bot – skipping AI title review."
    -            echo "is_repo_dev=false" >> $GITHUB_OUTPUT
    -            exit 0
    -          fi
    -          if [ ! -f .github/config/repo_devs.json ]; then
    -            echo "Error: .github/config/repo_devs.json not found" >&2
    -            exit 1
    -          fi
    -          # Validate JSON and extract repo_devs
    -          REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; })
    -          # Convert developer list into Bash array
    -          mapfile -t DEVS_ARRAY <<< "$REPO_DEVS"
    -          if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then
    -            echo "is_repo_dev=true" >> $GITHUB_OUTPUT
    -          else
    -            echo "is_repo_dev=false" >> $GITHUB_OUTPUT
    -          fi
    -
    -      - name: Get PR diff
    -        if: steps.actor.outputs.is_repo_dev == 'true'
    -        id: get_diff
    -        run: |
    -          git fetch origin ${{ github.base_ref }}
    -          git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff
    -          echo "diff<> $GITHUB_OUTPUT
    -          cat pr.diff >> $GITHUB_OUTPUT
    -          echo "EOF" >> $GITHUB_OUTPUT
    -
    -      - name: Check and sanitize PR title
    -        if: steps.actor.outputs.is_repo_dev == 'true'
    -        id: sanitize_pr_title
    -        env:
    -          PR_TITLE_RAW: ${{ github.event.pull_request.title }}
    -        run: |
    -          # Sanitize PR title: max 72 characters, only printable characters
    -          PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g')
    -          if [[ ${#PR_TITLE} -lt 5 ]]; then
    -            echo "PR title is too short. Must be at least 5 characters." >&2
    -          fi
    -          echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
    -
    -      - name: AI PR Title Analysis
    -        if: steps.actor.outputs.is_repo_dev == 'true'
    -        id: ai-title-analysis
    -        uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
    -        with:
    -          model: openai/gpt-4o
    -          system-prompt-file: ".github/config/system-prompt.txt"
    -          prompt: |
    -            Based on the following input data:
    -
    -            {
    -              "diff": "${{ steps.get_diff.outputs.diff }}",
    -              "pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}"
    -            }
    -
    -            Respond ONLY with valid JSON in the format:
    -            {
    -              "improved_rating": <0-10>,
    -              "improved_ai_title_rating": <0-10>,
    -              "improved_title": ""
    -            }
    -
    -      - name: Validate and set SCRIPT_OUTPUT
    -        if: steps.actor.outputs.is_repo_dev == 'true'
    -        run: |
    -          cat < ai_response.json
    -          ${{ steps.ai-title-analysis.outputs.response }}
    -          EOF
    -
    -          # Validate JSON structure
    -          jq -e '
    -            (keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and
    -            (.improved_rating | type == "number" and . >= 0 and . <= 10) and
    -            (.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and
    -            (.improved_title | type == "string")
    -          ' ai_response.json
    -          if [ $? -ne 0 ]; then
    -            echo "Invalid AI response format" >&2
    -            cat ai_response.json >&2
    -            exit 1
    -          fi
    -          # Parse JSON fields
    -          IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json)
    -          IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json)
    -          # Limit comment length to 1000 characters
    -          COMMENT=$(cat < /tmp/ai-title-comment.md
    -          # Log input and output to the GitHub Step Summary
    -          echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY
    -          echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY
    -          echo '```bash' >> $GITHUB_STEP_SUMMARY
    -          echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY
    -          echo '```' >> $GITHUB_STEP_SUMMARY
    -          echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY
    -          echo '```json' >> $GITHUB_STEP_SUMMARY
    -          cat ai_response.json >> $GITHUB_STEP_SUMMARY
    -          echo '```' >> $GITHUB_STEP_SUMMARY
    -
    -      - name: Post comment on PR if needed
    -        if: steps.actor.outputs.is_repo_dev == 'true'
    -        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
    -        continue-on-error: true
    -        with:
    -          github-token: ${{ github.token }}
    -          script: |
    -            const fs = require('fs');
    -            const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8');
    -            const { GITHUB_REPOSITORY } = process.env;
    -            const [owner, repo] = GITHUB_REPOSITORY.split('/');
    -            const issue_number = context.issue.number;
    -
    -            const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/);
    -            const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null;
    -
    -            const expectedActor = "github-actions[bot]";
    -            const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
    -
    -            const existing = comments.data.find(c =>
    -              c.user?.login === expectedActor &&
    -              c.body.includes("## 🤖 AI PR Title Suggestion")
    -            );
    -
    -            if (rating === null) {
    -              console.log("No rating found in AI response – skipping.");
    -              return;
    -            }
    -
    -            if (rating <= 5) {
    -              if (existing) {
    -                await github.rest.issues.updateComment({
    -                  owner, repo,
    -                  comment_id: existing.id,
    -                  body
    -                });
    -                console.log("Updated existing suggestion comment.");
    -              } else {
    -                await github.rest.issues.createComment({
    -                  owner, repo, issue_number,
    -                  body
    -                });
    -                console.log("Created new suggestion comment.");
    -              }
    -            } else {
    -              const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`;
    -
    -              if (existing) {
    -                await github.rest.issues.updateComment({
    -                  owner, repo,
    -                  comment_id: existing.id,
    -                  body: praise
    -                });
    -                console.log("Replaced suggestion with praise.");
    -              } else {
    -                console.log("Rating > 5 and no existing comment – skipping comment.");
    -              }
    -            }
    -
    -      - name: is not repo dev
    -        if: steps.actor.outputs.is_repo_dev != 'true'
    -        run: |
    -          exit 0 # Skip the AI title review for non-repo developers
    -
    -      - name: Clean up
    -        if: always()
    -        run: |
    -          rm -f pr.diff ai_response.json /tmp/ai-title-comment.md
    -          echo "Cleaned up temporary files."
    -        continue-on-error: true # Ensure cleanup runs even if previous steps fail
    diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml
    index 9561833f07..54bd4cb907 100644
    --- a/.github/workflows/backend-build.yml
    +++ b/.github/workflows/backend-build.yml
    @@ -20,6 +20,7 @@ permissions:
     
     jobs:
       build:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         strategy:
           fail-fast: false
    diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml
    index 08849c683b..0604f7176f 100644
    --- a/.github/workflows/build-enterprise.yml
    +++ b/.github/workflows/build-enterprise.yml
    @@ -37,6 +37,7 @@ jobs:
         uses: ./.github/workflows/_runner-pick.yml
     
       playwright-e2e-enterprise:
    +    environment: ci-unsigned
         needs: pick
         # Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE,
         # so the suite can't boot premium and would fail. See the header comment.
    @@ -309,6 +310,7 @@ jobs:
       # Multi-node regression: builds + seeds the clustered stack (testing/compose/docker-compose-multinode.yml)
       # and runs behave features/multinode. Licence-gated, so it runs after the Playwright job (not in parallel).
       multinode-e2e:
    +    environment: ci-unsigned
         needs: [pick, playwright-e2e-enterprise]
         # Nightly cron + manual dispatch only (heavy build), fork-gated for the licence secret.
         if: >-
    diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
    index eee07d599a..2f50249099 100644
    --- a/.github/workflows/build.yml
    +++ b/.github/workflows/build.yml
    @@ -61,6 +61,7 @@ jobs:
               filters: .github/config/.files.yaml
     
       gradle-cache-prime:
    +    environment: ci-unsigned
         name: Prime shared Gradle cache
         needs: [files-changed]
         runs-on: ubuntu-latest
    diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml
    index d45a68e860..2eec970b8f 100644
    --- a/.github/workflows/check-licence.yml
    +++ b/.github/workflows/check-licence.yml
    @@ -10,6 +10,7 @@ permissions:
     
     jobs:
       check-licence:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         steps:
           - name: Harden Runner
    diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml
    index 751a34a33e..f224ce18cf 100644
    --- a/.github/workflows/check-openapi.yml
    +++ b/.github/workflows/check-openapi.yml
    @@ -11,6 +11,7 @@ permissions:
     
     jobs:
       check-generate-openapi-docs:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         steps:
           - name: Harden Runner
    diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml
    index 4181390285..d6a61b45c4 100644
    --- a/.github/workflows/db-migration-test.yml
    +++ b/.github/workflows/db-migration-test.yml
    @@ -13,6 +13,7 @@ permissions:
     
     jobs:
       migration-test:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         timeout-minutes: 30
         steps:
    diff --git a/.github/workflows/deploy-on-v2-commit.yml b/.github/workflows/deploy-on-v2-commit.yml
    deleted file mode 100644
    index 01114c64b2..0000000000
    --- a/.github/workflows/deploy-on-v2-commit.yml
    +++ /dev/null
    @@ -1,209 +0,0 @@
    -name: Auto V2 Deploy on Push
    -
    -on:
    -  push:
    -    branches:
    -      - V2
    -      - deploy-on-v2-commit
    -
    -permissions:
    -  contents: read
    -
    -jobs:
    -  deploy-v2-on-push:
    -    environment: pr-preview
    -    runs-on: ubuntu-latest
    -    permissions:
    -      contents: read
    -      packages: write
    -    concurrency:
    -      group: deploy-v2-push-V2
    -      cancel-in-progress: true
    -
    -    steps:
    -      - name: Harden Runner
    -        uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
    -        with:
    -          egress-policy: audit
    -
    -      - name: Checkout code
    -        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    -
    -      - name: Set up Docker Buildx
    -        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
    -
    -      - name: Get commit hashes for frontend and backend
    -        id: commit-hashes
    -        run: |
    -          # Get last commit that touched the frontend folder, docker/frontend, or docker/compose
    -          FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
    -          if [ -z "$FRONTEND_HASH" ]; then
    -            FRONTEND_HASH="no-frontend-changes"
    -          fi
    -
    -          # Get last commit that touched backend code, docker/backend, or docker/compose
    -          BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
    -          if [ -z "$BACKEND_HASH" ]; then
    -            BACKEND_HASH="no-backend-changes"
    -          fi
    -
    -          echo "Frontend hash: $FRONTEND_HASH"
    -          echo "Backend hash: $BACKEND_HASH"
    -
    -          echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
    -          echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
    -
    -          # Short hashes for tags
    -          if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
    -            echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
    -          else
    -            echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
    -          fi
    -
    -          if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
    -            echo "backend_short=no-backend" >> $GITHUB_OUTPUT
    -          else
    -            echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
    -          fi
    -
    -      - name: Convert repository owner to lowercase
    -        id: repoowner
    -        run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
    -
    -      - name: Login to GitHub Container Registry
    -        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
    -        with:
    -          registry: ghcr.io
    -          username: ${{ github.actor }}
    -          password: ${{ github.token }}
    -
    -      - name: Check if frontend image exists
    -        id: check-frontend
    -        run: |
    -          if docker manifest inspect ${IMAGE_BASE}:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
    -            echo "exists=true" >> $GITHUB_OUTPUT
    -            echo "Frontend image already exists, skipping build"
    -          else
    -            echo "exists=false" >> $GITHUB_OUTPUT
    -            echo "Frontend image needs to be built"
    -          fi
    -
    -        env:
    -          IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
    -      - name: Check if backend image exists
    -        id: check-backend
    -        run: |
    -          if docker manifest inspect ${IMAGE_BASE}:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
    -            echo "exists=true" >> $GITHUB_OUTPUT
    -            echo "Backend image already exists, skipping build"
    -          else
    -            echo "exists=false" >> $GITHUB_OUTPUT
    -            echo "Backend image needs to be built"
    -          fi
    -
    -        env:
    -          IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
    -
    -      - name: Build and push frontend image
    -        if: steps.check-frontend.outputs.exists == 'false'
    -        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
    -        with:
    -          context: .
    -          file: ./docker/frontend/Dockerfile
    -          push: true
    -          cache-from: type=gha,scope=stirling-v2-frontend
    -          cache-to: type=gha,mode=max,scope=stirling-v2-frontend
    -          tags: |
    -            ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
    -            ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-frontend-latest
    -          build-args: VERSION_TAG=v2-alpha
    -          platforms: linux/amd64
    -
    -      - name: Build and push backend image
    -        if: steps.check-backend.outputs.exists == 'false'
    -        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
    -        with:
    -          context: .
    -          file: ./docker/backend/Dockerfile
    -          push: true
    -          cache-from: type=gha,scope=stirling-v2-backend
    -          cache-to: type=gha,mode=max,scope=stirling-v2-backend
    -          tags: |
    -            ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
    -            ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-backend-latest
    -          build-args: VERSION_TAG=v2-alpha
    -          platforms: linux/amd64
    -
    -      - name: Set up SSH
    -        run: |
    -          mkdir -p ~/.ssh/
    -          echo "${NEW_VPS_SSH_KEY}" > ../private.key
    -          chmod 600 ../private.key
    -
    -        env:
    -          NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
    -      - name: Deploy to VPS on port 3000
    -        run: |
    -          export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
    -
    -          cat > $UNIQUE_NAME << EOF
    -          version: '3.3'
    -          services:
    -            backend:
    -              container_name: stirling-v2-backend
    -              image: ${IMAGE_BASE}:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
    -              ports:
    -                - "13000:8080"
    -              volumes:
    -                - /stirling/V2/data:/usr/share/tessdata:rw
    -                - /stirling/V2/config:/configs:rw
    -                - /stirling/V2/logs:/logs:rw
    -              environment:
    -                DISABLE_ADDITIONAL_FEATURES: "true"
    -                SECURITY_ENABLELOGIN: "false"
    -                SYSTEM_DEFAULTLOCALE: en-US
    -                UI_APPNAME: "Stirling-PDF V2"
    -                UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
    -                UI_APPNAMENAVBAR: "V2 Deployment"
    -                SYSTEM_MAXFILESIZE: "100"
    -                METRICS_ENABLED: "true"
    -                SYSTEM_GOOGLEVISIBILITY: "false"
    -                SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
    -                baseUrl: "https://demo.stirlingpdf.cloud"
    -              restart: on-failure:5
    -
    -            frontend:
    -              container_name: stirling-v2-frontend
    -              image: ${IMAGE_BASE}:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
    -              ports:
    -                - "3000:80"
    -              environment:
    -                VITE_API_BASE_URL: "http://${NEW_VPS_HOST}:13000"
    -              depends_on:
    -                - backend
    -              restart: on-failure:5
    -          EOF
    -
    -          # Copy to remote with unique name
    -          scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/$UNIQUE_NAME
    -
    -          # SSH and rename/move atomically to avoid interference
    -          ssh -i ../private.key -o StrictHostKeyChecking=no ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH
    -            mkdir -p /stirling/V2/{data,config,logs}
    -            mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
    -            cd /stirling/V2
    -            docker-compose down || true
    -            docker-compose pull
    -            docker-compose up -d
    -            docker system prune -af --volumes || true
    -            docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
    -          ENDSSH
    -
    -        env:
    -          IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
    -          NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
    -          NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
    -      - name: Cleanup temporary files
    -        if: always()
    -        run: |
    -          rm -f ../private.key
    diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml
    index ddb35b4e1a..9d5911404f 100644
    --- a/.github/workflows/docker-compose-tests.yml
    +++ b/.github/workflows/docker-compose-tests.yml
    @@ -17,6 +17,7 @@ permissions:
     
     jobs:
       docker-compose-tests:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         permissions:
           actions: write
    diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml
    index 4aa2a78c89..7bc95df05e 100644
    --- a/.github/workflows/e2e-live.yml
    +++ b/.github/workflows/e2e-live.yml
    @@ -11,6 +11,7 @@ permissions:
     
     jobs:
       playwright-e2e-live:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         timeout-minutes: 30
         steps:
    diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml
    index 1155f01e39..458766660f 100644
    --- a/.github/workflows/frontend-backend-licenses-update.yml
    +++ b/.github/workflows/frontend-backend-licenses-update.yml
    @@ -42,6 +42,8 @@ jobs:
               filters: .github/config/.files.yaml
     
       generate-frontend-license-report:
    +    # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only.
    +    environment: ci-bot
         if: needs.files-changed.outputs.licenses-frontend == 'true'
         name: Generate Frontend License Report
         needs: files-changed
    @@ -316,6 +318,8 @@ jobs:
               GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
     
       generate-backend-license-report:
    +    # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only.
    +    environment: ci-bot
         if: needs.files-changed.outputs.licenses-backend == 'true'
         needs: files-changed
         name: Generate Backend License Report
    diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml
    index 3bb014a82d..d9477f722f 100644
    --- a/.github/workflows/multiOSReleases.yml
    +++ b/.github/workflows/multiOSReleases.yml
    @@ -38,6 +38,7 @@ permissions:
     
     jobs:
       determine-matrix:
    +    environment: ci-unsigned
         if: ${{ vars.CI_PROFILE != 'lite' }}
         runs-on: ubuntu-latest
         outputs:
    @@ -118,6 +119,7 @@ jobs:
             env:
               INPUT_PLATFORM: ${{ github.event.inputs.platform }}
       build-jars:
    +    environment: ci-unsigned
         needs: determine-matrix
         runs-on: ubuntu-latest
         strategy:
    @@ -204,7 +206,6 @@ jobs:
         runs-on: ${{ matrix.platform }}
         env:
           SM_API_KEY: ${{ secrets.SM_API_KEY }}
    -      WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
           RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
         steps:
           - name: Harden Runner
    @@ -295,7 +296,7 @@ jobs:
           # DigiCert KeyLocker Setup (Cloud HSM)
           - name: Setup DigiCert KeyLocker
             id: digicert-setup
    -        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
    +        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
             uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
             env:
               SM_API_KEY: ${{ secrets.SM_API_KEY }}
    @@ -305,7 +306,7 @@ jobs:
               SM_HOST: ${{ secrets.SM_HOST }}
     
           - name: Setup DigiCert KeyLocker Certificate
    -        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
    +        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
             shell: pwsh
             run: |
               Write-Host "Setting up DigiCert KeyLocker environment..."
    @@ -344,40 +345,8 @@ jobs:
               SM_API_KEY: ${{ secrets.SM_API_KEY }}
               SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
               SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
    -      # Traditional PFX Certificate Import (fallback if KeyLocker not configured)
    -      - name: Import Windows Code Signing Certificate
    -        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
    -        env:
    -          WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
    -          WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
    -        shell: powershell
    -        run: |
    -          if ($env:WINDOWS_CERTIFICATE) {
    -            Write-Host "Importing Windows Code Signing Certificate..."
    -
    -            # Decode base64 certificate and save to file
    -            $certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
    -            $certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
    -            [IO.File]::WriteAllBytes($certPath, $certBytes)
    -
    -            # Import certificate to CurrentUser\My store
    -            $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
    -
    -            # Extract and set thumbprint as environment variable
    -            $thumbprint = $cert.Thumbprint
    -            Write-Host "Certificate imported with thumbprint: $thumbprint"
    -            echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
    -
    -            # Clean up certificate file
    -            Remove-Item $certPath
    -
    -            Write-Host "Windows certificate import completed."
    -          } else {
    -            Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
    -          }
    -
           - name: Import Apple Developer Certificate
    -        if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
    +        if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
             env:
               APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
               APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
    @@ -398,7 +367,7 @@ jobs:
               rm certificate.p12
     
           - name: Verify Certificate
    -        if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
    +        if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
             run: |
               echo "Verifying Apple Developer Certificate..."
               KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
    @@ -414,7 +383,7 @@ jobs:
           # Without this, signCommand failures are opaque (Tauri captures but drops
           # smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
           - name: Preflight smctl
    -        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
    +        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
             shell: pwsh
             env:
               KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
    @@ -445,7 +414,7 @@ jobs:
           # smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
           # from env (set by prior DigiCert setup step). No --config-file needed.
           - name: Configure Windows code signing
    -        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
    +        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
             shell: bash
             env:
               KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
    @@ -466,7 +435,7 @@ jobs:
               sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/editor/src-tauri/tauri.windows.conf.json
     
           - name: Import release GPG signing key (Linux)
    -        if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
    +        if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
             run: |
               echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
               gpg --list-secret-keys --keyid-format=long
    @@ -498,8 +467,8 @@ jobs:
               #   APPIMAGETOOL_SIGN_PASSPHRASE  appimagetool uses this to unlock the GPG key non-interactively
               #   SIGN_KEY                      appimagetool picks the key matching this fingerprint
               # Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present.
    -          # Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or V2-master, when secret is present.
    -          SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
    +          # Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or the release branch, when secret is present.
    +          SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }}
               APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
               SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
               TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
    @@ -525,7 +494,7 @@ jobs:
             env:
               TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
               TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
    -          GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
    +          GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }}
               SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
               APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
             run: |
    @@ -564,7 +533,7 @@ jobs:
               echo "Stripped bundled libwayland from $(basename "$AI")"
     
           - name: Clear release GPG key from runner keyring (Linux)
    -        if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
    +        if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')
             env:
               RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
             run: |
    @@ -579,7 +548,7 @@ jobs:
           # artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
           # cargo output unsigned, so checking it produces false negatives.
           - name: Verify Windows Code Signature
    -        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
    +        if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }}
             timeout-minutes: 15
             shell: pwsh
             run: |
    @@ -911,11 +880,11 @@ jobs:
           # workflow_dispatch path requires platform=='all' so a single-platform
           # dispatch can't overwrite an existing release's full latest.json with a
           # partial one (action-gh-release defaults overwrite_files:true).
    -      # release / V2-master always build the full matrix so no extra guard needed.
    +      # release event / release branch always build the full matrix so no extra guard needed.
           # fail_on_unmatched_files makes a missing latest.json or installer fail loudly
           # instead of silently shipping a broken auto-update.
           - name: Upload binaries to Release
    -        if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master'
    +        if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/release'
             uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
             with:
               tag_name: v${{ needs.determine-matrix.outputs.version }}
    diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
    index 70e99d6074..c92d17f027 100644
    --- a/.github/workflows/nightly.yml
    +++ b/.github/workflows/nightly.yml
    @@ -127,6 +127,7 @@ jobs:
       # Runs the @nightly tag (conversion scenarios) plus a 10-shard concurrency run
       # of every other feature.
       cucumber-nightly:
    +    environment: ci-unsigned
         name: Cucumber (nightly scenarios + full concurrency)
         runs-on: ubuntu-latest
         # Fork pull requests get no MAVEN_* secrets, so the image build cannot work.
    diff --git a/.github/workflows/push-docker-base.yml b/.github/workflows/push-docker-base.yml
    index 658583ea08..97c227f23c 100644
    --- a/.github/workflows/push-docker-base.yml
    +++ b/.github/workflows/push-docker-base.yml
    @@ -17,6 +17,9 @@ permissions:
     
     jobs:
       push-base:
    +    # Own environment: docker-publish is branch-locked to release/main,
    +    # which excludes the baseDockerImage/accessIssueFix branches this runs on.
    +    environment: docker-base-publish
         if: ${{ vars.CI_PROFILE != 'lite' && github.actor == 'Frooodle' }}
         runs-on: ubuntu-24.04-8core
         permissions:
    diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml
    index b88d69c3c2..ec9d14822c 100644
    --- a/.github/workflows/push-docker.yml
    +++ b/.github/workflows/push-docker.yml
    @@ -20,9 +20,8 @@ on:
             default: false
       push:
         branches:
    -      - master
    +      - release
           - main
    -      - V2-master
     
     # cancel in-progress jobs if a new job is triggered
     # This is useful to avoid running multiple builds for the same branch if a new commit is pushed
    @@ -91,13 +90,13 @@ jobs:
               MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
     
           - name: Install cosign
    -        if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
    +        if: github.ref == 'refs/heads/release'
             uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
             with:
               cosign-release: "v2.4.1"
     
           - name: Install cosign
    -        if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
    +        if: github.ref == 'refs/heads/release'
             uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
             with:
               cosign-release: "v2.4.1"
    @@ -133,8 +132,8 @@ jobs:
                 ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
                 ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
               tags: |
    -            type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
    -            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
    +            type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/release' }}
    +            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/release' }}
     
           - name: Build and push Unified Dockerfile (latest variant)
             id: build-push-latest
    @@ -158,7 +157,7 @@ jobs:
               sbom: true
     
           - name: Sign regular images
    -        if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-latest.outputs.digest != ''
    +        if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-latest.outputs.digest != ''
             env:
               DIGEST: ${{ steps.build-push-latest.outputs.digest }}
               TAGS: ${{ steps.meta.outputs.tags }}
    @@ -182,8 +181,8 @@ jobs:
                 ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
                 ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
               tags: |
    -            type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
    -            type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
    +            type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/release' }}
    +            type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/release' }}
     
           - name: Build and push Unified Dockerfile (fat variant)
             id: build-push-fat
    @@ -204,7 +203,7 @@ jobs:
               sbom: true
     
           - name: Sign fat images
    -        if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-fat.outputs.digest != ''
    +        if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-fat.outputs.digest != ''
             env:
               DIGEST: ${{ steps.build-push-fat.outputs.digest }}
               TAGS: ${{ steps.meta-fat.outputs.tags }}
    @@ -226,8 +225,8 @@ jobs:
                 ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
                 ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
               tags: |
    -            type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
    -            type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
    +            type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }}
    +            type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }}
     
           - name: Build and push Unified Dockerfile (ultra-lite variant)
             id: build-push-lite
    @@ -248,7 +247,7 @@ jobs:
               sbom: true
     
           - name: Sign ultra-lite images
    -        if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-lite.outputs.digest != ''
    +        if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-lite.outputs.digest != ''
             env:
               DIGEST: ${{ steps.build-push-lite.outputs.digest }}
               TAGS: ${{ steps.meta-lite.outputs.tags }}
    @@ -260,7 +259,7 @@ jobs:
               done
     
           # Standalone unoserver image — versioned independently via
    -      # docker/unoserver/VERSION. master/V2-master: publish +latest
    +      # docker/unoserver/VERSION. release: publish +latest
           # only when the version is new. main/testMain: republish :alpha only
           # when the source hash differs from the published image's annotation.
           - name: Read unoserver image version
    @@ -319,7 +318,7 @@ jobs:
               fi
     
               case "$EFFECTIVE_REF" in
    -            refs/heads/master|refs/heads/V2-master)
    +            refs/heads/release)
                   if [ "${FORCE_REBUILD}" = "true" ]; then
                     echo "force_unoserver_rebuild=true — building stable regardless"
                     mode="stable"
    diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml
    index 38c1985a83..115de87d4e 100644
    --- a/.github/workflows/swagger.yml
    +++ b/.github/workflows/swagger.yml
    @@ -4,7 +4,7 @@ on:
       workflow_dispatch:
       push:
         branches:
    -      - master
    +      - release
     
     # cancel in-progress jobs if a new job is triggered
     # This is useful to avoid running multiple builds for the same branch if a new commit is pushed
    @@ -23,6 +23,9 @@ permissions:
     
     jobs:
       push:
    +    # package-publish holds SWAGGERHUB_API_KEY. It requires reviewer approval and
    +    # is limited to main / release / v* tags, so every push to release waits on one.
    +    environment: package-publish
         if: ${{ vars.CI_PROFILE != 'lite' }}
         runs-on: ubuntu-latest
         steps:
    diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml
    index ab6d8aca71..ddf1104bac 100644
    --- a/.github/workflows/tauri-build.yml
    +++ b/.github/workflows/tauri-build.yml
    @@ -57,6 +57,9 @@ permissions:
     
     jobs:
       determine-matrix:
    +    # Only probes APPLE_CERTIFICATE for presence, so it stays on the unrestricted
    +    # signing environment - release-signing would block every PR run.
    +    environment: ci-signing
         if: ${{ vars.CI_PROFILE != 'lite' }}
         runs-on: ubuntu-latest
         outputs:
    @@ -103,6 +106,12 @@ jobs:
               echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
     
       build:
    +    # Windows/GPG signing only runs on main (see the per-step gates below), so only
    +    # that path needs the reviewer-gated release-signing environment. Everything else
    +    # (PRs, merge queue, nightly) signs macOS only and uses ci-signing, which has no
    +    # approval or branch restriction.
    +    environment:
    +      name: ${{ (inputs.sign && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) && 'release-signing' || 'ci-signing' }}
         needs: determine-matrix
         strategy:
           fail-fast: false
    @@ -110,7 +119,6 @@ jobs:
         runs-on: ${{ matrix.platform }}
         env:
           SM_API_KEY: ${{ secrets.SM_API_KEY }}
    -      WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
           APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
           RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
           # Per-platform sign gate. macOS signs on any run with the cert available,
    @@ -264,38 +272,6 @@ jobs:
                 }
               }
     
    -      # Traditional PFX Certificate Import (fallback if KeyLocker not configured)
    -      - name: Import Windows Code Signing Certificate
    -        if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
    -        env:
    -          WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
    -          WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
    -        shell: powershell
    -        run: |
    -          if ($env:WINDOWS_CERTIFICATE) {
    -            Write-Host "Importing Windows Code Signing Certificate..."
    -
    -            # Decode base64 certificate and save to file
    -            $certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
    -            $certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
    -            [IO.File]::WriteAllBytes($certPath, $certBytes)
    -
    -            # Import certificate to CurrentUser\My store
    -            $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
    -
    -            # Extract and set thumbprint as environment variable
    -            $thumbprint = $cert.Thumbprint
    -            Write-Host "Certificate imported with thumbprint: $thumbprint"
    -            echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
    -
    -            # Clean up certificate file
    -            Remove-Item $certPath
    -
    -            Write-Host "Windows certificate import completed."
    -          } else {
    -            Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
    -          }
    -
           - name: Import Apple Developer Certificate
             if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
             env:
    diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml
    index 660cd2458b..4a79cb3733 100644
    --- a/.github/workflows/test-build-docker.yml
    +++ b/.github/workflows/test-build-docker.yml
    @@ -37,6 +37,7 @@ jobs:
       # spring-security=true matrix entry if `task backend:build` and
       # `task backend:build:ci` produce equivalent JARs (verify before wiring).
       test-build-docker-images:
    +    environment: ci-unsigned
         runs-on: ubuntu-latest
         strategy:
           fail-fast: false
    diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml
    deleted file mode 100644
    index 751eaf44f7..0000000000
    --- a/.github/workflows/testdriver.yml
    +++ /dev/null
    @@ -1,235 +0,0 @@
    -name: UI test with TestDriverAI
    -
    -on:
    -  push:
    -    branches: ["master", "UITest", "testdriver"]
    -
    -# cancel in-progress jobs if a new job is triggered
    -# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
    -# or a pull request is updated.
    -# It helps to save resources and time by ensuring that only the latest commit is built and tested
    -# This is particularly useful for long-running jobs that may take a while to complete.
    -# The `group` is set to a combination of the workflow name, event name, and branch name.
    -# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
    -# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
    -concurrency:
    -  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
    -  cancel-in-progress: true
    -
    -permissions:
    -  contents: read
    -
    -jobs:
    -  deploy:
    -    environment: pr-preview
    -    if: ${{ vars.CI_PROFILE != 'lite' }}
    -    runs-on: ubuntu-latest
    -    permissions:
    -      contents: read
    -      packages: write
    -    steps:
    -      - name: Harden Runner
    -        uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
    -        with:
    -          egress-policy: audit
    -
    -      - name: Checkout repository
    -        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    -
    -      - name: Set up JDK 25
    -        uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
    -        with:
    -          java-version: "25"
    -          distribution: "temurin"
    -
    -      - name: Cache Gradle User Home
    -        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
    -        with:
    -          path: |
    -            ~/.gradle/caches
    -            ~/.gradle/wrapper
    -          key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
    -          restore-keys: |
    -            gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
    -            gradle-${{ runner.os }}-${{ runner.arch }}-
    -
    -      - name: Build with Gradle
    -        run: ./gradlew build
    -        env:
    -          MAVEN_USER: ${{ secrets.MAVEN_USER }}
    -          MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
    -          MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
    -          DISABLE_ADDITIONAL_FEATURES: true
    -
    -      - name: Set up Docker Buildx
    -        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
    -
    -      - name: Get version number
    -        id: versionNumber
    -        run: |
    -          VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
    -          echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
    -
    -      - name: Convert repository owner to lowercase
    -        id: repoowner
    -        run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
    -
    -      - name: Login to GitHub Container Registry
    -        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
    -        with:
    -          registry: ghcr.io
    -          username: ${{ github.actor }}
    -          password: ${{ github.token }}
    -
    -      - name: Build and push test image
    -        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
    -        with:
    -          context: .
    -          file: ./docker/embedded/Dockerfile
    -          push: true
    -          cache-from: type=gha,scope=stirling-pdf-latest
    -          cache-to: type=gha,mode=max,scope=stirling-pdf-latest
    -          tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:test-${{ github.sha }}
    -          build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
    -          platforms: linux/amd64
    -
    -      - name: Set up SSH
    -        run: |
    -          mkdir -p ~/.ssh/
    -          echo "${NEW_VPS_SSH_KEY}" > ../private.key
    -          sudo chmod 600 ../private.key
    -
    -        env:
    -          NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
    -      - name: Deploy to VPS
    -        run: |
    -          cat > docker-compose.yml << EOF
    -          version: '3.3'
    -          services:
    -            stirling-pdf:
    -              container_name: stirling-pdf-test-${{ github.sha }}
    -              image: ${IMAGE_BASE}:test-${{ github.sha }}
    -              ports:
    -                - "1337:8080"
    -              volumes:
    -                - /stirling/test-${{ github.sha }}/data:/usr/share/tessdata:rw
    -                - /stirling/test-${{ github.sha }}/config:/configs:rw
    -                - /stirling/test-${{ github.sha }}/logs:/logs:rw
    -              environment:
    -                DISABLE_ADDITIONAL_FEATURES: "true"
    -                SECURITY_ENABLELOGIN: "false"
    -                SYSTEM_DEFAULTLOCALE: en-US
    -                UI_APPNAME: "Stirling-PDF Test"
    -                UI_HOMEDESCRIPTION: "Test Deployment"
    -                UI_APPNAMENAVBAR: "Test"
    -                SYSTEM_MAXFILESIZE: "100"
    -                METRICS_ENABLED: "true"
    -                SYSTEM_GOOGLEVISIBILITY: "false"
    -                SYSTEM_ENABLEANALYTICS: "false"
    -              restart: on-failure:5
    -          EOF
    -
    -          scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml
    -
    -          ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << EOF
    -            mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs}
    -            mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml
    -            cd /stirling/test-${{ github.sha }}
    -            docker-compose pull
    -            docker-compose up -d
    -          EOF
    -
    -        env:
    -          IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test
    -          NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
    -          NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
    -  files-changed:
    -    if: always()
    -    name: detect what files changed
    -    runs-on: ubuntu-latest
    -    timeout-minutes: 3
    -    outputs:
    -      frontend: ${{ steps.changes.outputs.frontend }}
    -    steps:
    -      - name: Harden the runner (Audit all outbound calls)
    -        uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
    -        with:
    -          egress-policy: audit
    -
    -      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    -
    -      - name: Check for file changes
    -        uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
    -        id: changes
    -        with:
    -          filters: ".github/config/.files.yaml"
    -
    -  test:
    -    environment: pr-preview
    -    if: needs.files-changed.outputs.frontend == 'true'
    -    needs: [deploy, files-changed]
    -    runs-on: ubuntu-latest
    -    steps:
    -      - name: Harden Runner
    -        uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
    -        with:
    -          egress-policy: audit
    -
    -      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    -
    -      - name: Set up Node
    -        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
    -        with:
    -          cache: "npm"
    -          cache-dependency-path: frontend/package-lock.json
    -
    -      - name: Run TestDriver.ai
    -        uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
    -        with:
    -          key: ${{secrets.TESTDRIVER_API_KEY}}
    -          prerun: |
    -            choco install go-task -y
    -            task frontend:build
    -            cd frontend
    -            npm install dashcam-chrome --save
    -            Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337"
    -            Start-Sleep -Seconds 20
    -          prompt: |
    -            1. /run testing/testdriver/test.yml
    -        env:
    -          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    -          FORCE_COLOR: "3"
    -
    -  cleanup:
    -    environment: pr-preview
    -    needs: [deploy, test]
    -    runs-on: ubuntu-latest
    -    if: always()
    -
    -    steps:
    -      - name: Harden Runner
    -        uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
    -        with:
    -          egress-policy: audit
    -
    -      - name: Set up SSH
    -        run: |
    -          mkdir -p ~/.ssh/
    -          echo "${NEW_VPS_SSH_KEY}" > ../private.key
    -          sudo chmod 600 ../private.key
    -
    -        env:
    -          NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
    -      - name: Cleanup deployment
    -        if: always()
    -        run: |
    -          ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << EOF
    -            cd /stirling/test-${{ github.sha }}
    -            docker-compose down
    -            cd /stirling
    -            rm -rf test-${{ github.sha }}
    -          EOF
    -        env:
    -          NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
    -          NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
    -        continue-on-error: true # Ensure cleanup runs even if previous steps fail
    diff --git a/WINDOWS_SIGNING.md b/WINDOWS_SIGNING.md
    index 58ffd6e657..95cbbd24e2 100644
    --- a/WINDOWS_SIGNING.md
    +++ b/WINDOWS_SIGNING.md
    @@ -4,6 +4,11 @@ This guide explains how to set up Windows code signing for Stirling-PDF desktop
     
     ## Overview
     
    +Releases are signed with **DigiCert KeyLocker**, a cloud HSM: the private key never
    +leaves DigiCert, and the runner signs through a PKCS#11 provider. The older approach
    +of uploading a base64 `.pfx` to a repository secret has been removed from the
    +workflows - the sections below describe KeyLocker, which is what actually runs.
    +
     Windows code signing is essential for:
     - Preventing Windows SmartScreen warnings
     - Building trust with users
    @@ -49,29 +54,19 @@ openssl pkcs12 -export -out certificate.pfx -inkey private-key.key -in certifica
     
     ### Required Secrets
     
    -Navigate to your GitHub repository → Settings → Secrets and variables → Actions
    +Navigate to your GitHub repository → Settings → Environments → `release-signing`.
     
    -Add the following secrets:
    +These live in the `release-signing` environment, not at repository scope. That
    +environment requires reviewer approval and is limited to `main`, `release`,
    +`hotfix/*` and `v*` tags. All five come from the DigiCert ONE console.
     
    -#### 1. `WINDOWS_CERTIFICATE`
    -- **Description**: Base64-encoded .pfx certificate file
    -- **How to create**:
    -
    -**On macOS/Linux:**
    -```bash
    -base64 -i certificate.pfx | pbcopy  # Copies to clipboard
    -```
    -
    -**On Windows (PowerShell):**
    -```powershell
    -[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Set-Clipboard
    -```
    -
    -Paste the entire base64 string into the GitHub secret.
    -
    -#### 2. `WINDOWS_CERTIFICATE_PASSWORD`
    -- **Description**: Password for the .pfx certificate
    -- **Value**: The password you set when creating/exporting the .pfx file
    +| Secret | Description |
    +| --- | --- |
    +| `SM_API_KEY` | KeyLocker API key. Also acts as the on/off switch: signing steps are gated on it being non-empty. |
    +| `SM_CLIENT_CERT_FILE_B64` | Base64-encoded PKCS#12 client authentication certificate. |
    +| `SM_CLIENT_CERT_PASSWORD` | Password for that client certificate. |
    +| `SM_KEYPAIR_ALIAS` | Alias of the signing keypair to use. |
    +| `SM_HOST` | DigiCert ONE host, e.g. `https://clientauth.one.digicert.com`. |
     
     ### Optional Secrets for Tauri Updater
     
    @@ -110,23 +105,23 @@ The Windows signing configuration is already set up:
     
     ### 2. GitHub Workflow (.github/workflows/tauri-build.yml)
     
    -The workflow includes three Windows signing steps:
    +The workflow includes four Windows signing steps, all gated on `SM_API_KEY` being
    +set and the ref being the release branch:
     
    -1. **Import Certificate**: Decodes and imports the .pfx certificate into Windows certificate store
    -2. **Build Tauri App**: Builds and signs the application using the imported certificate
    -3. **Verify Signature**: Validates that both .exe and .msi files are properly signed
    +1. **Setup DigiCert KeyLocker**: Installs the DigiCert signing tools via `digicert/ssm-code-signing`
    +2. **Setup DigiCert KeyLocker Certificate**: Writes the client cert and exports the PKCS#11 config
    +3. **Configure Windows code signing / Build Tauri app**: Signs through the PKCS#11 provider
    +4. **Verify Windows Code Signature**: Validates that the .exe and .msi are properly signed
     
     ## Testing the Setup
     
     ### 1. Local Testing (Windows Only)
     
    -Before pushing to GitHub, test locally:
    +KeyLocker is CI-only. To check signing locally, install your own certificate into
    +the Windows store and point Tauri at it; the build no longer reads any certificate
    +from an environment variable.
     
     ```powershell
    -# Set environment variables
    -$env:WINDOWS_CERTIFICATE = [Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx"))
    -$env:WINDOWS_CERTIFICATE_PASSWORD = "your-certificate-password"
    -
     # Build the application
     cd frontend
     npm run tauri build
    @@ -191,9 +186,10 @@ Look for:
     - Consider EV certificate for immediate reputation
     
     ### Certificate Not Found During Build
    -- Verify `WINDOWS_CERTIFICATE` secret is set
    -- Check base64 encoding is correct (no extra whitespace)
    -- Ensure password is correct
    +- Verify `SM_API_KEY` is present in the `release-signing` environment. If it is empty
    +  the signing steps skip silently and the build succeeds unsigned.
    +- Check `SM_CLIENT_CERT_FILE_B64` base64 encoding is correct (no extra whitespace)
    +- Ensure `SM_CLIENT_CERT_PASSWORD` and `SM_KEYPAIR_ALIAS` match the DigiCert keypair
     
     ## Security Best Practices
     
    @@ -220,11 +216,10 @@ Look for:
     ## Certificate Lifecycle
     
     ### Before Expiration
    -1. Obtain new certificate from CA (typically annual renewal)
    -2. Convert to .pfx format if needed
    -3. Update `WINDOWS_CERTIFICATE` secret with new base64-encoded certificate
    -4. Update `WINDOWS_CERTIFICATE_PASSWORD` if password changed
    -5. Test build to verify new certificate works
    +1. Renew the certificate in the DigiCert ONE console (typically annual)
    +2. If the keypair alias changed, update `SM_KEYPAIR_ALIAS` in the `release-signing` environment
    +3. If the client authentication certificate was reissued, update `SM_CLIENT_CERT_FILE_B64` and `SM_CLIENT_CERT_PASSWORD`
    +4. Test build to verify the new certificate works
     
     ### Expired Certificates
     - Signed binaries remain valid (timestamp proves signing time)
    diff --git a/frontend/editor/src/core/services/updateService.ts b/frontend/editor/src/core/services/updateService.ts
    index 043c53bc36..8b23d26deb 100644
    --- a/frontend/editor/src/core/services/updateService.ts
    +++ b/frontend/editor/src/core/services/updateService.ts
    @@ -185,7 +185,7 @@ export class UpdateService {
        */
       async getCurrentVersionFromGitHub(): Promise {
         const url =
    -      "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/V2-master/build.gradle";
    +      "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/release/build.gradle";
     
         try {
           const response = await fetch(url);
    
    From a14eec94ec13677d71541bcfd26c1397a776ade4 Mon Sep 17 00:00:00 2001
    From: James Brunton 
    Date: Tue, 18 Aug 2026 09:02:17 +0000
    Subject: [PATCH 145/154] Fix corner radius on Mantine checkboxes in Processor
     (#7537)
    
    # Description of Changes
    
    ## Before
    image
    
    ## After
    image
    ---
     frontend/editor/src/portal/theme/mantineTheme.ts | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/frontend/editor/src/portal/theme/mantineTheme.ts b/frontend/editor/src/portal/theme/mantineTheme.ts
    index 7108a90073..e371b4aeb8 100644
    --- a/frontend/editor/src/portal/theme/mantineTheme.ts
    +++ b/frontend/editor/src/portal/theme/mantineTheme.ts
    @@ -163,6 +163,10 @@ export const mantineTheme = createTheme({
         CloseButton: { defaultProps: { "aria-label": "Close" } },
         Modal: { defaultProps: { closeButtonProps: { "aria-label": "Close" } } },
         Drawer: { defaultProps: { closeButtonProps: { "aria-label": "Close" } } },
    +    // The portal's md default radius (8px) is right for cards and buttons but
    +    // rounds a 20px checkbox into a circle. Pin it to the smaller radius the
    +    // editor's checkboxes use so the box reads as a checkbox.
    +    Checkbox: { styles: { input: { borderRadius: "var(--radius-sm)" } } },
       },
       fontFamily: "var(--font-sans)",
       fontFamilyMonospace: "var(--font-mono)",
    
    From fb70fc13da03e25ee535b74116de00a83648764e Mon Sep 17 00:00:00 2001
    From: James Brunton 
    Date: Tue, 18 Aug 2026 13:08:23 +0000
    Subject: [PATCH 146/154] Fix tools which crash in the Pipelines page (#7538)
    
    # Description of Changes
    Overlay PDFs and Change Metadata both crashed in the Processor because
    they required `FilesModalContext` and `ViewerContext` respectively.
    Neither of those contexts make sense to provide in the Processor because
    there are no files in context and there is no Viewer, so redesign both
    tool settings to only optionally require these contexts. Their behaviour
    is unchanged in the Editor but they now work in the Processor (just
    without the extra info about the active files, since there are none).
    
    Also hooks up the Reorganise Pages settings so that it can be used from
    Automate. The component already existed but just wasn't being used,
    which just looks like an oversight.
    ---
     .../ChangeMetadataSingleStep.tsx              | 153 ++++++++++------
     .../tools/overlayPdfs/OverlayPdfsSettings.tsx |  61 +++++--
     .../src/core/contexts/FilesModalContext.tsx   |   4 +-
     ...tomatableToolsHaveOperationConfig.test.tsx |  11 ++
     .../core/data/useTranslatedToolRegistry.tsx   |   5 +-
     .../pipelines/PipelineStepSettings.test.tsx   | 168 +++++++++++++++++-
     6 files changed, 323 insertions(+), 79 deletions(-)
    
    diff --git a/frontend/editor/src/core/components/tools/changeMetadata/ChangeMetadataSingleStep.tsx b/frontend/editor/src/core/components/tools/changeMetadata/ChangeMetadataSingleStep.tsx
    index 2eff20b23b..07ba4e7e05 100644
    --- a/frontend/editor/src/core/components/tools/changeMetadata/ChangeMetadataSingleStep.tsx
    +++ b/frontend/editor/src/core/components/tools/changeMetadata/ChangeMetadataSingleStep.tsx
    @@ -1,5 +1,7 @@
    +import { useContext, useEffect, useState } from "react";
     import { Stack, Divider, Text } from "@mantine/core";
     import { useTranslation } from "react-i18next";
    +import { ViewerContext } from "@app/contexts/ViewerContext";
     import {
       ChangeMetadataParameters,
       createCustomMetadataFunctions,
    @@ -19,6 +21,31 @@ interface ChangeMetadataSingleStepProps {
       disabled?: boolean;
     }
     
    +/**
    + * Pre-fills the form from the currently open document's existing metadata.
    + * Isolated in its own component so it only mounts where a ViewerProvider exists
    + * (the editor and the in-editor Automate modal). The pipeline builder has no
    + * viewer and no single "current document", so it is skipped there rather than
    + * crashing on useViewer.
    + */
    +const MetadataPrefill = ({
    +  onParameterChange,
    +  onExtractingChange,
    +}: {
    +  onParameterChange: ChangeMetadataSingleStepProps["onParameterChange"];
    +  onExtractingChange: (extracting: boolean) => void;
    +}) => {
    +  const { isExtractingMetadata } = useMetadataExtraction({
    +    updateParameter: onParameterChange,
    +  });
    +
    +  useEffect(() => {
    +    onExtractingChange(isExtractingMetadata);
    +  }, [isExtractingMetadata, onExtractingChange]);
    +
    +  return null;
    +};
    +
     const ChangeMetadataSingleStep = ({
       parameters,
       onParameterChange,
    @@ -26,77 +53,85 @@ const ChangeMetadataSingleStep = ({
     }: ChangeMetadataSingleStepProps) => {
       const { t } = useTranslation();
     
    +  // Auto-prefill reads the viewer/file contexts, which only exist in the editor.
    +  // Gate on the viewer so the pipeline builder renders the fields without it.
    +  const hasViewerContext = useContext(ViewerContext) !== null;
    +  const [isExtractingMetadata, setIsExtractingMetadata] = useState(false);
    +
       // Get custom metadata functions using the utility
       const { addCustomMetadata, removeCustomMetadata, updateCustomMetadata } =
         createCustomMetadataFunctions(parameters, onParameterChange);
     
    -  // Extract metadata from uploaded files
    -  const { isExtractingMetadata } = useMetadataExtraction({
    -    updateParameter: onParameterChange,
    -  });
    -
       const isDeleteAllEnabled = parameters.deleteAll;
       const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata;
     
       return (
    -    
    -      {/* Delete All */}
    -      
    -        
    -          {t("changeMetadata.deleteAll.label", "Delete All Metadata")}
    -        
    -        
    +      {hasViewerContext && (
    +        
    -      
    -
    -      
    -
    -      {/* Standard Metadata Fields */}
    +      )}
           
    -        
    -          {t("changeMetadata.standardFields.title", "Standard Metadata")}
    -        
    -        
    +        {/* Delete All */}
    +        
    +          
    +            {t("changeMetadata.deleteAll.label", "Delete All Metadata")}
    +          
    +          
    +        
    +
    +        
    +
    +        {/* Standard Metadata Fields */}
    +        
    +          
    +            {t("changeMetadata.standardFields.title", "Standard Metadata")}
    +          
    +          
    +        
    +
    +        
    +
    +        {/* Document Dates */}
    +        
    +          
    +            {t("changeMetadata.dates.title", "Document Dates")}
    +          
    +          
    +        
    +
    +        
    +
    +        {/* Advanced Options */}
    +        
    +          
    +            {t("changeMetadata.advanced.title", "Advanced Options")}
    +          
    +          
    +        
           
    -
    -      
    -
    -      {/* Document Dates */}
    -      
    -        
    -          {t("changeMetadata.dates.title", "Document Dates")}
    -        
    -        
    -      
    -
    -      
    -
    -      {/* Advanced Options */}
    -      
    -        
    -          {t("changeMetadata.advanced.title", "Advanced Options")}
    -        
    -        
    -      
    -    
    +    
       );
     };
     
    diff --git a/frontend/editor/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx b/frontend/editor/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
    index 2648990248..e7246bb4e3 100644
    --- a/frontend/editor/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
    +++ b/frontend/editor/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
    @@ -1,3 +1,4 @@
    +import { useContext, useRef } from "react";
     import {
       Stack,
       Text,
    @@ -7,6 +8,7 @@ import {
       Divider,
     } from "@mantine/core";
     import { Button } from "@app/ui/Button";
    +import { FilePicker } from "@app/ui/FilePicker";
     import { ActionIcon } from "@app/ui/ActionIcon";
     import { SegmentedControl } from "@app/ui/SegmentedControl";
     import { useTranslation } from "react-i18next";
    @@ -15,7 +17,7 @@ import {
       type OverlayMode,
     } from "@app/hooks/tools/overlayPdfs/useOverlayPdfsParameters";
     import LocalIcon from "@app/components/shared/LocalIcon";
    -import { useFilesModalContext } from "@app/contexts/FilesModalContext";
    +import { FilesModalContext } from "@app/contexts/FilesModalContext";
     import styles from "@app/components/tools/overlayPdfs/OverlayPdfsSettings.module.css";
     import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
     
    @@ -34,7 +36,12 @@ export default function OverlayPdfsSettings({
       disabled = false,
     }: OverlayPdfsSettingsProps) {
       const { t } = useTranslation();
    -  const { openFilesModal } = useFilesModalContext();
    +  // Read optionally: the portal pipeline builder mounts no FilesModalProvider.
    +  // Present (editor tool + Automate modal) -> keep the workspace file picker;
    +  // absent (portal) -> fall back to the plain file input below.
    +  const filesModal = useContext(FilesModalContext);
    +  // Clears the FilePicker so the same file can be re-selected (Mantine resetRef).
    +  const resetOverlayPicker = useRef<() => void>(null);
     
       const handleOverlayFilesChange = (files: File[]) => {
         onParameterChange("overlayFiles", files);
    @@ -66,8 +73,8 @@ export default function OverlayPdfsSettings({
       };
     
       const handleOpenOverlayFilesModal = () => {
    -    if (disabled) return;
    -    openFilesModal({
    +    if (disabled || !filesModal) return;
    +    filesModal.openFilesModal({
           customHandler: (files: File[]) => {
             handleOverlayFilesChange([
               ...(parameters.overlayFiles || []),
    @@ -77,6 +84,17 @@ export default function OverlayPdfsSettings({
         });
       };
     
    +  const appendOverlayFiles = (files: File[]) => {
    +    if (files.length === 0) return;
    +    handleOverlayFilesChange([...(parameters.overlayFiles || []), ...files]);
    +    resetOverlayPicker.current?.();
    +  };
    +
    +  const overlayFilesButtonLabel =
    +    parameters.overlayFiles?.length > 0
    +      ? t("overlay-pdfs.overlayFiles.addMore", "Add more PDFs...")
    +      : t("overlay-pdfs.overlayFiles.placeholder", "Choose PDF(s)...");
    +
       return (
         
           
    @@ -183,17 +201,30 @@ export default function OverlayPdfsSettings({
             
               {t("overlay-pdfs.overlayFiles.label", "Overlay Files")}
             
    -        
    +        {filesModal ? (
    +          
    +        ) : (
    +          }
    +            fullWidth
    +          >
    +            {overlayFilesButtonLabel}
    +          
    +        )}
     
             {parameters.overlayFiles?.length > 0 &&
               (() => {
    diff --git a/frontend/editor/src/core/contexts/FilesModalContext.tsx b/frontend/editor/src/core/contexts/FilesModalContext.tsx
    index 73d1b0477f..17585ae7e8 100644
    --- a/frontend/editor/src/core/contexts/FilesModalContext.tsx
    +++ b/frontend/editor/src/core/contexts/FilesModalContext.tsx
    @@ -41,7 +41,9 @@ interface FilesModalContextType {
       setOnModalClose: (callback: () => void) => void;
     }
     
    -const FilesModalContext = createContext(null);
    +export const FilesModalContext = createContext(
    +  null,
    +);
     
     export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({
       children,
    diff --git a/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx b/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx
    index 461c07ebdf..c8942942df 100644
    --- a/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx
    +++ b/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx
    @@ -30,4 +30,15 @@ describe("automatable tools", () => {
     
         expect(offeredWithoutConfig).toEqual([]);
       });
    +
    +  // Reorganize Pages has an automatable form (organization mode + page-order string) and a
    +  // context-free settings component, but its registry entry once left automationSettings null,
    +  // so both Automate and the pipeline builder showed "no configurable settings". Guard the wiring.
    +  test("Reorganize Pages exposes automation settings so it is configurable, not no-settings", () => {
    +    const { result } = renderHook(() => useTranslatedToolCatalog());
    +
    +    expect(
    +      result.current.regularTools.reorganizePages?.automationSettings,
    +    ).toBeTruthy();
    +  });
     });
    diff --git a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx
    index 0a39da2fb9..5ae8075d0f 100644
    --- a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx
    +++ b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx
    @@ -700,7 +700,10 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
             endpoints: ["rearrange-pages"],
             operationConfig: asRegistryConfig(reorganizePagesOperationConfig),
             synonyms: getSynonyms(t, "reorganizePages"),
    -        automationSettings: null,
    +        automationSettings: lazySettings(
    +          () =>
    +            import("@app/components/tools/reorganizePages/ReorganizePagesSettings"),
    +        ),
           },
           scalePages: {
             icon: (
    diff --git a/frontend/editor/src/portal/components/pipelines/PipelineStepSettings.test.tsx b/frontend/editor/src/portal/components/pipelines/PipelineStepSettings.test.tsx
    index 946e24b810..55a52b496e 100644
    --- a/frontend/editor/src/portal/components/pipelines/PipelineStepSettings.test.tsx
    +++ b/frontend/editor/src/portal/components/pipelines/PipelineStepSettings.test.tsx
    @@ -1,10 +1,23 @@
     import { describe, expect, it, vi } from "vitest";
    -import { useEffect, useState } from "react";
    -import { render, screen } from "@testing-library/react";
    +import {
    +  Component,
    +  Suspense,
    +  useEffect,
    +  useState,
    +  type ComponentType,
    +  type ReactNode,
    +} from "react";
    +import { render, renderHook, screen, waitFor } from "@testing-library/react";
     import { PortalTestProviders } from "@portal/test/TestQueryProvider";
    +import { useTranslatedToolCatalog } from "@app/data/useTranslatedToolRegistry";
    +import { PreferencesProvider } from "@app/contexts/PreferencesContext";
    +import { SidebarProvider } from "@app/contexts/SidebarContext";
     import { Tooltip } from "@app/components/shared/Tooltip";
     import type { ToolRegistry } from "@app/data/toolsTaxonomy";
    -import type { WorkingToolStep } from "@app/hooks/tools/shared/toolAutomation";
    +import {
    +  getExecutableTools,
    +  type WorkingToolStep,
    +} from "@app/hooks/tools/shared/toolAutomation";
     import {
       asRegistryConfig,
       type ErasedToolParams,
    @@ -13,6 +26,10 @@ import {
     import ConvertSettings from "@app/components/tools/convert/ConvertSettings";
     import { convertOperationConfig } from "@app/hooks/tools/convert/useConvertOperation";
     import { defaultParameters as convertDefaults } from "@app/hooks/tools/convert/useConvertParameters";
    +import ChangeMetadataSingleStep from "@app/components/tools/changeMetadata/ChangeMetadataSingleStep";
    +import { defaultParameters as changeMetadataDefaults } from "@app/hooks/tools/changeMetadata/useChangeMetadataParameters";
    +import OverlayPdfsSettings from "@app/components/tools/overlayPdfs/OverlayPdfsSettings";
    +import { defaultParameters as overlayDefaults } from "@app/hooks/tools/overlayPdfs/useOverlayPdfsParameters";
     import { PipelineStepSettings } from "@portal/components/pipelines/PipelineStepSettings";
     
     // Override only useTranslation; keep the rest of react-i18next (initReactI18next et al.) real, so
    @@ -21,6 +38,7 @@ vi.mock("react-i18next", async (importOriginal) => ({
       ...(await importOriginal()),
       useTranslation: () => ({
         t: (key: string, fallback?: string) => fallback ?? key,
    +    i18n: { language: "en-US", changeLanguage: vi.fn() },
       }),
     }));
     
    @@ -63,6 +81,32 @@ const convertRegistry = {
       },
     } as unknown as Partial;
     
    +// The real Change Metadata automation settings. Its editor variant auto-prefills the
    +// form from the open document via useViewer; that path is now gated on a ViewerProvider
    +// so it renders here (the portal mounts none) instead of crashing on useViewer.
    +const changeMetadataStep = {
    +  support: "editable",
    +  toolId: "changeMetadata",
    +  params: changeMetadataDefaults,
    +} as unknown as WorkingToolStep;
    +
    +const changeMetadataRegistry = {
    +  changeMetadata: { automationSettings: ChangeMetadataSingleStep },
    +} as unknown as Partial;
    +
    +// The real Overlay PDFs automation settings. Its overlay-file picker uses the
    +// editor FilesModal when present; that read is now optional so the portal (which
    +// mounts no FilesModalProvider) renders a plain file input instead of crashing.
    +const overlayStep = {
    +  support: "editable",
    +  toolId: "overlayPdfs",
    +  params: overlayDefaults,
    +} as unknown as WorkingToolStep;
    +
    +const overlayRegistry = {
    +  overlayPdfs: { automationSettings: OverlayPdfsSettings },
    +} as unknown as Partial;
    +
     describe("PipelineStepSettings", () => {
       it("renders reused editor tool settings (which use the shared Tooltip) without app-wide Preferences/Sidebar providers", () => {
         expect(() =>
    @@ -94,6 +138,36 @@ describe("PipelineStepSettings", () => {
         expect(screen.getByText(/Convert from/)).toBeInTheDocument();
       });
     
    +  it("renders the Change Metadata tool's fields in the portal, with no ViewerProvider mounted", () => {
    +    expect(() =>
    +      render(
    +        
    +           {}}
    +          />
    +        ,
    +      ),
    +    ).not.toThrow();
    +    expect(screen.getByText("Standard Metadata")).toBeInTheDocument();
    +  });
    +
    +  it("renders the Overlay PDFs tool's fields in the portal, with no FilesModalProvider mounted", () => {
    +    expect(() =>
    +      render(
    +        
    +           {}}
    +          />
    +        ,
    +      ),
    +    ).not.toThrow();
    +    expect(screen.getByText("Overlay Mode")).toBeInTheDocument();
    +  });
    +
       // Reproduces the convert-in-pipeline bug: picking a source format fires several onParameterChange
       // calls in one tick (set fromExtension, auto-target, reset options). If each rebuilt from the
       // step snapshot captured at render they'd clobber each other and the earlier field would be lost.
    @@ -148,3 +222,91 @@ describe("PipelineStepSettings", () => {
         });
       });
     });
    +
    +// Records a render crash and swallows it (renders nothing), so one broken tool is attributed by id
    +// instead of aborting the whole sweep - mirroring the portal's own ErrorBoundary around the builder.
    +class CaptureBoundary extends Component<
    +  { onError: (error: Error) => void; children: ReactNode },
    +  { failed: boolean }
    +> {
    +  state = { failed: false };
    +  static getDerivedStateFromError() {
    +    return { failed: true };
    +  }
    +  componentDidCatch(error: Error) {
    +    this.props.onError(error);
    +  }
    +  render() {
    +    return this.state.failed ? null : this.props.children;
    +  }
    +}
    +
    +// Automated version of the manual "add every tool" sweep: render each tool's real automation
    +// settings in a portal-only context (the same Preferences + Sidebar + Suspense wrappers
    +// PipelineStepSettings uses, and NO editor providers) and fail listing any that throw. This is the
    +// guard that would have caught Change Metadata (useViewer) and Overlay PDFs (useFilesModalContext).
    +describe("PipelineStepSettings: every tool's settings render in the portal", () => {
    +  it("renders each tool's automation settings without throwing", async () => {
    +    const { result } = renderHook(() => useTranslatedToolCatalog());
    +    const catalog = result.current.allTools;
    +    // getExecutableTools is exactly what PipelineBuilder feeds its "Add a tool" picker, so this
    +    // sweeps precisely the tools a user can add. Narrow to "editable" (renders a settings
    +    // component); "noSettings"/"unsupported" steps show a Banner instead and can't crash.
    +    const editableTools = getExecutableTools(catalog)
    +      .filter((tool) => tool.support === "editable")
    +      .map((tool) => [tool.toolId, catalog[tool.toolId]] as const)
    +      .filter(([, entry]) => Boolean(entry?.automationSettings));
    +    // Guard against the filter silently matching nothing (e.g. a registry-shape change).
    +    expect(editableTools.length).toBeGreaterThan(10);
    +
    +    const failures: { toolId: string; message: string }[] = [];
    +
    +    for (const [toolId, entry] of editableTools) {
    +      const Settings = entry.automationSettings as ComponentType<
    +        ToolAutomationSettingsProps
    +      >;
    +      const params = (entry.operationConfig?.defaultParameters ??
    +        {}) as ErasedToolParams;
    +
    +      const caught: { error: Error | null } = { error: null };
    +      // The sentinel sibling commits only once the lazy Settings actually renders, so we wait for a
    +      // real render (or a caught throw) - not just the providers' wrapper DOM.
    +      const { unmount } = render(
    +        
    +          
    +            
    +               {
    +                  caught.error = error;
    +                }}
    +              >
    +                
    +                   {}}
    +                    disabled={false}
    +                  />
    +                  
    +                
    +              
    +            
    +          
    +        ,
    +      );
    +
    +      await waitFor(() =>
    +        expect(
    +          caught.error !== null ||
    +            screen.queryByTestId(`rendered-${toolId}`) !== null,
    +        ).toBe(true),
    +      );
    +
    +      if (caught.error) {
    +        failures.push({ toolId, message: caught.error.message });
    +      }
    +      unmount();
    +    }
    +
    +    expect(failures).toEqual([]);
    +  }, 30000);
    +});
    
    From cf49742d9774802c603b4d068c2f8ac9d3ffbfd1 Mon Sep 17 00:00:00 2001
    From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
    Date: Tue, 18 Aug 2026 13:56:47 +0000
    Subject: [PATCH 147/154] Fix the top bar styling (#7544)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Every top bar styled itself, so none of them matched the new UI. Also,
    colors on the premium banner (and possibly others) clashed since the
    theme changes.
    
    ## Before Example Issue
    
    Screenshot 2026-08-17 at 11 47
    20 PM
    
    
    ## After (all)
    
    danger__dark
    danger__light
    default-app__dark
    default-app__light
    free-tier-limit__dark
    free-tier-limit__light
    server-attention__dark
    server-attention__light
    team-invitation__dark
    team-invitation__light
    upgrade-prompt__dark
    upgrade-prompt__light
    
    
    ## What changed
    
    - `InfoBanner` exposed 8 colour-override props (`background`,
    `borderColor`, `textColor`, `iconColor`, `buttonColor`,
    `buttonTextColor`, `closeIconColor`, `buttonVariant`), so every caller
    invented its own look. Replaced with a closed tone set: `info` · `promo`
    · `warning` · `danger`.
    - Tone drives the whole bar — fill, border, icon and the button — so a
    CTA can't drift from the bar it sits on. Text is neutral in every tone;
    only the icon carries the tone colour.
    - All colour comes from `--c-*` tokens mixed over `--c-surface`, so the
    bars follow light and dark instead of ignoring them. The old bars were
    hardcoded: in dark mode the two licence warnings stayed cream-on-white.
    - `promo` keeps the gradient it was always meant to have, built from the
    existing `--c-hue-indigo`/`--c-hue-purple` stops (documented in
    `colors.css` as gradient hues, deliberately not accent-following), with
    the existing `premium` button accent on it.
    - Deleted the hardcoded colours from all four callers: the purple
    gradient (`#667eea`→`#764ba2`), the orange soup (`#FFF4E6` / `#9A3412` /
    `#EA580C`) duplicated across the urgent banner and the admin plan
    section, and the fixed dark bar (`--mantine-color-dark-7`) on the team
    invitation.
    - `UpgradeBanner|AdminPlanSection` sat on the theme linter's exemption
    list, which is how those colours survived the theme migration. Exemption
    removed, so `code-colors` now guards them.
    - The banner's class was colliding with `core/ui/Banner.css`'s
    `.sui-banner` (16 live rules), which restyled it in the app but not in
    Storybook — that's why the two disagreed on radius, border and tone.
    Renamed to `.app-banner`; the two surfaces now render identically.
    - Bar is square and full-bleed with a single hairline rule underneath;
    button labels are optically centred.
    - Added `--c-warning-subtle`, matching the existing `--c-danger-subtle`
    / `--c-success-subtle`.
    - New `Shared → Top bars` story renders all six bars at once, so a
    change to the shared component is visible against the whole set.
    - Unrelated one-liner: `frontend/.prettierignore` now ignores the
    gitignored `editor/screenshots/` capture artifacts, which were failing
    `format:check` locally. Happy to drop it if you'd rather keep this PR to
    the bars.
    
    ## Testing
    
    - `task frontend:check` — typecheck, lint (oxlint + 4 theme-lint passes
    + stylelint), format, 244 files / 2119 tests.
    - `frontend:storybook:a11y:changed` — clean in light and dark.
    - The a11y gate caught a real defect mid-change: giving each banner
    `role="region"` with the same label produced duplicate landmarks, which
    the app hits for real whenever two banners show at once. Landmark
    removed.
    - All six bars captured in the running editor, light and dark, and
    diffed against `origin/main`'s component rendered with each caller's
    original props.
    ---
     .../public/locales/en-US/translation.toml     |   6 +-
     frontend/editor/scripts/lint/theme-lint.mjs   |   1 -
     .../shared/TeamInvitationBanner.tsx           |   8 +-
     .../src/core/components/AppLayout.stories.tsx |   4 +-
     .../src/core/components/shared/AppBanner.css  | 115 ++++++++
     .../components/shared/AppBanner.stories.tsx   | 194 +++++++++++++
     .../src/core/components/shared/AppBanner.tsx  | 124 +++++++++
     .../components/shared/InfoBanner.stories.tsx  |  38 ---
     .../src/core/components/shared/InfoBanner.tsx | 263 ------------------
     frontend/editor/src/core/theme/colors.css     |   5 +
     .../components/shared/DefaultAppBanner.tsx    |   4 +-
     .../components/shared/UpgradeBanner.tsx       |  22 +-
     .../configSections/AdminPlanSection.tsx       |  11 +-
     13 files changed, 453 insertions(+), 342 deletions(-)
     create mode 100644 frontend/editor/src/core/components/shared/AppBanner.css
     create mode 100644 frontend/editor/src/core/components/shared/AppBanner.stories.tsx
     create mode 100644 frontend/editor/src/core/components/shared/AppBanner.tsx
     delete mode 100644 frontend/editor/src/core/components/shared/InfoBanner.stories.tsx
     delete mode 100644 frontend/editor/src/core/components/shared/InfoBanner.tsx
    
    diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml
    index 70d3d03bf4..850f69ca02 100644
    --- a/frontend/editor/public/locales/en-US/translation.toml
    +++ b/frontend/editor/public/locales/en-US/translation.toml
    @@ -1879,6 +1879,9 @@ width = "Width"
     [app]
     description = "The Free Adobe Acrobat alternative (10M+ Downloads)"
     
    +[appBanner]
    +dismiss = "Dismiss"
    +
     [attachments]
     convertToPdfA3b = "Convert to PDF/A-3b"
     convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments"
    @@ -4827,9 +4830,6 @@ title = "Image to PDF"
     [imageToPdf]
     tags = "conversion,img,jpg,picture,photo"
     
    -[infoBanner]
    -dismiss = "Dismiss"
    -
     [invite]
     acceptError = "Failed to create account"
     accountFor = "Creating account for"
    diff --git a/frontend/editor/scripts/lint/theme-lint.mjs b/frontend/editor/scripts/lint/theme-lint.mjs
    index 494fc03cca..97b32ef208 100644
    --- a/frontend/editor/scripts/lint/theme-lint.mjs
    +++ b/frontend/editor/scripts/lint/theme-lint.mjs
    @@ -642,7 +642,6 @@ const CODE_EXEMPT_PATH = [
       /mantineTheme|\/theme\.ts$|toolsTaxonomy|LayoutPreview|PageNumberPreview|CloudStorageIcons|BrandMarks/,
       /\/onboarding\//,
       /addStamp|addWatermark|\/tooltips\//,
    -  /UpgradeBanner|AdminPlanSection/,
       // Stories are checked like app code; colour-as-data lines opt out with
       // `theme-allow-color`.
       /\.test\.[jt]sx?$|\/types\//,
    diff --git a/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx b/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx
    index 3b373e9c3b..638c752f9e 100644
    --- a/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx
    +++ b/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx
    @@ -3,7 +3,7 @@ import { Group, Text } from "@mantine/core";
     import { Button } from "@app/ui/Button";
     import { useTranslation } from "react-i18next";
     import LocalIcon from "@app/components/shared/LocalIcon";
    -import { InfoBanner } from "@app/components/shared/InfoBanner";
    +import { AppBanner } from "@app/components/shared/AppBanner";
     import { useSaaSTeam } from "@app/contexts/SaaSTeamContext";
     
     /**
    @@ -105,7 +105,7 @@ export function TeamInvitationBanner() {
       );
     
       return (
    -    
       );
     }
    diff --git a/frontend/editor/src/core/components/AppLayout.stories.tsx b/frontend/editor/src/core/components/AppLayout.stories.tsx
    index 4d7e6780cf..69aceef9d7 100644
    --- a/frontend/editor/src/core/components/AppLayout.stories.tsx
    +++ b/frontend/editor/src/core/components/AppLayout.stories.tsx
    @@ -4,7 +4,7 @@ import { AppLayout } from "@app/components/AppLayout";
     import { BannerProvider, useBanner } from "@app/contexts/BannerContext";
     import { NavigationProvider } from "@app/contexts/NavigationContext";
     import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
    -import { InfoBanner } from "@app/components/shared/InfoBanner";
    +import { AppBanner } from "@app/components/shared/AppBanner";
     
     const meta = {
       title: "Components/AppLayout",
    @@ -49,7 +49,7 @@ function BannerSetter() {
       const { setBanner } = useBanner();
       useEffect(() => {
         setBanner(
    -      ;
    +export default meta;
    +type Story = StoryObj;
    +
    +export const Info: Story = {
    +  args: {
    +    icon: "info-rounded",
    +    title: "Heads up",
    +    message: "This document contains form fields that will be flattened.",
    +  },
    +};
    +
    +export const Promo: Story = {
    +  args: {
    +    tone: "promo",
    +    icon: "stars-rounded",
    +    title: "Upgrade to Server Plan",
    +    message:
    +      "Get the most out of Stirling PDF with unlimited users and advanced features.",
    +    buttonText: "Upgrade Now",
    +    buttonIcon: "upgrade-rounded",
    +    onButtonClick: () => {},
    +    compact: true,
    +  },
    +};
    +
    +export const Warning: Story = {
    +  args: {
    +    tone: "warning",
    +    icon: "warning-rounded",
    +    title: "Action required",
    +    message: "Some pages could not be processed and were skipped.",
    +    buttonText: "Review",
    +    onButtonClick: () => {},
    +  },
    +};
    +
    +export const Danger: Story = {
    +  args: {
    +    tone: "danger",
    +    icon: "warning-rounded",
    +    title: "This server needs admin attention",
    +    message: "Review the license requirements to keep this server compliant.",
    +    buttonText: "See info",
    +    buttonIcon: "info-rounded",
    +    onButtonClick: () => {},
    +    dismissible: false,
    +  },
    +};
    +
    +export const Compact: Story = {
    +  args: {
    +    compact: true,
    +    icon: "info-rounded",
    +    message: "Autosave is enabled for this file.",
    +    dismissible: false,
    +  },
    +};
    +
    +/** Message-only, no title: the message takes the title's weight so the bar still reads. */
    +export const MessageOnly: Story = {
    +  args: {
    +    icon: "picture-as-pdf-rounded",
    +    message:
    +      "Make Stirling PDF your default application for opening PDF files.",
    +    buttonText: "Set Default",
    +    onButtonClick: () => {},
    +    secondaryButtonText: "Don't remind me again",
    +    onSecondaryButtonClick: () => {},
    +  },
    +};
    +
    +function Row({ caption, children }: { caption: string; children: ReactNode }) {
    +  return (
    +    
    + + {caption} + + {children} +
    + ); +} + +/** + * Every top bar the app can show, in one place: each entry mirrors a real caller, + * so a change to the component is visible against the whole set at once. Renders a + * composition rather than the component, so it takes no args of its own. + */ +export const AllTopBars: StoryObj = { + render: () => ( +
    + + {}} + /> + + + + {}} + dismissible={false} + /> + + + + {}} + dismissible={false} + /> + + + + {}} + secondaryButtonText="Decline" + onSecondaryButtonClick={() => {}} + dismissible={false} + /> + + + + {}} + secondaryButtonText="Don't remind me again" + onSecondaryButtonClick={() => {}} + /> + + + + {}} + dismissible={false} + /> + +
    + ), +}; diff --git a/frontend/editor/src/core/components/shared/AppBanner.tsx b/frontend/editor/src/core/components/shared/AppBanner.tsx new file mode 100644 index 0000000000..ee0ba03741 --- /dev/null +++ b/frontend/editor/src/core/components/shared/AppBanner.tsx @@ -0,0 +1,124 @@ +import React, { ReactNode } from "react"; +import { Button } from "@app/ui/Button"; +import { ActionIcon } from "@app/ui/ActionIcon"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import "@app/components/shared/AppBanner.css"; + +/** Picks the whole look. Callers choose meaning, never colours. */ +export type AppBannerTone = "info" | "promo" | "warning" | "danger"; + +/** Tone decides the button too, so the CTA can't drift from the bar it sits on. */ +const TONE_BUTTON = { + info: { variant: "secondary", accent: "default" }, + promo: { variant: "primary", accent: "premium" }, + warning: { variant: "primary", accent: "warning" }, + danger: { variant: "primary", accent: "danger" }, +} as const; + +interface AppBannerProps { + /** A LocalIcon name, or a pre-rendered node (e.g. a logo) dropped in as-is. */ + icon?: string | ReactNode; + title?: ReactNode; + message: ReactNode; + buttonText?: string; + buttonIcon?: string; + onButtonClick?: () => void; + /** Muted secondary action, e.g. "Don't remind me again". */ + secondaryButtonText?: string; + onSecondaryButtonClick?: () => void; + onDismiss?: () => void; + dismissible?: boolean; + loading?: boolean; + show?: boolean; + tone?: AppBannerTone; + compact?: boolean; +} + +/** The app's top bar: dismissible messaging above the workspace. */ +export const AppBanner: React.FC = ({ + icon, + title, + message, + buttonText, + buttonIcon = "check-circle-rounded", + onButtonClick, + secondaryButtonText, + onSecondaryButtonClick, + onDismiss, + dismissible = true, + loading = false, + show = true, + tone = "info", + compact = false, +}) => { + const { t } = useTranslation(); + if (!show) return null; + + const iconSize = compact ? "1rem" : "1.25rem"; + + return ( +
    + {icon != null && ( + + {typeof icon === "string" ? ( + + ) : ( + icon + )} + + )} + +
    + {title && {title}} + {message} +
    + +
    + {buttonText && onButtonClick && ( + + )} + {secondaryButtonText && onSecondaryButtonClick && ( + + )} + {dismissible && ( + onDismiss?.()} + aria-label={t("appBanner.dismiss", "Dismiss")} + > + + + )} +
    +
    + ); +}; diff --git a/frontend/editor/src/core/components/shared/InfoBanner.stories.tsx b/frontend/editor/src/core/components/shared/InfoBanner.stories.tsx deleted file mode 100644 index 5fad071d05..0000000000 --- a/frontend/editor/src/core/components/shared/InfoBanner.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { InfoBanner } from "@app/components/shared/InfoBanner"; - -const meta = { - title: "Shared/InfoBanner", - component: InfoBanner, - parameters: { layout: "padded" }, -} satisfies Meta; -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - icon: "info-rounded", - title: "Heads up", - message: "This document contains form fields that will be flattened.", - }, -}; - -export const Warning: Story = { - args: { - tone: "warning", - icon: "warning-rounded", - title: "Action required", - message: "Some pages could not be processed and were skipped.", - buttonText: "Review", - onButtonClick: () => {}, - }, -}; - -export const Compact: Story = { - args: { - compact: true, - icon: "info-rounded", - message: "Autosave is enabled for this file.", - dismissible: false, - }, -}; diff --git a/frontend/editor/src/core/components/shared/InfoBanner.tsx b/frontend/editor/src/core/components/shared/InfoBanner.tsx deleted file mode 100644 index 2056b6a92f..0000000000 --- a/frontend/editor/src/core/components/shared/InfoBanner.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { ReactNode } from "react"; -import { Paper, Group, Text, Stack } from "@mantine/core"; -import { Button, type ButtonVariant, type ButtonAccent } from "@app/ui/Button"; -import { ActionIcon } from "@app/ui/ActionIcon"; -import { useTranslation } from "react-i18next"; -import LocalIcon from "@app/components/shared/LocalIcon"; - -type InfoBannerTone = "info" | "warning"; - -const toneStyles: Record< - InfoBannerTone, - { - background: string; - border: string; - text: string; - icon: string; - buttonColor: string; - } -> = { - info: { - background: "var(--mantine-color-blue-0)", - border: "var(--mantine-color-blue-2)", - text: "var(--mantine-color-blue-9)", - icon: "var(--mantine-color-blue-6)", - buttonColor: "blue", - }, - warning: { - background: "var(--mantine-color-orange-0)", - border: "var(--mantine-color-orange-3)", - text: "var(--color-amber-dark)", - icon: "var(--mantine-color-orange-7)", - buttonColor: "orange", - }, -}; - -function toSharedButtonVariant( - variant: "light" | "filled" | "white" | "outline" | "subtle", -): ButtonVariant { - switch (variant) { - case "filled": - return "primary"; - case "outline": - return "secondary"; - case "subtle": - return "tertiary"; - case "light": - case "white": - default: - return "secondary"; - } -} - -function toSharedButtonAccent(color: string | undefined): ButtonAccent { - // Mantine colours may carry a shade suffix (e.g. "orange.7"); use the hue. - const hue = (color ?? "").split(".")[0]; - switch (hue) { - case "red": - return "danger"; - case "green": - return "success"; - case "yellow": - case "orange": - return "warning"; - case "blue": - default: - return "default"; - } -} - -interface InfoBannerProps { - /** - * Either a LocalIcon name (string) for the standard sized icon slot, or a - * pre-rendered ReactNode (e.g. a logo image) which is dropped in as-is. - */ - icon?: string | ReactNode; - title?: ReactNode; - message: ReactNode; - buttonText?: string; - buttonIcon?: string; - onButtonClick?: () => void; - /** Optional muted secondary action (e.g. "Don't remind me again"). */ - secondaryButtonText?: string; - onSecondaryButtonClick?: () => void; - onDismiss?: () => void; - dismissible?: boolean; - loading?: boolean; - show?: boolean; - tone?: InfoBannerTone; - background?: string; - borderColor?: string; - textColor?: string; - iconColor?: string; - buttonColor?: string; - buttonVariant?: "light" | "filled" | "white" | "outline" | "subtle"; - /** Override the button label colour (for dark/custom theme variants). */ - buttonTextColor?: string; - minHeight?: number | string; - closeIconColor?: string; - compact?: boolean; -} - -/** - * Generic info banner component for displaying dismissible messages at the top of the app - */ -export const InfoBanner: React.FC = ({ - icon, - title, - message, - buttonText, - buttonIcon = "check-circle-rounded", - onButtonClick, - secondaryButtonText, - onSecondaryButtonClick, - onDismiss, - dismissible = true, - loading = false, - show = true, - tone = "info", - background, - borderColor, - textColor, - iconColor, - buttonColor, - buttonVariant = "light", - buttonTextColor, - minHeight = 56, - closeIconColor, - compact = false, -}) => { - const { t } = useTranslation(); - if (!show) { - return null; - } - - const toneStyle = toneStyles[tone] ?? toneStyles.info; - const resolvedTextColor = textColor ?? toneStyle.text; - const handleDismiss = () => { - onDismiss?.(); - }; - - const iconSize = compact ? "1rem" : "1.2rem"; - const textSize = compact ? "xs" : "sm"; - - return ( - - - - {icon != null && - (typeof icon === "string" ? ( - - ) : ( -
    - {icon} -
    - ))} - - {title && ( - - {title} - - )} - - {message} - - -
    - - {buttonText && onButtonClick && ( - - )} - {secondaryButtonText && onSecondaryButtonClick && ( - - )} - {dismissible && ( - - - - )} - -
    -
    - ); -}; diff --git a/frontend/editor/src/core/theme/colors.css b/frontend/editor/src/core/theme/colors.css index dffd6beaba..dd3c2aec7c 100644 --- a/frontend/editor/src/core/theme/colors.css +++ b/frontend/editor/src/core/theme/colors.css @@ -71,6 +71,11 @@ html[data-app-theme="light"] { var(--c-success) 10%, var(--c-surface) ); + --c-warning-subtle: color-mix( + in srgb, + var(--c-warning) 10%, + var(--c-surface) + ); /* ── Decorative / brand / categorical palette ────────────────────────── Fixed hues that intentionally do NOT follow the chosen accent: brand diff --git a/frontend/editor/src/desktop/components/shared/DefaultAppBanner.tsx b/frontend/editor/src/desktop/components/shared/DefaultAppBanner.tsx index 1b24cd4675..5ba92c2782 100644 --- a/frontend/editor/src/desktop/components/shared/DefaultAppBanner.tsx +++ b/frontend/editor/src/desktop/components/shared/DefaultAppBanner.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { InfoBanner } from "@app/components/shared/InfoBanner"; +import { AppBanner } from "@app/components/shared/AppBanner"; import { useDefaultApp } from "@app/hooks/useDefaultApp"; export const DefaultAppBanner: React.FC = () => { @@ -15,7 +15,7 @@ export const DefaultAppBanner: React.FC = () => { const [sessionDismissed, setSessionDismissed] = useState(false); return ( - { ); return ( - { buttonIcon="info-rounded" onButtonClick={buttonText ? handleSeeInfo : undefined} dismissible={false} - minHeight={60} - background="#FFF4E6" - borderColor="var(--mantine-color-orange-7)" - textColor="#9A3412" - iconColor="#EA580C" - buttonVariant="filled" - buttonColor="orange.7" /> ); }; @@ -341,7 +334,7 @@ const UpgradeBanner: React.FC = () => { return ( <> {friendlyVisible && ( - { onButtonClick={handleUpgrade} onDismiss={handleFriendlyDismiss} show={friendlyVisible} - background="linear-gradient(135deg, #667eea 0%, #764ba2 100%)" - borderColor="transparent" - textColor="#fff" - iconColor="#fff" - closeIconColor="#fff" - buttonVariant="filled" - buttonColor="blue" - minHeight={48} + tone="promo" compact /> )} diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx index f683861e8d..c69040f0e7 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx @@ -12,7 +12,7 @@ import AvailablePlansSection from "@app/components/shared/config/configSections/ import StaticPlanSection from "@app/components/shared/config/configSections/plan/StaticPlanSection"; import LicenseKeySection from "@app/components/shared/config/configSections/plan/LicenseKeySection"; import { alert } from "@app/components/toast"; -import { InfoBanner } from "@app/components/shared/InfoBanner"; +import { AppBanner } from "@app/components/shared/AppBanner"; import { useLicenseAlert } from "@app/hooks/useLicenseAlert"; import { getPreferredCurrency, @@ -200,7 +200,7 @@ const AdminPlanSection: React.FC = () => { {shouldShowLicenseWarning && ( - { buttonIcon="upgrade-rounded" onButtonClick={scrollToPlans} dismissible={false} - minHeight={68} - background="#FFF4E6" - borderColor="var(--mantine-color-orange-7)" - textColor="#9A3412" - iconColor="#EA580C" - buttonVariant="filled" - buttonColor="orange.7" /> )} From 913601ff0372d3fa1cadd11e48b1ebd2c921cdaa Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:00:43 +0000 Subject: [PATCH 148/154] Consolidate the editor + processor sidebar footers into one component (#7539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Both sidebars ended in a different bottom section. The editor showed an account row (avatar, name, settings); the processor showed a "Link Stirling account" CTA plus a `Settings` nav item and no identity at all. They are now **one shared ``** rendering the same rows in both apps, in this order: 1. the link-account CTA (self-hosted, when unlinked) 2. free credits remaining 3. **Open \** 4. the account row — avatar, name, settings It's a **single surface** with hairline dividers between rows, not stacked cards. Rows are assembled as a list, so a row this build doesn't show (no wallet, no processor access, nothing to link) takes its divider with it rather than leaving a stray line. This also fixes the profile-picture/initials desync between the sidebar and the account settings page. ## Screenshots Captured with the stubbed Playwright harness at 1600x900, scoped to the sidebar and auto-cropped to the region that actually changed. Base is `origin/main`; every state is driven by dummy backend stubs so all the nav-bar permutations are covered. montage_cloud-dark montage_cloud-light montage_editor-dark montage_editor-light montage_processor-dark montage_processor-light The free-credits meter is a cloud-build surface, so the self-hosted capture can't reach it. Those states come from the new Storybook stories with dummy wallet data (`Shared/NavFooter`), which is also where the credit tone bands and the collapsed rail are easiest to review. ## How it's wired `NavFooter` is purely presentational. Each app resolves its own data through three `@app/*` seams, so core carries no build-specific gating and any box whose data is absent is dropped rather than rendered empty. | Seam | core | cloud / proprietary / saas | |---|---|---| | `useFreeCreditsSummary` | `null` — self-hosted editor installs aren't metered | cloud reads `freeRemaining` / `freeAllowance` off the same `useWallet()` the Plan page's free meter uses, so the sidebar and Plan can't disagree | | `useOtherAppSwitch` | `null` — core ships no processor | gated on `portalAccess` (`/api/v1/auth/me` in SaaS, the Spring session flag self-hosted) | | Link-account CTA | n/a | unchanged conditions — passed in as `accountExtras`, still only when `linkState === "unlinked"`, still a no-op in SaaS | - The processor reads the meter through its own `@portal/hooks/useFreeCreditsSummary` rather than the editor's `@app` one. Self-hosted resolves `@app/*` as proprietary → core, where the cloud wallet hook isn't in the cascade, and the implementation can't live in `proprietary/` because core/desktop builds ship no portal and must never resolve `@portal`. Keeping it in `portal/` gets the figure to the linked self-hosted processor without weakening that rule; it reads the same `GET /api/v1/payg/wallet` the Usage page's trial meter already renders, gated on link state and behind the portal's query cache. `portal-saas/` just re-exports the cloud hook, so both footers share one fetch. The processor-access gate previously lived in two near-identical `AppSwitcher` copies. It moves into `useOtherAppSwitch`, `AppSwitcher` now reads it too, and the duplicate `saas/components/shared/AppSwitcher.tsx` is deleted — the logo switcher and the footer row can no longer disagree about access. ## Profile picture sync One `useAccountIdentity` hook now backs the editor footer, the processor footer and the account settings page. Previously settings derived its initial from `email[0]` while the sidebar used `displayName[0]`, and the two drew different blue discs. Alongside that, the shared `Avatar`: - falls back to initials when a picture URL fails to load, instead of leaving an empty disc - renders one letter for single-word names (`admin` → "A", not "AD") - gains an `xl` size so the settings hero disc is the same component ## Notes - Labelled **"Free credits"** rather than "free monthly credits": `freeAllowance` is documented as a one-time lifetime grant, not a monthly reset, so "monthly" would misdescribe the data. Happy to change if the backend semantics differ from the type comments. ## Testing - `task frontend:check` and `task frontend:typecheck:all` pass (all 9 build variants). - 9 new `Shared/NavFooter` stories pass the Chromium + axe story scan; `frontend:storybook:a11y:changed` reports no regressions. - Stubbed E2E suite passes, including the `config-button` tour/settings specs that target the account row. Two failures (`console-clean › landing`, `viewer-text-selection › Ctrl+C`) also fail on `origin/main` locally — they need a backend on :8080 and clipboard permissions. --- .../public/locales/en-US/translation.toml | 23 +- .../config/configSections/usageMeters.tsx | 22 +- .../src/cloud/hooks/useFreeCreditsSummary.ts | 50 ++++ .../editor/src/cloud/hooks/useOpenPlan.ts | 13 ++ frontend/editor/src/cloud/hooks/useWallet.ts | 90 +++++++- .../src/core/components/shared/BrandMark.css | 46 ++++ .../core/components/shared/FileSidebar.css | 92 +------- .../core/components/shared/FileSidebar.tsx | 130 ++--------- .../components/shared/navFooter/NavFooter.css | 156 +++++++++++++ .../shared/navFooter/NavFooter.stories.tsx | 119 ++++++++++ .../shared/navFooter/NavFooter.test.tsx | 58 +++++ .../components/shared/navFooter/NavFooter.tsx | 213 ++++++++++++++++++ .../shared/navFooter/NavFooterCreditsRow.css | 83 +++++++ .../shared/navFooter/NavFooterCreditsRow.tsx | 158 +++++++++++++ .../src/core/hooks/useAccountIdentity.ts | 64 ++++++ .../src/core/hooks/useFreeCreditsSummary.ts | 12 + frontend/editor/src/core/hooks/useOpenPlan.ts | 10 + .../src/core/hooks/useOtherAppSwitch.ts | 12 + frontend/editor/src/core/query/keys.ts | 3 + .../src/core/services/navFooterCache.ts | 73 ++++++ frontend/editor/src/core/ui/Avatar.css | 6 + frontend/editor/src/core/ui/Avatar.tsx | 27 ++- .../hooks/useFreeCreditsSummary.ts | 7 + .../src/portal-saas/hooks/useOpenPlan.ts | 11 + .../editor/src/portal/components/Sidebar.css | 8 +- .../editor/src/portal/components/Sidebar.tsx | 29 ++- .../billing/PrepaidCapacityCard.tsx | 9 +- .../portal/components/billing/WalletMeter.tsx | 26 ++- .../hooks/useFreeCreditsSummary.test.tsx | 110 +++++++++ .../src/portal/hooks/useFreeCreditsSummary.ts | 65 ++++++ .../editor/src/portal/hooks/useOpenPlan.ts | 13 ++ frontend/editor/src/portal/queries/keys.ts | 2 + .../editor/src/proprietary/billing/format.ts | 23 ++ .../editor/src/proprietary/billing/index.ts | 1 + .../components/shared/AppSwitcher.tsx | 20 +- .../proprietary/hooks/useOtherAppSwitch.ts | 15 ++ .../saas/components/shared/AppSwitcher.tsx | 41 ---- .../shared/config/configSections/Overview.tsx | 24 +- .../src/saas/hooks/useOtherAppSwitch.ts | 16 ++ .../src/saas/hooks/usePortalAccess.test.tsx | 43 +++- .../editor/src/saas/hooks/usePortalAccess.ts | 76 ++++--- .../src/saas/hooks/useWallet.poll.test.tsx | 158 +++++++++++++ 42 files changed, 1804 insertions(+), 353 deletions(-) create mode 100644 frontend/editor/src/cloud/hooks/useFreeCreditsSummary.ts create mode 100644 frontend/editor/src/cloud/hooks/useOpenPlan.ts create mode 100644 frontend/editor/src/core/components/shared/navFooter/NavFooter.css create mode 100644 frontend/editor/src/core/components/shared/navFooter/NavFooter.stories.tsx create mode 100644 frontend/editor/src/core/components/shared/navFooter/NavFooter.test.tsx create mode 100644 frontend/editor/src/core/components/shared/navFooter/NavFooter.tsx create mode 100644 frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.css create mode 100644 frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.tsx create mode 100644 frontend/editor/src/core/hooks/useAccountIdentity.ts create mode 100644 frontend/editor/src/core/hooks/useFreeCreditsSummary.ts create mode 100644 frontend/editor/src/core/hooks/useOpenPlan.ts create mode 100644 frontend/editor/src/core/hooks/useOtherAppSwitch.ts create mode 100644 frontend/editor/src/core/services/navFooterCache.ts create mode 100644 frontend/editor/src/portal-saas/hooks/useFreeCreditsSummary.ts create mode 100644 frontend/editor/src/portal-saas/hooks/useOpenPlan.ts create mode 100644 frontend/editor/src/portal/hooks/useFreeCreditsSummary.test.tsx create mode 100644 frontend/editor/src/portal/hooks/useFreeCreditsSummary.ts create mode 100644 frontend/editor/src/portal/hooks/useOpenPlan.ts create mode 100644 frontend/editor/src/proprietary/hooks/useOtherAppSwitch.ts delete mode 100644 frontend/editor/src/saas/components/shared/AppSwitcher.tsx create mode 100644 frontend/editor/src/saas/hooks/useOtherAppSwitch.ts create mode 100644 frontend/editor/src/saas/hooks/useWallet.poll.test.tsx diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 850f69ca02..12ad26ceec 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -5057,6 +5057,14 @@ title = "Upload from Mobile" tags = "Multi Tool,Multi operation,UI,click drag,front end,client side,interactive,intractable,move,delete,migrate,divide" title = "PDF Multi Tool" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" + [oauth.error] message = "Authentication was not successful. You can close this window and try again." title = "Authentication Failed" @@ -5621,8 +5629,8 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] -barAria = "Free PDFs used" -capSuffix = "/ {{limit}} free PDFs" +barAria = "Free PDFs remaining" +capSuffix = "of {{limit}} free PDFs left" metaCategories = "Automation · AI · API requests" [payg.free.member] @@ -6685,12 +6693,12 @@ reachedTitle = "Monthly spend limit reached" title = "Couldn't open Stripe portal" [portal.billing.walletMeter] -barAria = "Free PDFs used" -capSuffix_one = "of {{allowance}} free PDFs used" -capSuffix_other = "of {{allowance}} free PDFs used" +barAria = "Free PDFs remaining" +capSuffix_one = "of {{allowance}} free PDF left" +capSuffix_other = "of {{allowance}} free PDFs left" eyebrow = "Processor trial" -statusLabel_one = "{{remaining}} left" -statusLabel_other = "{{remaining}} left" +statusLabel_one = "{{used}} used" +statusLabel_other = "{{used}} used" sub = "Use the PDF Editor for free. Pay to process PDFs automatically." title_one = "Process {{allowance}} PDFs free" title_other = "Process {{allowance}} PDFs free" @@ -7665,7 +7673,6 @@ integrations = "Integrations" pipelines = "Pipelines" policies = "Policies" procurement = "Procurement" -settings = "Settings" sources = "Sources" usage = "Usage & Billing" users = "Users" diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx index 8811e537b2..c4407cae7f 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx @@ -12,6 +12,7 @@ import { formatPeriodDate, MeterBar, meterState, + remainingMeter, } from "@app/billing"; import "@app/components/shared/config/configSections/Payg.css"; import "@app/components/shared/config/configSections/PaygFree.css"; @@ -48,7 +49,8 @@ export function useFreeSnapshot(): FreeSnapshot { export function FreeMeterPanel({ snap }: { snap: FreeSnapshot }) { const { t } = useTranslation(); - const { state, pct } = meterState(snap.billableUsed, snap.billableLimit); + const remaining = Math.max(0, snap.billableLimit - snap.billableUsed); + const { state, pct } = remainingMeter(remaining, snap.billableLimit); const stateLabel = state === "DEGRADED" ? t("payg.free.state.limitReached", "Limit reached") @@ -60,9 +62,9 @@ export function FreeMeterPanel({ snap }: { snap: FreeSnapshot }) { { + if (live !== undefined) writeCachedCredits(live); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wallet]); + + return (live !== undefined ? live : seed) ?? null; +} diff --git a/frontend/editor/src/cloud/hooks/useOpenPlan.ts b/frontend/editor/src/cloud/hooks/useOpenPlan.ts new file mode 100644 index 0000000000..4d532319f6 --- /dev/null +++ b/frontend/editor/src/cloud/hooks/useOpenPlan.ts @@ -0,0 +1,13 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +/** + * Cloud editor builds open the settings modal on its Plan section, which is + * where the free grant is explained and the Processor plan is switched on. + * Routed rather than called directly because the modal is URL-driven here + * (`/settings/*`), the same path the admin tour uses to open it. + */ +export function useOpenPlan(): (() => void) | null { + const navigate = useNavigate(); + return useCallback(() => navigate("/settings/plan"), [navigate]); +} diff --git a/frontend/editor/src/cloud/hooks/useWallet.ts b/frontend/editor/src/cloud/hooks/useWallet.ts index 0a3f78b3ce..ed3cb2ce6b 100644 --- a/frontend/editor/src/cloud/hooks/useWallet.ts +++ b/frontend/editor/src/cloud/hooks/useWallet.ts @@ -32,6 +32,14 @@ * promise see the UI flip exactly once the new state is visible — no * intermediate flash of the old value. * + *

    Freshness

    + * + * The figures drain as metered work runs, so a mounted consumer re-reads the + * wallet every {@link WALLET_POLL_MS} and again whenever the tab regains + * visibility. Those refreshes are silent — they leave {@code loading} and + * {@code error} alone and only commit fresher data — so consumers that gate on + * those flags don't flicker on a background tick. + * *

    Dev preview fallback

    * * When the hook is rendered outside the saas app (e.g. on {@code @@ -178,6 +186,13 @@ function reuseIfEqual(prev: Wallet | null, next: Wallet): Wallet { return prev; } +/** + * How often a mounted consumer re-reads the wallet. Matches the app query + * client's staleTime, so the sidebar meter and anything cached elsewhere age + * out on the same clock. + */ +const WALLET_POLL_MS = 30_000; + export function useWallet(): UseWalletResult { // Resolved once: the dev-preview side-channel when rendered outside the real // app (saas /dev/payg-preview route), else null (every real build + desktop). @@ -201,13 +216,29 @@ export function useWallet(): UseWalletResult { // "the request fired." Cleared when no load is pending. const inFlight = useRef | null>(null); + // Set for refreshes the user didn't ask for (the poll below). Silence governs + // whether a load may RAISE `loading` / `error`, never whether it may clear + // them: consumers gate on both — the limit modals do + // `if (loading || !wallet) return null`, and Plan swaps in an error alert — + // so a background tick must not blink an open modal out or replace a working + // page over a transient failure. Clearing is always the latest request's job, + // silent or not; a silent load that skipped the clear would strand `loading` + // true after superseding a visible one, which suppresses those modals for the + // rest of the session. + const silentRefresh = useRef(false); + useEffect(() => { const reqId = ++latestReqId.current; let cancelled = false; + const silent = silentRefresh.current; + silentRefresh.current = false; + const promise = (async () => { - setLoading(true); - setError(null); + if (!silent) { + setLoading(true); + setError(null); + } if (devPreview) { const synth = devPreview.buildWallet(devPreview.role()); @@ -221,11 +252,22 @@ export function useWallet(): UseWalletResult { const res = await apiClient.get("/api/v1/payg/wallet"); if (cancelled || reqId !== latestReqId.current) return; setWallet((prev) => reuseIfEqual(prev, res.data)); + // Fresh data retires any earlier failure, including one a silent poll + // is recovering from — otherwise Plan keeps its alert over good data. + setError(null); } catch (e: unknown) { if (cancelled || reqId !== latestReqId.current) return; - console.warn("[useWallet] fetch failed", e); - setError(e instanceof Error ? e.message : "Failed to load wallet"); + if (!silent) { + console.warn("[useWallet] fetch failed", e); + setError(e instanceof Error ? e.message : "Failed to load wallet"); + } + // A failed background refresh is a non-event: the last good snapshot + // stands and the next tick self-heals, so it neither surfaces nor + // logs — otherwise an offline tab warns every WALLET_POLL_MS. } finally { + // Deliberately not gated on `silent`: whichever load is latest owns + // settling the flag, or a silent refresh that supersedes a visible one + // leaves it stuck true. if (!cancelled && reqId === latestReqId.current) { setLoading(false); } @@ -242,6 +284,46 @@ export function useWallet(): UseWalletResult { }; }, [devPreview, refetchTick]); + // The wallet drains as automation, AI and API work runs, so a figure fetched + // on mount goes stale while the user watches it. Refresh on a timer, and + // immediately on returning to the tab — coming back to a stale number is the + // case people actually notice. Hidden tabs don't poll, and the dev-preview + // wallet is synthesised locally so there is nothing to re-read. + useEffect(() => { + if (devPreview) return; + + let timer: ReturnType | undefined; + const refresh = () => { + silentRefresh.current = true; + setRefetchTick((t) => t + 1); + }; + const stop = () => { + if (timer !== undefined) { + clearInterval(timer); + timer = undefined; + } + }; + const start = () => { + stop(); + timer = setInterval(refresh, WALLET_POLL_MS); + }; + const onVisibilityChange = () => { + if (document.visibilityState === "visible") { + refresh(); + start(); + } else { + stop(); + } + }; + + if (document.visibilityState === "visible") start(); + document.addEventListener("visibilitychange", onVisibilityChange); + return () => { + stop(); + document.removeEventListener("visibilitychange", onVisibilityChange); + }; + }, [devPreview]); + const refetch = useCallback(async () => { setRefetchTick((t) => t + 1); // Snapshot the next-tick promise so the caller awaits this refetch diff --git a/frontend/editor/src/core/components/shared/BrandMark.css b/frontend/editor/src/core/components/shared/BrandMark.css index 7ddff9b4c7..df05ff1307 100644 --- a/frontend/editor/src/core/components/shared/BrandMark.css +++ b/frontend/editor/src/core/components/shared/BrandMark.css @@ -48,9 +48,55 @@ transform: matrix(0.483871, -0.017568, 0, 0.338028, 23.887097, 26.886428); } +/* One-shot "thinking" drift — the two parallelograms swap past each other and + settle back. Same motion the chat FAB loops while the agent works, but this + pair starts and ends at rest (translate 0, full opacity) so a single + iteration can end without snapping. Callers apply it for one beat; see + NavFooter.css for the hover use. */ +@keyframes sui-brandmark-drift-a { + 0%, + 100% { + transform: translate(0, 0); + opacity: 1; + } + 25% { + transform: translate(-1px, -5px); + opacity: 0.55; + } + 50% { + transform: translate(-6px, 0); + opacity: 0.9; + } + 75% { + transform: translate(-1px, 5px); + opacity: 0.6; + } +} + +@keyframes sui-brandmark-drift-b { + 0%, + 100% { + transform: translate(0, 0); + opacity: 1; + } + 25% { + transform: translate(1px, 5px); + opacity: 0.85; + } + 50% { + transform: translate(6px, 0); + opacity: 0.5; + } + 75% { + transform: translate(1px, -5px); + opacity: 0.85; + } +} + @media (prefers-reduced-motion: reduce) { .sui-brandmark__a, .sui-brandmark__b { transition: none; + animation: none; } } diff --git a/frontend/editor/src/core/components/shared/FileSidebar.css b/frontend/editor/src/core/components/shared/FileSidebar.css index 590f59fa7d..2347d9a2b2 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.css +++ b/frontend/editor/src/core/components/shared/FileSidebar.css @@ -75,16 +75,13 @@ padding: 0.25rem 0; overflow: hidden; } -.file-sidebar-footer-box { - padding: 0.25rem 0; - flex-shrink: 0; -} +/* The footer is the shared : it brings its own boxes and padding, + so this class only positions it in the column. */ /* Collapsed rail: the file tree isn't rendered, so hide its (empty) box and let the boxes stack at the top — controls, then the settings footer right after — instead of the files box stretching to fill. */ -.file-sidebar[data-collapsed="true"] .file-sidebar-controls, -.file-sidebar[data-collapsed="true"] .file-sidebar-footer-box { +.file-sidebar[data-collapsed="true"] .file-sidebar-controls { padding: 0.25rem; } .file-sidebar[data-collapsed="true"] .file-sidebar-files-box { @@ -538,86 +535,3 @@ pointer-events: none; animation: none; } - -/* ---- Bottom bar (user + settings) ---- */ -.file-sidebar-bottom-bar { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 6px; - flex-shrink: 0; - min-height: 40px; -} - -/* Bottom bar settings icon tracks the right edge during collapse animation */ - -.file-sidebar-bottom-avatar { - width: 28px; - height: 28px; - border-radius: 50%; - background-color: var(--c-accent-text); - color: var(--c-text-on-primary); - font-size: 12px; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - user-select: none; - overflow: hidden; -} - -/* No colored disc behind an actual photo; keep it for the initials fallback. */ -.file-sidebar-bottom-avatar--picture { - background-color: transparent; -} - -.file-sidebar-bottom-avatar-img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; -} - -.file-sidebar-bottom-name { - flex: 1; - font-size: 13px; - font-weight: 500; - color: var(--c-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; -} - -.file-sidebar-bottom-bar[role="button"]:hover { - background-color: var(--c-hover); -} - -.file-sidebar-bottom-bar[role="button"]:focus-visible { - outline: 2px solid var(--c-primary); - outline-offset: -2px; -} - -.file-sidebar-bottom-settings { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 6px; - color: var(--c-text-subtle); - padding: 0; - flex-shrink: 0; - margin-left: auto; -} - -.file-sidebar[data-collapsed="true"] .file-sidebar-bottom-settings { - width: 32px; - height: 32px; -} - -.file-sidebar[data-collapsed="true"] .file-sidebar-bottom-bar { - justify-content: center; - padding: 8px 0; -} diff --git a/frontend/editor/src/core/components/shared/FileSidebar.tsx b/frontend/editor/src/core/components/shared/FileSidebar.tsx index 2514e7f556..1c06236027 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebar.tsx @@ -22,13 +22,15 @@ import { } from "@app/contexts/NavigationContext"; import { useViewer } from "@app/contexts/ViewerContext"; import { useFileHandler } from "@app/hooks/useFileHandler"; -import { useAuth } from "@app/auth/UseSession"; -import { useProfilePictureUrl } from "@app/hooks/useProfilePictureUrl"; +import { useAccountIdentity } from "@app/hooks/useAccountIdentity"; +import { useFreeCreditsSummary } from "@app/hooks/useFreeCreditsSummary"; +import { useOtherAppSwitch } from "@app/hooks/useOtherAppSwitch"; +import { useOpenPlan } from "@app/hooks/useOpenPlan"; +import { NavFooter } from "@app/components/shared/navFooter/NavFooter"; import { useIndexedDB, useIndexedDBRevision, } from "@app/contexts/IndexedDBContext"; -import { accountService } from "@app/services/accountService"; import { GoogleDriveIcon } from "@app/components/shared/CloudStorageIcons"; import { AppSwitcher } from "@app/components/shared/AppSwitcher"; import { SidebarToggleIcon } from "@app/components/shared/SidebarToggleIcon"; @@ -37,8 +39,7 @@ import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import FolderSpecialIcon from "@mui/icons-material/FolderSpecial"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import AddIcon from "@mui/icons-material/Add"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import SettingsIcon from "@mui/icons-material/Settings"; +import OpenInFullIcon from "@mui/icons-material/OpenInFull"; import type { FileId } from "@app/types/file"; import { FileItem } from "@app/components/shared/FileSidebarFileItem"; import { useLabelName } from "@app/data/labelDisplay"; @@ -241,43 +242,11 @@ const FileSidebar = forwardRef( const { addFiles } = useFileHandler(); const indexedDB = useIndexedDB(); - // Each auth layer derives its own displayName from its native user shape. - // Fall back to the proprietary REST endpoint only when the auth - // context yields nothing - then to "User" as a generic last resort. - const { displayName: authDisplayName, isAnonymous } = useAuth(); - const [accountUsername, setAccountUsername] = useState(null); - const displayName = - authDisplayName ?? accountUsername ?? t("auth.displayName.user", "User"); - - const profilePictureUrl = useProfilePictureUrl(); - const [pictureFailed, setPictureFailed] = useState(false); - useEffect(() => setPictureFailed(false), [profilePictureUrl]); - const showProfilePicture = !!profilePictureUrl && !pictureFailed; - - useEffect(() => { - if (!config?.enableLogin) { - setAccountUsername(null); - return; - } - if (authDisplayName) { - // The auth context has a name; don't bother hitting the REST - // endpoint, but clear any stale cached value from a prior call. - setAccountUsername(null); - return; - } - accountService - .getAccountData() - .then((data) => { - // Always reflect the latest result - including clearing it on - // sign-out, when the endpoint returns no username (or 401s into - // the catch branch below). Without this, signing out would leave - // the old username on screen. - setAccountUsername(data?.username ?? null); - }) - .catch(() => { - setAccountUsername(null); - }); - }, [config?.enableLogin, authDisplayName]); + const { displayName, profilePictureUrl, isAnonymous } = + useAccountIdentity(); + const credits = useFreeCreditsSummary(); + const otherApp = useOtherAppSwitch(); + const openPlan = useOpenPlan(); // Leaf files = user-visible files (excludes intermediate tool outputs) const [allFileStubs, setAllFileStubs] = useState([]); @@ -1115,7 +1084,7 @@ const FileSidebar = forwardRef( )} data-testid="open-files-page" > - + ( {/* Getting-started checklist, floating above the footer (SaaS only). */} - {/* Box 3 — account footer (avatar + name + settings). */} - - {/* Bottom bar: user name + settings */} - -
    e.key === "Enter" && onOpenSettings() - : undefined - } - data-testid={onOpenSettings ? "config-button" : undefined} - data-tour={onOpenSettings ? "config-button" : undefined} - aria-label={ - onOpenSettings - ? t("fileSidebar.openSettings", "Open settings") - : displayName - } - style={onOpenSettings ? { cursor: "pointer" } : undefined} - > -
    - {showProfilePicture ? ( - setPictureFailed(true)} - /> - ) : ( - displayName.charAt(0).toUpperCase() - )} -
    - {!collapsed && ( - - {displayName} - - )} - {onOpenSettings && !collapsed && ( -
    - -
    - )} -
    -
    -
    + {/* Box 3 — the shared footer: credits, app switch, account row. */} +
    ); }, diff --git a/frontend/editor/src/core/components/shared/navFooter/NavFooter.css b/frontend/editor/src/core/components/shared/navFooter/NavFooter.css new file mode 100644 index 0000000000..aa2688cd35 --- /dev/null +++ b/frontend/editor/src/core/components/shared/navFooter/NavFooter.css @@ -0,0 +1,156 @@ +/* Shared sidebar footer: one surface holding the link-account CTA, the credits + meter, the other-app switch and the account row, hairline-separated. + Structural only — every colour comes from a --c-* semantic token. */ + +.nav-footer { + display: flex; + flex-direction: column; + flex-shrink: 0; + /* Vertical only: the slots carry the horizontal padding so their separator + runs the full width of the surface. */ + padding: 0.25rem 0; + overflow: hidden; +} + +.nav-footer__slot { + padding-inline: 0.375rem; +} + +/* Separators are drawn by the slots themselves, never as their own elements. + A slot whose contents render nothing (the link-account CTA returns null once + the org is linked, and an element is truthy even when it renders null) is + :empty, so it is skipped by both rules below — it can't leave a line behind, + and it can't push one to the top or bottom of the surface. A rule that only + ever matches a slot PRECEDED by another visible slot cannot draw a leading + separator, whatever the caller passes in. */ +.nav-footer__slot:empty { + display: none; +} + +.nav-footer__slot:not(:empty) ~ .nav-footer__slot:not(:empty) { + border-top: 1px solid var(--c-border-subtle); + margin-top: 0.25rem; + padding-top: 0.25rem; +} + +/* Fades the rows up on the first footer mount of a page session only. They are + seeded from cache, so they're already present at first paint; replaying this + on every later mount (switching apps, remounting a view) would animate + content that never changed and read as a twitch. */ +@keyframes nav-footer-row-in { + from { + opacity: 0; + transform: translateY(0.25rem); + } + to { + opacity: 1; + transform: none; + } +} + +.nav-footer[data-animate] .nav-footer__slot:not(:empty) { + animation: nav-footer-row-in var(--motion-enter) both; +} + +@media (prefers-reduced-motion: reduce) { + .nav-footer[data-animate] .nav-footer__slot:not(:empty) { + animation: none; + } +} + +/* ---- Rows (link-account, credits, switch, account) ---- */ + +.nav-footer__row { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + min-height: 2.25rem; + padding: 0.25rem 0.375rem; + border: 0; + border-radius: 0.5rem; + background: none; + color: var(--c-text); + font: inherit; + text-align: left; + cursor: pointer; +} + +.nav-footer__row:disabled { + cursor: default; +} + +.nav-footer__row:not(:disabled):hover { + background-color: var(--c-hover); +} + +.nav-footer__row:focus-visible { + outline: 2px solid var(--c-primary); + outline-offset: -2px; +} + +.nav-footer__row-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 1.625rem; +} + +/* Hovering the switch row plays the mark's "thinking" drift once — the same + motion the chat FAB loops, for a single beat, as a hint that the row hands + off to the other app. One iteration only: it starts and ends at rest, so + nothing snaps when it finishes, and re-entering the row replays it. */ +.nav-footer__row:hover .sui-brandmark__a { + animation: sui-brandmark-drift-a 1.1s ease-in-out 1; +} +.nav-footer__row:hover .sui-brandmark__b { + animation: sui-brandmark-drift-b 1.1s ease-in-out 1; +} + +.nav-footer__row-label { + flex: 1; + min-width: 0; + font-size: 0.8125rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Trailing affordance on a row: the account row's gear, the switch row's + leaving-this-app arrow. */ +.nav-footer__trailing { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + color: var(--c-text-subtle); +} + +/* Rows contributed by a caller (the link-account NavItem) sit in the same + surface, so match this footer's row metrics rather than the nav rail's. */ +.nav-footer .sui-navitem { + min-height: 2.25rem; + padding: 0.25rem 0.375rem; + margin: 0; + border-radius: 0.5rem; + font-size: 0.8125rem; +} + +/* ---- Collapsed icon rail ---- */ + +.nav-footer[data-collapsed] .nav-footer__slot { + padding-inline: 0.25rem; +} + +.nav-footer[data-collapsed] .nav-footer__row { + justify-content: center; + padding-inline: 0; +} + +.nav-footer[data-collapsed] .sui-navitem { + justify-content: center; + padding-inline: 0; +} diff --git a/frontend/editor/src/core/components/shared/navFooter/NavFooter.stories.tsx b/frontend/editor/src/core/components/shared/navFooter/NavFooter.stories.tsx new file mode 100644 index 0000000000..31a04be596 --- /dev/null +++ b/frontend/editor/src/core/components/shared/navFooter/NavFooter.stories.tsx @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import LinkIcon from "@mui/icons-material/Link"; +import { NavItem } from "@app/ui/NavItem"; +import { NavFooter } from "@app/components/shared/navFooter/NavFooter"; + +/** Stands in for a CTA that has decided it has nothing to show. */ +function RendersNothing() { + return null; +} + +const meta: Meta = { + title: "Shared/NavFooter", + component: NavFooter, + parameters: { layout: "padded" }, + args: { + displayName: "admin", + onOpenSettings: () => {}, + credits: { remaining: 247, total: 500 }, + onOpenPlan: () => {}, + otherApp: { app: "processor", onOpen: () => {} }, + }, + decorators: [ + (S) => ( +
    + +
    + ), + ], +}; +export default meta; +type Story = StoryObj; + +/** The editor's footer: credits, "Open PDF Processor", the account row. */ +export const InEditor: Story = {}; + +/** The processor's footer. Same three boxes, opposite switch target. */ +export const InProcessor: Story = { + args: { otherApp: { app: "editor", onOpen: () => {} } }, +}; + +/** Self-hosted processor: no wallet, so no meter, and the link-account CTA + * rides along in the account box. */ +export const WithLinkAccountCta: Story = { + args: { + credits: null, + otherApp: { app: "editor", onOpen: () => {} }, + accountExtras: ( + } + /> + ), + }, +}; + +/** Regression guard: the processor always passes its link-account CTA, but that + * component renders null once the org is linked. An element is truthy even + * when it renders nothing, so this must not leave a separator above the first + * visible row. */ +export const ExtrasThatRenderNothing: Story = { + args: { accountExtras: }, +}; + +/** A real profile picture replaces the initials disc. */ +export const WithProfilePicture: Story = { + args: { + displayName: "Ada Lovelace", + profilePictureUrl: + "data:image/svg+xml;utf8," + + encodeURIComponent( + '', + ), + }, +}; + +/** Credits running low — the dot and bar shift to the warning tone at 20% left. */ +export const CreditsLow: Story = { + args: { credits: { remaining: 42, total: 500 } }, +}; + +/** Allowance exhausted. */ +export const CreditsExhausted: Story = { + args: { credits: { remaining: 0, total: 500 } }, +}; + +/** Core OSS: no wallet, no second app, settings only. */ +export const MinimalBuild: Story = { + args: { credits: null, otherApp: null }, +}; + +/** No settings handler — the account row is inert identity, not a button. */ +export const NoSettings: Story = { + args: { onOpenSettings: undefined }, +}; + +/** Collapsed icon rail: labels become tooltips. */ +export const Collapsed: Story = { + args: { collapsed: true }, + decorators: [ + (S) => ( +
    + +
    + ), + ], +}; diff --git a/frontend/editor/src/core/components/shared/navFooter/NavFooter.test.tsx b/frontend/editor/src/core/components/shared/navFooter/NavFooter.test.tsx new file mode 100644 index 0000000000..a32063e56f --- /dev/null +++ b/frontend/editor/src/core/components/shared/navFooter/NavFooter.test.tsx @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { cleanup, render } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import { NavFooter } from "@app/components/shared/navFooter/NavFooter"; + +/** The footer's tooltips need Mantine's theme context. */ +function withProviders(ui: React.ReactNode) { + return {ui}; +} + +function renderFooter() { + const { container } = render( + withProviders( + {}} + credits={{ remaining: 247, total: 500 }} + otherApp={{ app: "processor", onOpen: () => {} }} + />, + ), + ); + return container.querySelector(".nav-footer") as HTMLElement; +} + +describe("NavFooter — enter animation", () => { + it("plays once per page session, not on every remount", () => { + // The rows are seeded from cache, so they're present at first paint. Every + // later mount — switching apps, remounting a view — would otherwise replay + // the fade on content that never changed, which reads as a twitch. + expect(renderFooter().dataset.animate).toBe("true"); + cleanup(); + expect(renderFooter().dataset.animate).toBeUndefined(); + cleanup(); + expect(renderFooter().dataset.animate).toBeUndefined(); + }); +}); + +describe("NavFooter — separators", () => { + it("never renders a divider beside a row that renders nothing", () => { + // Dividers are CSS between adjacent non-empty slots, so an extras element + // that returns null (the linked org's link-account CTA) can't leave a line. + const { container } = render( + withProviders( + {}} + credits={null} + otherApp={null} + accountExtras={<>{null}} + />, + ), + ); + const slots = container.querySelectorAll(".nav-footer__slot"); + const filled = [...slots].filter((s) => s.childElementCount > 0); + expect(filled).toHaveLength(1); + expect(container.querySelectorAll(".nav-footer__divider")).toHaveLength(0); + }); +}); diff --git a/frontend/editor/src/core/components/shared/navFooter/NavFooter.tsx b/frontend/editor/src/core/components/shared/navFooter/NavFooter.tsx new file mode 100644 index 0000000000..373c91bccf --- /dev/null +++ b/frontend/editor/src/core/components/shared/navFooter/NavFooter.tsx @@ -0,0 +1,213 @@ +import { useState, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Tooltip } from "@mantine/core"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import SettingsIcon from "@mui/icons-material/Settings"; +import { Avatar, NavSurface } from "@app/ui"; +import { BrandMark } from "@app/components/shared/BrandMark"; +import { type AppSwitchTarget } from "@app/components/shared/AppSwitch"; +import { + NavFooterCreditsRow, + type NavFooterCredits, +} from "@app/components/shared/navFooter/NavFooterCreditsRow"; +import "@app/components/shared/navFooter/NavFooter.css"; + +export interface NavFooterAppLink { + /** The app this footer is NOT in — the one the row opens. */ + app: AppSwitchTarget; + onOpen: () => void; +} + +export interface NavFooterProps { + /** Name shown next to the avatar, and the source of its initials fallback. */ + displayName: string; + /** Profile picture; initials are drawn when absent or the URL fails to load. */ + profilePictureUrl?: string | null; + /** Omit to render the account row as static text (no settings affordance). */ + onOpenSettings?: () => void; + /** Null/undefined hides the meter — builds with no wallet never show it. */ + credits?: NavFooterCredits | null; + /** Opens the plan surface from the credits row; omit to leave it inert. */ + onOpenPlan?: () => void; + /** Null/undefined hides the switch row — e.g. no access to the other app. */ + otherApp?: NavFooterAppLink | null; + /** Extra rows above the account row (the self-hosted link-account CTA). */ + accountExtras?: ReactNode; + /** Icon-rail state: labels collapse to tooltips. */ + collapsed?: boolean; + className?: string; +} + +/** + * Whether the enter animation has already played this page session. The rows + * are seeded from cache now, so they're present from first paint and every + * later mount — switching apps, remounting a view — would otherwise replay the + * animation on content that never changed, which reads as the UI twitching. + */ +let hasPlayedEnter = false; + +/** + * The bottom section every sidebar ends with, shared by the editor and the + * processor so both present the same rows. ONE surface, hairline-separated, in + * this order: + * + * 1. caller-contributed rows (the self-hosted link-account CTA) + * 2. free credits remaining + * 3. "Open " + * 4. the account row — avatar, name, settings + * + * Purely presentational: each app resolves its own identity, wallet and + * app-switch access and passes them in, so this file carries no build-specific + * gating. A row whose data is absent is dropped, and so is the separator that + * would have sat beside it. + */ +export function NavFooter({ + displayName, + profilePictureUrl, + onOpenSettings, + credits, + onOpenPlan, + otherApp, + accountExtras, + collapsed = false, + className, +}: NavFooterProps) { + const { t } = useTranslation(); + const [animate] = useState(() => { + if (hasPlayedEnter) return false; + hasPlayedEnter = true; + return true; + }); + + const settingsLabel = t("fileSidebar.openSettings", "Open settings"); + const accountLabel = onOpenSettings + ? `${displayName} - ${settingsLabel}` + : displayName; + + // One surface, hairline-separated rows. Each row gets a slot; the separators + // are drawn by CSS between adjacent NON-EMPTY slots (see NavFooter.css), so a + // row that renders nothing — the link-account CTA returns null once the org is + // linked, and an element is truthy even then — can't leave a line behind. + const rows: Array<{ key: string; node: ReactNode }> = []; + + if (accountExtras) rows.push({ key: "extras", node: accountExtras }); + + if (credits) { + rows.push({ + key: "credits", + node: ( + + ), + }); + } + + if (otherApp) { + rows.push({ + key: "switch", + node: ( + + + + ), + }); + } + + rows.push({ + key: "account", + node: ( + + + + ), + }); + + return ( + + {rows.map((row) => ( +
    + {row.node} +
    + ))} +
    + ); +} + +function openAppLabel( + app: AppSwitchTarget, + t: (key: string, fallback: string) => string, +): string { + return app === "editor" + ? t("navFooter.openEditor", "Open PDF Editor") + : t("navFooter.openProcessor", "Open PDF Processor"); +} diff --git a/frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.css b/frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.css new file mode 100644 index 0000000000..b37def6b2d --- /dev/null +++ b/frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.css @@ -0,0 +1,83 @@ +/* Free-credits meter inside the sidebar footer. The row base (padding, hover, + focus) comes from NavFooter.css; these rules are the meter itself. */ + +.nav-footer__credits { + flex-direction: column; + align-items: stretch; + gap: 0.375rem; + cursor: default; +} + +/* Inert by default, so it must not read as hoverable; the actionable variant + opts back into the shared row hover. */ +.nav-footer__credits:hover { + background: none; +} + +.nav-footer__credits--actionable { + cursor: pointer; +} +.nav-footer__credits--actionable:hover { + background-color: var(--c-hover); +} + +.nav-footer__credits-head { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; +} + +.nav-footer__dot { + width: 0.4375rem; + height: 0.4375rem; + border-radius: 50%; + flex-shrink: 0; + background-color: var(--c-success); +} +.nav-footer__dot[data-tone="warning"] { + background-color: var(--c-warning); +} +.nav-footer__dot[data-tone="danger"] { + background-color: var(--c-danger); +} + +.nav-footer__credits-label { + flex: 1; + min-width: 0; + font-weight: 500; + color: var(--c-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nav-footer__credits-count { + flex-shrink: 0; + color: var(--c-text-muted); + font-variant-numeric: tabular-nums; +} + +/* ---- Collapsed rail ---- */ + +/* Rotated so the fill starts at 12 o'clock and runs clockwise. */ +.nav-footer__credits-ring { + width: 1.25rem; + height: 1.25rem; + margin-inline: auto; + transform: rotate(-90deg); +} + +.nav-footer__credits-ring-track, +.nav-footer__credits-ring-fill { + fill: none; + stroke-width: 3; +} + +.nav-footer__credits-ring-track { + stroke: var(--c-surface-sunken); +} + +.nav-footer__credits-ring-fill { + stroke-linecap: round; +} diff --git a/frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.tsx b/frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.tsx new file mode 100644 index 0000000000..94e941d71c --- /dev/null +++ b/frontend/editor/src/core/components/shared/navFooter/NavFooterCreditsRow.tsx @@ -0,0 +1,158 @@ +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Tooltip } from "@mantine/core"; +import { ProgressBar } from "@app/ui"; +import "@app/components/shared/navFooter/NavFooterCreditsRow.css"; + +export interface NavFooterCredits { + /** Free credits still available to spend. */ + remaining: number; + /** Size of the free allowance — the "of N" denominator. */ + total: number; +} + +/** Remaining-credit bands, mirroring the usage meters' 80% / 100% thresholds. */ +function creditsTone(remaining: number, total: number): string { + if (remaining <= 0) return "danger"; + return total > 0 && remaining / total <= 0.2 ? "warning" : "success"; +} + +interface NavFooterCreditsRowProps { + credits: NavFooterCredits; + /** Icon rail: the figures drop and the bar alone carries the state. */ + collapsed: boolean; + /** Row label, passed in so the meter owns no copy of its own. */ + label: string; + /** Opens the plan surface. Omit to render the meter as inert text. */ + onOpen?: () => void; +} + +/** + * The free-credits meter as it appears in the sidebar footer: a state dot, the + * label, "X of Y" remaining, and a fill bar underneath. Figures are clamped + * here so a wallet that reports more remaining than the allowance (or negative) + * can't overflow the bar. + * + * Rendered as a {@code nav-footer__row}, so it inherits that row's metrics + * from NavFooter.css and only brings its own meter styling. + */ +export function NavFooterCreditsRow({ + credits, + collapsed, + label, + onOpen, +}: NavFooterCreditsRowProps) { + const { t } = useTranslation(); + + const total = Math.max(0, credits.total); + const remaining = Math.min(Math.max(0, credits.remaining), total); + const tone = creditsTone(remaining, total); + const count = t("navFooter.credits.count", "{{remaining}} of {{total}}", { + remaining: remaining.toLocaleString(), + total: total.toLocaleString(), + }); + + return ( + + + {collapsed ? ( + // The rail is one icon wide, so a full-width bar would read as a + // stray line; a ring carries the same fraction at icon size. + 0 ? remaining / total : 0} + tone={tone} + label={`${label}: ${count}`} + /> + ) : ( + <> +
    + + {label} + {count} +
    + 0 ? remaining / total : 0} + height={6} + color={`var(--c-${tone})`} + label={`${label}: ${count}`} + /> + + )} +
    +
    + ); +} + +/** Icon-sized donut carrying the same remaining fraction as the expanded bar. */ +function CreditsRing({ + fraction, + tone, + label, +}: { + fraction: number; + tone: string; + label: string; +}) { + const RADIUS = 8; + const circumference = 2 * Math.PI * RADIUS; + const filled = Math.min(1, Math.max(0, fraction)) * circumference; + + return ( + + + + + ); +} + +/** + * The meter is a button only where there is a plan surface to open — otherwise + * it stays a plain div, so a build with nowhere to go doesn't advertise a + * click that does nothing. + */ +function Row({ + onOpen, + label, + children, +}: { + onOpen?: () => void; + label: string; + children: ReactNode; +}) { + const className = `nav-footer__row nav-footer__credits${ + onOpen ? " nav-footer__credits--actionable" : "" + }`; + if (!onOpen) return
    {children}
    ; + return ( + + ); +} diff --git a/frontend/editor/src/core/hooks/useAccountIdentity.ts b/frontend/editor/src/core/hooks/useAccountIdentity.ts new file mode 100644 index 0000000000..026ac08dab --- /dev/null +++ b/frontend/editor/src/core/hooks/useAccountIdentity.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAuth } from "@app/auth/UseSession"; +import { useProfilePictureUrl } from "@app/hooks/useProfilePictureUrl"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { accountService } from "@app/services/accountService"; + +export interface AccountIdentity { + /** Never empty — falls back to a generic "User" so a row is never blank. */ + displayName: string; + profilePictureUrl: string | null; + isAnonymous: boolean; +} + +/** + * The signed-in identity as the UI should draw it: one name and one picture, + * resolved the same way everywhere. Every surface that shows "who am I" (the + * editor and processor sidebar footers, the account settings page) reads this, + * so a user can't see one initial in the sidebar and a different one in + * settings. + * + * Resolution order for the name: the auth layer's own displayName (each layer + * derives it from its native user shape), then the proprietary REST endpoint, + * then a generic last resort. + */ +export function useAccountIdentity(): AccountIdentity { + const { t } = useTranslation(); + const { config } = useAppConfig(); + const { displayName: authDisplayName, isAnonymous } = useAuth(); + const profilePictureUrl = useProfilePictureUrl(); + const [accountUsername, setAccountUsername] = useState(null); + + useEffect(() => { + if (!config?.enableLogin) { + setAccountUsername(null); + return; + } + if (authDisplayName) { + // The auth context has a name; don't bother hitting the REST + // endpoint, but clear any stale cached value from a prior call. + setAccountUsername(null); + return; + } + accountService + .getAccountData() + .then((data) => { + // Always reflect the latest result - including clearing it on + // sign-out, when the endpoint returns no username (or 401s into + // the catch branch below). Without this, signing out would leave + // the old username on screen. + setAccountUsername(data?.username ?? null); + }) + .catch(() => { + setAccountUsername(null); + }); + }, [config?.enableLogin, authDisplayName]); + + return { + displayName: + authDisplayName ?? accountUsername ?? t("auth.displayName.user", "User"), + profilePictureUrl, + isAnonymous, + }; +} diff --git a/frontend/editor/src/core/hooks/useFreeCreditsSummary.ts b/frontend/editor/src/core/hooks/useFreeCreditsSummary.ts new file mode 100644 index 0000000000..52702a3f74 --- /dev/null +++ b/frontend/editor/src/core/hooks/useFreeCreditsSummary.ts @@ -0,0 +1,12 @@ +import { type NavFooterCredits } from "@app/components/shared/navFooter/NavFooterCreditsRow"; + +/** + * Free credits left on this team's allowance, for the sidebar footer meter. + * Null hides the meter entirely. + * + * Core has no wallet — self-hosted installs aren't metered — so there is + * nothing to show. Cloud builds override this with the live wallet figure. + */ +export function useFreeCreditsSummary(): NavFooterCredits | null { + return null; +} diff --git a/frontend/editor/src/core/hooks/useOpenPlan.ts b/frontend/editor/src/core/hooks/useOpenPlan.ts new file mode 100644 index 0000000000..da6fe2207e --- /dev/null +++ b/frontend/editor/src/core/hooks/useOpenPlan.ts @@ -0,0 +1,10 @@ +/** + * Opens the plan surface behind the sidebar footer's free-credits row, or null + * when this build has none (the row is then inert text rather than a button). + * + * Core ships no wallet and no plan section, so there is nothing to open. Builds + * that meter usage override this with their own surface. + */ +export function useOpenPlan(): (() => void) | null { + return null; +} diff --git a/frontend/editor/src/core/hooks/useOtherAppSwitch.ts b/frontend/editor/src/core/hooks/useOtherAppSwitch.ts new file mode 100644 index 0000000000..612589899b --- /dev/null +++ b/frontend/editor/src/core/hooks/useOtherAppSwitch.ts @@ -0,0 +1,12 @@ +import { type NavFooterAppLink } from "@app/components/shared/navFooter/NavFooter"; + +/** + * The sibling app this build can switch to (editor ⇄ processor), or null when + * there is none. The single gate behind both the brand switcher and the + * sidebar footer's "Open ..." row, so the two can never disagree about access. + * + * Core ships no processor, so there is nothing to switch to. + */ +export function useOtherAppSwitch(): NavFooterAppLink | null { + return null; +} diff --git a/frontend/editor/src/core/query/keys.ts b/frontend/editor/src/core/query/keys.ts index a7a68ea256..5354b56b63 100644 --- a/frontend/editor/src/core/query/keys.ts +++ b/frontend/editor/src/core/query/keys.ts @@ -6,5 +6,8 @@ export const qk = { ["editor", "endpointEnabled", endpoint] as const, footerInfo: () => ["editor", "footerInfo"] as const, groupEnabled: (group: string) => ["editor", "groupEnabled", group] as const, + /** Keyed on the asking identity: two users must never share one answer. */ + portalAccess: (userId: string | null) => + ["editor", "portalAccess", userId] as const, users: () => ["editor", "users"] as const, } as const; diff --git a/frontend/editor/src/core/services/navFooterCache.ts b/frontend/editor/src/core/services/navFooterCache.ts new file mode 100644 index 0000000000..cbde941c81 --- /dev/null +++ b/frontend/editor/src/core/services/navFooterCache.ts @@ -0,0 +1,73 @@ +/** + * Last-known sidebar-footer state, so the rows are correct at first paint + * instead of arriving a request later. + * + * The footer is mounted by both apps, and the editor and processor are separate + * React trees with separate query caches — so without this, every navigation + * between them (and every remount inside them) re-ran the fetches and the rows + * visibly popped in and shoved each other around. Persisting to storage rather + * than to an in-memory cache is what makes it survive that boundary, and a + * reload. + * + * Deliberately stale-then-revalidate: what's stored is only ever what the + * backend last said, every reader refetches immediately and overwrites, and + * nothing is gated on it — the processor enforces its own access server-side, + * and a stale credit figure is replaced within a second of the wallet landing. + */ +const CREDITS_KEY = "stirling.navFooter.credits"; +const OTHER_APP_KEY = "stirling.navFooter.otherApp"; + +/** Figures, or null for a team that sees no meter at all (a paying one). */ +export type CachedCredits = { remaining: number; total: number } | null; + +function read(key: string): string | null { + try { + return localStorage.getItem(key); + } catch { + // Private mode / storage disabled — behave as a first-ever load. + return null; + } +} + +function write(key: string, value: string): void { + try { + localStorage.setItem(key, value); + } catch { + // Nothing to do: the cache is an optimisation, never a correctness input. + } +} + +/** `undefined` when this browser has never seen an answer. */ +export function readCachedCredits(): CachedCredits | undefined { + const raw = read(CREDITS_KEY); + if (raw === null) return undefined; + if (raw === "none") return null; + try { + const parsed: unknown = JSON.parse(raw); + if ( + typeof parsed === "object" && + parsed !== null && + typeof (parsed as CachedCredits & object).remaining === "number" && + typeof (parsed as CachedCredits & object).total === "number" + ) { + return parsed as CachedCredits; + } + } catch { + // Corrupt entry — fall through and treat it as never-seen. + } + return undefined; +} + +export function writeCachedCredits(credits: CachedCredits): void { + write(CREDITS_KEY, credits === null ? "none" : JSON.stringify(credits)); +} + +/** `undefined` when this browser has never seen an answer. */ +export function readCachedOtherApp(): boolean | undefined { + const raw = read(OTHER_APP_KEY); + return raw === null ? undefined : raw === "true"; +} + +export function writeCachedOtherApp(canOpen: boolean): void { + write(OTHER_APP_KEY, String(canOpen)); +} diff --git a/frontend/editor/src/core/ui/Avatar.css b/frontend/editor/src/core/ui/Avatar.css index e2e8dc63bd..360c15c566 100644 --- a/frontend/editor/src/core/ui/Avatar.css +++ b/frontend/editor/src/core/ui/Avatar.css @@ -46,6 +46,12 @@ height: 2.5rem; font-size: 1rem; } +/* Account-settings hero disc. */ +.sui-avatar--xl { + width: 4.5rem; + height: 4.5rem; + font-size: 1.75rem; +} .sui-avatar__img { width: 100%; diff --git a/frontend/editor/src/core/ui/Avatar.tsx b/frontend/editor/src/core/ui/Avatar.tsx index c7cfac501b..42e7aca11f 100644 --- a/frontend/editor/src/core/ui/Avatar.tsx +++ b/frontend/editor/src/core/ui/Avatar.tsx @@ -1,6 +1,7 @@ +import { useEffect, useState } from "react"; import "@app/ui/Avatar.css"; -export type AvatarSize = "xs" | "sm" | "md" | "lg"; +export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl"; export type AvatarTone = | "blue" | "purple" @@ -23,10 +24,12 @@ export interface AvatarProps { className?: string; } -function initialsOf(name: string): string { +function avatarInitials(name: string): string { const parts = name.trim().split(/\s+/).filter(Boolean); if (parts.length === 0) return "?"; - if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + // Single word (a username or an email) reads as one letter — two letters of + // "admin" ("AD") looks like a different person's initials, not a truncation. + if (parts.length === 1) return parts[0].slice(0, 1).toUpperCase(); return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } @@ -43,6 +46,13 @@ export function Avatar({ ariaLabel, className, }: AvatarProps) { + // A picture URL that 404s (expired signed URL, deleted upload) must not leave + // an empty disc — fall back to the same initials the no-picture case shows, so + // every surface rendering this identity agrees on what it draws. + const [srcFailed, setSrcFailed] = useState(false); + useEffect(() => setSrcFailed(false), [src]); + const showImage = Boolean(src) && !srcFailed; + const classes = [ "sui-avatar", `sui-avatar--${size}`, @@ -53,11 +63,16 @@ export function Avatar({ .filter(Boolean) .join(" "); - const content = src ? ( - {ariaLabel + const content = showImage ? ( + {ariaLabel setSrcFailed(true)} + /> ) : ( - {initialsOf(name)} + {avatarInitials(name)} ); diff --git a/frontend/editor/src/portal-saas/hooks/useFreeCreditsSummary.ts b/frontend/editor/src/portal-saas/hooks/useFreeCreditsSummary.ts new file mode 100644 index 0000000000..8f7a16ca90 --- /dev/null +++ b/frontend/editor/src/portal-saas/hooks/useFreeCreditsSummary.ts @@ -0,0 +1,7 @@ +/** + * SaaS has no link concept — the signed-in account IS the SaaS account, and the + * editor's cloud wallet hook is already in this build's {@code @app/*} cascade. + * Delegating to it means the processor footer and the editor footer share one + * wallet fetch and can't disagree, so there is nothing portal-specific to do. + */ +export { useFreeCreditsSummary } from "@app/hooks/useFreeCreditsSummary"; diff --git a/frontend/editor/src/portal-saas/hooks/useOpenPlan.ts b/frontend/editor/src/portal-saas/hooks/useOpenPlan.ts new file mode 100644 index 0000000000..ce12e843b3 --- /dev/null +++ b/frontend/editor/src/portal-saas/hooks/useOpenPlan.ts @@ -0,0 +1,11 @@ +import { useCallback } from "react"; +import { useUI } from "@portal/contexts/UIContext"; + +/** + * SaaS processor: the settings modal it hosts carries the same Plan section the + * editor opens, so the footer's credits row lands both apps in one place. + */ +export function useOpenPlan(): (() => void) | null { + const { openSettings } = useUI(); + return useCallback(() => openSettings("plan"), [openSettings]); +} diff --git a/frontend/editor/src/portal/components/Sidebar.css b/frontend/editor/src/portal/components/Sidebar.css index dfb533f8ab..48b54b177d 100644 --- a/frontend/editor/src/portal/components/Sidebar.css +++ b/frontend/editor/src/portal/components/Sidebar.css @@ -123,8 +123,6 @@ } .portal-sidebar[data-collapsed] .portal-sidebar__footer { margin-inline: 0.375rem; - padding-inline: 0; - align-items: center; } .portal-sidebar__logo { @@ -179,10 +177,8 @@ gap: 0.125rem; } +/* The shared brings its own boxes, padding and gap; the sidebar + only positions it. */ .portal-sidebar__footer { margin: 0 0.625rem 0.75rem; - padding: 0.5rem 0.375rem; - display: flex; - flex-direction: column; - gap: 0.5rem; } diff --git a/frontend/editor/src/portal/components/Sidebar.tsx b/frontend/editor/src/portal/components/Sidebar.tsx index ebf91c636e..8ce7008d67 100644 --- a/frontend/editor/src/portal/components/Sidebar.tsx +++ b/frontend/editor/src/portal/components/Sidebar.tsx @@ -2,6 +2,10 @@ import { useMediaQuery } from "@mantine/hooks"; import { Tooltip } from "@mantine/core"; import { ActionIcon, NavItem, NavSurface } from "@app/ui"; import { BrandSwitcher } from "@app/components/shared/BrandSwitcher"; +import { NavFooter } from "@app/components/shared/navFooter/NavFooter"; +import { useAccountIdentity } from "@app/hooks/useAccountIdentity"; +import { useFreeCreditsSummary } from "@portal/hooks/useFreeCreditsSummary"; +import { useOpenPlan } from "@portal/hooks/useOpenPlan"; import { SidebarToggleIcon } from "@app/components/shared/SidebarToggleIcon"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -10,7 +14,7 @@ import { useUI } from "@portal/contexts/UIContext"; import { LinkAccountFooterItem } from "@portal/components/LinkAccountFooterItem"; import { EDITOR_URL, EDITOR_IS_SAME_APP } from "@portal/auth/editorUrl"; import { EDITOR_BASENAME } from "@app/routes/editorBasename"; -import { CloseIcon, SettingsIcon } from "@portal/components/icons"; +import { CloseIcon } from "@portal/components/icons"; import { GROUP_PROCESSOR, GROUP_PLATFORM, @@ -41,6 +45,9 @@ export function Sidebar() { const isMobile = useMediaQuery(MOBILE_QUERY, false, { getInitialValueInEffect: false, }); + const { displayName, profilePictureUrl } = useAccountIdentity(); + const credits = useFreeCreditsSummary(); + const openPlan = useOpenPlan(); // Collapse is a desktop-only affordance: on mobile the sidebar is an // off-canvas drawer, so the icon-rail state never applies there. @@ -146,15 +153,17 @@ export function Sidebar() { ))} - - - } - onClick={() => openSettings()} - /> - + } + collapsed={collapsed} + /> ); } diff --git a/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx b/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx index c7ab0e8704..6ea4d8d336 100644 --- a/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx +++ b/frontend/editor/src/portal/components/billing/PrepaidCapacityCard.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { Button, Card } from "@app/ui"; -import { formatPeriodDate, MeterBar, meterState } from "@app/billing"; +import { formatPeriodDate, MeterBar, remainingMeter } from "@app/billing"; import type { Wallet } from "@portal/api/billing"; /** @@ -10,8 +10,8 @@ import type { Wallet } from "@portal/api/billing"; * - No bundle → a slim "Get 12 months for the price of 10" offer nudge with a * "Review offer" CTA (the demo's commit-nudge card), shown only when a buyer * ({@code onBuy}, leader) is present. - * - Bundle held → the capacity meter (fills as the pool is drawn down, so it - * warns as capacity runs low) plus a "Top up" action for the leader. + * - Bundle held → the capacity meter (drains towards empty as the pool is drawn + * down, so it warns as capacity runs low) plus a "Top up" action for the leader. * * Prepaid is consumed before metered billing and sits outside the spend limit, so * it reads as its own dimension. Buying/topping up opens {@code BundleCheckoutModal} @@ -55,8 +55,7 @@ export function PrepaidCapacityCard({ const remaining = wallet.prepaidUnitsRemaining; const total = wallet.prepaidUnitsTotal; - const used = Math.max(0, total - remaining); - const { state, pct } = meterState(used, total); + const { state, pct } = remainingMeter(remaining, total); const stateLabel = state === "DEGRADED" ? t("portal.billing.prepaid.state.exhausted", "Used up") diff --git a/frontend/editor/src/portal/components/billing/WalletMeter.tsx b/frontend/editor/src/portal/components/billing/WalletMeter.tsx index c8be188390..9558960e89 100644 --- a/frontend/editor/src/portal/components/billing/WalletMeter.tsx +++ b/frontend/editor/src/portal/components/billing/WalletMeter.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Card } from "@app/ui"; -import { formatMinor, MeterBar, meterState } from "@app/billing"; +import { formatMinor, MeterBar, remainingMeter } from "@app/billing"; import type { Wallet } from "@portal/api/billing"; import type { LocalUsage } from "@portal/api/link"; @@ -15,8 +15,10 @@ interface Props { } /** - * The free Processor-trial meter — "X / N free PDFs used" against the one-time - * grant. Uses the shared {@link MeterBar} (same `paygf-meter` structure as the + * The free Processor-trial meter — "X of N free PDFs left" against the one-time + * grant, with what has been used alongside as the status badge. The bar shows what + * is left, so it drains towards empty as the grant is spent. + * Uses the shared {@link MeterBar} (same `paygf-meter` structure as the * cloud plan page). The subscribed spend-vs-cap meter is a separate surface * ({@code SpendLimitCard}); this card is only the free face. * @@ -30,7 +32,7 @@ export function WalletMeter({ wallet, unsynced, action }: Props) { const pending = unsynced?.totalUnsyncedUnits ?? 0; const used = wallet.billableUsed + pending; const remaining = Math.max(0, wallet.freeRemaining - pending); - const { state, pct } = meterState(used, wallet.freeAllowance); + const { state, pct } = remainingMeter(remaining, wallet.freeAllowance); const rate = wallet.pricePerDocMinor != null && wallet.pricePerDocMinor > 0 ? wallet.pricePerDocMinor @@ -76,11 +78,14 @@ export function WalletMeter({ wallet, unsynced, action }: Props) {
    diff --git a/frontend/editor/src/portal/hooks/useFreeCreditsSummary.test.tsx b/frontend/editor/src/portal/hooks/useFreeCreditsSummary.test.tsx new file mode 100644 index 0000000000..fb48250fed --- /dev/null +++ b/frontend/editor/src/portal/hooks/useFreeCreditsSummary.test.tsx @@ -0,0 +1,110 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { cleanup, render, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { LinkProvider, type LinkState } from "@portal/contexts/LinkContext"; +import { useFreeCreditsSummary } from "@portal/hooks/useFreeCreditsSummary"; + +const fetchWallet = vi.fn(); +vi.mock("@portal/api/billing", () => ({ + fetchWallet: () => fetchWallet(), +})); + +function Probe() { + const credits = useFreeCreditsSummary(); + return ( + + {credits ? `${credits.remaining}/${credits.total}` : "none"} + + ); +} + +function renderFor(initialState: LinkState) { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( + + + + + , + ).getByTestId("credits"); +} + +describe("useFreeCreditsSummary (self-hosted) — wallet behind the link gate", () => { + beforeEach(() => { + // The figures persist across mounts now, so isolate the suite from itself. + localStorage.clear(); + fetchWallet.mockReset(); + fetchWallet.mockResolvedValue({ + status: "free", + freeRemaining: 247, + freeAllowance: 500, + }); + }); + + it("unlinked reads no wallet at all", async () => { + const el = renderFor("unlinked"); + await waitFor(() => expect(el.textContent).toBe("none")); + expect(fetchWallet).not.toHaveBeenCalled(); + }); + + it("linked surfaces the free grant", async () => { + const el = renderFor("linked-free"); + await waitFor(() => expect(el.textContent).toBe("247/500")); + }); + + it("hides the meter once the team subscribes", async () => { + // The grant is a lifetime pool that survives subscribing, so a paying team + // would otherwise sit on a spent meter forever. + fetchWallet.mockResolvedValue({ + status: "subscribed", + freeRemaining: 0, + freeAllowance: 500, + }); + const el = renderFor("linked-subscribed"); + // The row holds its space while the wallet loads, then drops once the + // answer says this team is paying. + await waitFor(() => expect(el.textContent).toBe("none")); + }); + + it("hides the meter when the wallet read fails", async () => { + fetchWallet.mockRejectedValue(new Error("saas unreachable")); + const el = renderFor("linked-subscribed"); + await waitFor(() => expect(el.textContent).toBe("none")); + }); + + it("ignores cached figures once the instance is unlinked", async () => { + // The cache survives an unlink and nothing rewrites it afterwards, so the + // linkage gate has to cover the seed too, not just the fetch. + const linked = renderFor("linked-free"); + await waitFor(() => expect(linked.textContent).toBe("247/500")); + cleanup(); + + fetchWallet.mockClear(); + const unlinked = renderFor("unlinked"); + expect(unlinked.textContent).toBe("none"); + expect(fetchWallet).not.toHaveBeenCalled(); + }); + + it("shows the last known figures while the wallet reloads", async () => { + // What stops the row popping in — and resizing the footer — every time the + // processor mounts. + const el = renderFor("linked-free"); + await waitFor(() => expect(el.textContent).toBe("247/500")); + cleanup(); + + let release: (v: unknown) => void = () => {}; + fetchWallet.mockReturnValue( + new Promise((resolve) => { + release = resolve; + }), + ); + const second = renderFor("linked-free"); + // Seeded before the refetch lands... + expect(second.textContent).toBe("247/500"); + release({ status: "free", freeRemaining: 12, freeAllowance: 500 }); + // ...then updated in place, without the row ever being absent. + await waitFor(() => expect(second.textContent).toBe("12/500")); + }); +}); diff --git a/frontend/editor/src/portal/hooks/useFreeCreditsSummary.ts b/frontend/editor/src/portal/hooks/useFreeCreditsSummary.ts new file mode 100644 index 0000000000..3fd814ebb1 --- /dev/null +++ b/frontend/editor/src/portal/hooks/useFreeCreditsSummary.ts @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useLink } from "@portal/contexts/LinkContext"; +import { fetchWallet } from "@portal/api/billing"; +import { qk } from "@portal/queries/keys"; +import { + readCachedCredits, + writeCachedCredits, + type CachedCredits, +} from "@app/services/navFooterCache"; +import { type NavFooterCredits } from "@app/components/shared/navFooter/NavFooterCreditsRow"; + +/** + * Free credits left on this team's allowance, for the processor's sidebar + * footer meter. Null hides the meter. + * + * This is the portal's own seam rather than the editor's {@code + * @app/hooks/useFreeCreditsSummary}, because self-hosted resolves {@code @app/*} + * as proprietary → core: the cloud wallet hook isn't in that cascade, and the + * implementation can't move down into proprietary either, since core/desktop + * builds ship no portal and must never resolve {@code @portal}. Keeping it here + * means only builds that actually have a processor pull in the wallet read. + * + * Self-hosted reads the same {@code GET /api/v1/payg/wallet} the Usage page's + * trial meter renders — {@code apiClient.saas} with the admin's Supabase JWT, + * since the wallet lives in the cloud even when the instance doesn't. Gated on + * linkage: an unlinked instance has no wallet to read. + * + * Free teams only, matching the editor and the Plan page. The grant is a + * lifetime pool that survives subscribing, so a paying team would otherwise sit + * on a permanent "0 of 500" in red; their usage lives on Usage & Billing. + */ +export function useFreeCreditsSummary(): NavFooterCredits | null { + const { isLinked } = useLink(); + // Shared query key, so the footer rides the same cached snapshot as any other + // wallet reader rather than adding a fetch per mount. + const { data: wallet } = useQuery({ + queryKey: qk.wallet(isLinked), + queryFn: fetchWallet, + enabled: isLinked, + }); + // Shared with the editor's seam, so crossing between the two apps shows the + // figures the other one last saw rather than re-fetching into an empty row. + const [seed] = useState(readCachedCredits); + + const live: CachedCredits | undefined = !wallet + ? undefined + : wallet.status === "subscribed" + ? null + : { remaining: wallet.freeRemaining, total: wallet.freeAllowance }; + + useEffect(() => { + // Only once linked: an unlinked instance never asks, so it has no answer of + // its own and must not overwrite what the editor recorded. + if (isLinked && live !== undefined) writeCachedCredits(live); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wallet, isLinked]); + + // Linkage gates the seed as well as the fetch. The cache outlives an unlink + // — nothing refetches or rewrites it once the instance stops asking — so + // without this an unlinked instance would keep showing the figures from when + // it was linked, indefinitely. + if (!isLinked) return null; + return (live !== undefined ? live : seed) ?? null; +} diff --git a/frontend/editor/src/portal/hooks/useOpenPlan.ts b/frontend/editor/src/portal/hooks/useOpenPlan.ts new file mode 100644 index 0000000000..57b30c91d0 --- /dev/null +++ b/frontend/editor/src/portal/hooks/useOpenPlan.ts @@ -0,0 +1,13 @@ +import { useCallback } from "react"; +import { useView } from "@portal/contexts/ViewContext"; + +/** + * Self-hosted processor: settings carries no Plan section (it is a cloud + * surface, and this build's registry has none), so the footer's credits row + * opens the portal's own Usage & Billing view instead — the same figures, on + * the surface this flavor actually owns. + */ +export function useOpenPlan(): (() => void) | null { + const { setActiveView } = useView(); + return useCallback(() => setActiveView("usage"), [setActiveView]); +} diff --git a/frontend/editor/src/portal/queries/keys.ts b/frontend/editor/src/portal/queries/keys.ts index 6c29430cb4..e1c46a59c0 100644 --- a/frontend/editor/src/portal/queries/keys.ts +++ b/frontend/editor/src/portal/queries/keys.ts @@ -20,6 +20,8 @@ export const qk = { // Keyed on linkage: an unlinked account has no deal to read, so linking must not // serve the unlinked (null) snapshot back from cache. procurement: (linked: boolean) => ["portal", "procurement", linked] as const, + // Same reasoning: an unlinked instance has no wallet in the cloud. + wallet: (linked: boolean) => ["portal", "wallet", linked] as const, // Tier-dependent documents: (tier: Tier) => ["portal", "documents", tier] as const, diff --git a/frontend/editor/src/proprietary/billing/format.ts b/frontend/editor/src/proprietary/billing/format.ts index f91f98af44..ff44e69882 100644 --- a/frontend/editor/src/proprietary/billing/format.ts +++ b/frontend/editor/src/proprietary/billing/format.ts @@ -293,6 +293,29 @@ export function computeBundleQuote( export type MeterState = "FULL" | "WARNED" | "DEGRADED"; +/** + * Meter for a balance that is spent DOWN — a free grant, a prepaid pool. The + * bar shows what is LEFT, so full reads as "plenty" and empty as "none", which + * is how the sidebar footer's credits row reads and the only direction that + * matches a figure quoting the remainder. + * + * The state bands still key on consumption, so the tone is unchanged: amber + * once 80% is gone, red once it's exhausted. Meters for money SPENT against a + * cap keep using {@link meterState} directly — there a full bar correctly means + * "at your ceiling". + */ +export function remainingMeter( + remaining: number, + total: number, +): { state: MeterState; pct: number } { + const { state } = meterState(Math.max(0, total - remaining), total); + const pct = + total > 0 + ? Math.min(100, Math.max(0, (Math.max(0, remaining) / total) * 100)) + : 0; + return { state, pct }; +} + /** Warn (≥80%) / degrade (≥100%) band for a usage meter; mirrors the BE thresholds. */ export function meterState( used: number, diff --git a/frontend/editor/src/proprietary/billing/index.ts b/frontend/editor/src/proprietary/billing/index.ts index 687adec541..2fe9dffd93 100644 --- a/frontend/editor/src/proprietary/billing/index.ts +++ b/frontend/editor/src/proprietary/billing/index.ts @@ -14,6 +14,7 @@ export { docCapForMoney, formatPeriodDate, meterState, + remainingMeter, PREPAID_MONTHS_GRANTED, PREPAID_MONTHS_PAID, PDFS_PER_USER_MONTH, diff --git a/frontend/editor/src/proprietary/components/shared/AppSwitcher.tsx b/frontend/editor/src/proprietary/components/shared/AppSwitcher.tsx index 2e02db3068..9ba0b6438d 100644 --- a/frontend/editor/src/proprietary/components/shared/AppSwitcher.tsx +++ b/frontend/editor/src/proprietary/components/shared/AppSwitcher.tsx @@ -1,15 +1,21 @@ -import { useNavigate } from "react-router-dom"; -import { useAuth } from "@app/auth/context"; import { Logo } from "@app/ui/Logo"; import { BrandSwitcher } from "@app/components/shared/BrandSwitcher"; import { type AppSwitcherProps } from "@core/components/shared/AppSwitcher"; -import { PORTAL_BASENAME } from "@app/routes/portalBasename"; +import { useOtherAppSwitch } from "@app/hooks/useOtherAppSwitch"; +/** + * Sidebar brand header for builds that ship the processor. When this user can + * open it, the Stirling logo doubles as the editor⇄processor switcher: the mark + * morphs into a chevron and opens the switch menu (the same BrandSwitcher the + * processor sidebar uses). Users without access get a plain logo. + * + * The access gate lives in {@link useOtherAppSwitch} so this header and the + * sidebar footer's "Open PDF Processor" row are driven by one answer. + */ export function AppSwitcher({ collapsed }: AppSwitcherProps) { - const { portalAccess } = useAuth(); - const navigate = useNavigate(); + const otherApp = useOtherAppSwitch(); - if (!portalAccess) { + if (!otherApp) { return ( navigate(PORTAL_BASENAME)} + onSwitch={otherApp.onOpen} collapsed={collapsed} /> ); diff --git a/frontend/editor/src/proprietary/hooks/useOtherAppSwitch.ts b/frontend/editor/src/proprietary/hooks/useOtherAppSwitch.ts new file mode 100644 index 0000000000..8bf07b5c2f --- /dev/null +++ b/frontend/editor/src/proprietary/hooks/useOtherAppSwitch.ts @@ -0,0 +1,15 @@ +import { useNavigate } from "react-router-dom"; +import { useAuth } from "@app/auth/context"; +import { PORTAL_BASENAME } from "@app/routes/portalBasename"; +import { type NavFooterAppLink } from "@app/components/shared/navFooter/NavFooter"; + +/** + * Self-hosted: the Spring session carries `portalAccess`, so the switch to the + * processor is offered exactly when that flag is set. + */ +export function useOtherAppSwitch(): NavFooterAppLink | null { + const { portalAccess } = useAuth(); + const navigate = useNavigate(); + if (!portalAccess) return null; + return { app: "processor", onOpen: () => navigate(PORTAL_BASENAME) }; +} diff --git a/frontend/editor/src/saas/components/shared/AppSwitcher.tsx b/frontend/editor/src/saas/components/shared/AppSwitcher.tsx deleted file mode 100644 index 364f094478..0000000000 --- a/frontend/editor/src/saas/components/shared/AppSwitcher.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { Logo } from "@app/ui/Logo"; -import { BrandSwitcher } from "@app/components/shared/BrandSwitcher"; -import { type AppSwitcherProps } from "@core/components/shared/AppSwitcher"; -import { usePortalAccess } from "@app/hooks/usePortalAccess"; -import { PORTAL_BASENAME } from "@app/routes/portalBasename"; - -/** - * SaaS sidebar brand header. When the backend says this user can open the - * processor (`/api/v1/auth/me` → `portalAccess` — the exact signal the - * processor's own gate uses), the Stirling logo doubles as the - * editor⇄processor switcher: the mark morphs into a chevron and opens the - * switch menu (same BrandSwitcher the processor sidebar uses). Users without - * access get a plain logo. - * - * Deliberately NOT gated on the editor's Supabase auth context: that context - * never fetches /me, so it can't know about portal access (and its session - * state doesn't always mirror the backend login that actually grants it). - */ -export function AppSwitcher({ collapsed }: AppSwitcherProps) { - const portalAccess = usePortalAccess(); - const navigate = useNavigate(); - - if (!portalAccess) { - return ( - - ); - } - - return ( - navigate(PORTAL_BASENAME)} - collapsed={collapsed} - /> - ); -} diff --git a/frontend/editor/src/saas/components/shared/config/configSections/Overview.tsx b/frontend/editor/src/saas/components/shared/config/configSections/Overview.tsx index fac87148fe..3a28152425 100644 --- a/frontend/editor/src/saas/components/shared/config/configSections/Overview.tsx +++ b/frontend/editor/src/saas/components/shared/config/configSections/Overview.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react"; import { Alert, - Avatar, Divider, Group, Image, @@ -11,10 +10,12 @@ import { TextInput, Modal, } from "@mantine/core"; +import { Avatar } from "@app/ui/Avatar"; import { Button as DSButton } from "@app/ui/Button"; import { FilePicker } from "@app/ui/FilePicker"; import { useTranslation } from "react-i18next"; import { useAuth } from "@app/auth/UseSession"; +import { useAccountIdentity } from "@app/hooks/useAccountIdentity"; import { isUserAnonymous, linkEmailIdentity, @@ -46,6 +47,8 @@ const Overview: React.FC = ({ onLogoutClick }) => { refreshProfilePicture, refreshProfilePictureMetadata, } = useAuth(); + // Same name + initials the sidebar footer draws, so the two discs agree. + const { displayName } = useAccountIdentity(); const PROFILE_BUCKET = "profile-pictures"; @@ -67,7 +70,6 @@ const Overview: React.FC = ({ onLogoutClick }) => { const provider = profilePictureMetadata?.provider; const profilePath = user ? `${user.id}/avatar` : null; - const profileInitial = user?.email?.trim()?.charAt(0)?.toUpperCase() || "U"; const handleProfileUpload = async (file: File | null) => { if (!file || !user || !profilePath) { @@ -410,12 +412,9 @@ const Overview: React.FC = ({ onLogoutClick }) => { - {profileInitial} - + name={displayName} + size="xl" + />
    = ({ onLogoutClick }) => { - {profileInitial} - + name={displayName} + size="xl" + />
    navigate(PORTAL_BASENAME) }; +} diff --git a/frontend/editor/src/saas/hooks/usePortalAccess.test.tsx b/frontend/editor/src/saas/hooks/usePortalAccess.test.tsx index a0e8ba618d..809138850a 100644 --- a/frontend/editor/src/saas/hooks/usePortalAccess.test.tsx +++ b/frontend/editor/src/saas/hooks/usePortalAccess.test.tsx @@ -1,5 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { renderHook, waitFor } from "@testing-library/react"; +import { renderHook as baseRenderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ReactNode } from "react"; const get = vi.fn(); let currentUserId: string | null = null; @@ -20,10 +22,28 @@ function meReturning(portalAccess: boolean) { return { data: { user: { portalAccess } } }; } +// A fresh client per render, so one test's cached answer can't satisfy the +// next — each case exercises a cold cache unless it deliberately shares one. +let client: QueryClient; + +function renderHook(cb: () => T) { + return baseRenderHook(cb, { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); +} + describe("usePortalAccess", () => { beforeEach(() => { + // The hook now remembers the last answer across mounts, so without this a + // prior test's result seeds the next one. + localStorage.clear(); get.mockReset(); currentUserId = null; + client = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: 0, staleTime: 0 } }, + }); }); it("reports the backend's answer for the signed-in user", async () => { @@ -82,12 +102,31 @@ describe("usePortalAccess", () => { expect(first.result.current).toBe(false); first.unmount(); - // The failure isn't sticky. + // The failure isn't sticky — a cold cache asks again. + client.clear(); get.mockResolvedValue(meReturning(true)); const second = renderHook(() => usePortalAccess()); await waitFor(() => expect(second.result.current).toBe(true)); }); + it("shows the last known answer at first paint, then revalidates", async () => { + // What stops the switcher and the footer's "Open ..." row popping in a + // request late on every mount. + currentUserId = "admin-1"; + get.mockResolvedValue(meReturning(true)); + const first = renderHook(() => usePortalAccess()); + await waitFor(() => expect(first.result.current).toBe(true)); + first.unmount(); + + client.clear(); + get.mockResolvedValue(meReturning(false)); + const second = renderHook(() => usePortalAccess()); + // Seeded from the remembered answer before the request lands... + expect(second.result.current).toBe(true); + // ...and corrected once the backend disagrees. + await waitFor(() => expect(second.result.current).toBe(false)); + }); + it("ignores a response that lands after unmount", async () => { currentUserId = "admin-1"; let resolveMe: (v: unknown) => void = () => {}; diff --git a/frontend/editor/src/saas/hooks/usePortalAccess.ts b/frontend/editor/src/saas/hooks/usePortalAccess.ts index 442061cbe1..6e91f0864c 100644 --- a/frontend/editor/src/saas/hooks/usePortalAccess.ts +++ b/frontend/editor/src/saas/hooks/usePortalAccess.ts @@ -1,52 +1,64 @@ import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import apiClient from "@app/services/apiClient"; import { useAuth } from "@app/auth/UseSession"; +import { + readCachedOtherApp, + writeCachedOtherApp, +} from "@app/services/navFooterCache"; +import { qk } from "@app/query/keys"; + +async function fetchPortalAccess(): Promise { + const res = await apiClient.get<{ user?: { portalAccess?: boolean } }>( + "/api/v1/auth/me", + ); + return res.data.user?.portalAccess === true; +} /** * Whether the current user can open the processor (admin portal), straight * from the backend (`/api/v1/auth/me` → `portalAccess`) — the same signal the * processor's own SaasPortalGate uses. Components that must mirror processor - * access (e.g. the sidebar's editor⇄processor switcher) ask here. + * access (the sidebar's editor⇄processor switcher and its footer row) ask here. * * The editor's Supabase auth context can't *answer* this — it never fetches - * /me — so it is used only to identify who is asking. Keying the effect on - * that identity is what keeps the answer per-user: the SPA can swap users + * /me — so it is used only to identify who is asking. That identity is the + * cache key, which is what keeps the answer per-user: the SPA can swap users * without a reload (Supabase fires SIGNED_OUT/SIGNED_IN in place; only the - * settings Logout button hard-navigates), so any answer held beyond the - * current identity would leak to whoever signs in next. + * settings Logout button hard-navigates), and a keyed cache addresses each + * identity separately rather than holding one answer that would have to be + * invalidated on the swap — the bug class this hook once had. * - * Deliberately unmemoised beyond the mount: the one consumer (the sidebar - * switcher) mounts once, so a cross-mount cache would only add user-scoped - * state that has to be invalidated on identity change — the bug class this - * hook already had once. Guests skip the request entirely. + * Cached through the app query client, so leaving the editor for the processor + * and coming back resolves from cache: the switcher is there on first paint + * instead of appearing a request later. Guests skip the request entirely. */ export function usePortalAccess(): boolean { const { user } = useAuth(); const userId = user?.id ?? null; - const [access, setAccess] = useState(false); + // The query cache is per-tree and per-load, so it can't help a cold start or + // the hop into the processor, which mounts its own client. Seed from the last + // answer this browser saw so the switcher and the footer's "Open ..." row are + // there at first paint. Marked ancient so it still revalidates immediately. + const [seed] = useState(readCachedOtherApp); + + const { data, isSuccess } = useQuery({ + queryKey: qk.portalAccess(userId), + queryFn: fetchPortalAccess, + // Signed out: nothing to ask, and any previous answer is void. + enabled: userId !== null, + // Backend unreachable or guest (401) means no access now; a later refetch + // asks again rather than trusting the failure. + retry: false, + initialData: seed, + initialDataUpdatedAt: 0, + }); useEffect(() => { - // Signed out: nothing to ask, and any previous answer is void. - if (userId === null) { - setAccess(false); - return; - } + // Only a real answer is recorded — a failed probe is not one, so the next + // mount trusts the last backend response rather than a network blip. + if (isSuccess && data !== undefined) writeCachedOtherApp(data); + }, [isSuccess, data]); - let cancelled = false; - apiClient - .get<{ user?: { portalAccess?: boolean } }>("/api/v1/auth/me") - .then((res) => { - if (!cancelled) setAccess(res.data.user?.portalAccess === true); - }) - .catch(() => { - // Backend unreachable or guest (401): no access now; a remount or - // identity change asks again rather than trusting a failure. - if (!cancelled) setAccess(false); - }); - return () => { - cancelled = true; - }; - }, [userId]); - - return access; + return data === true; } diff --git a/frontend/editor/src/saas/hooks/useWallet.poll.test.tsx b/frontend/editor/src/saas/hooks/useWallet.poll.test.tsx new file mode 100644 index 0000000000..b72eac67f8 --- /dev/null +++ b/frontend/editor/src/saas/hooks/useWallet.poll.test.tsx @@ -0,0 +1,158 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { expectConsole } from "@app/tests/failOnConsole"; + +const get = vi.fn(); +vi.mock("@app/services/apiClient", () => ({ + default: { get: (...args: unknown[]) => get(...args) }, +})); +vi.mock("@app/hooks/walletDevPreview", () => ({ + getWalletDevPreview: () => null, +})); +vi.mock("@app/services/billing", () => ({ createPortalSession: vi.fn() })); +vi.mock("@app/platform/openExternal", () => ({ openExternal: vi.fn() })); + +const { useWallet } = await import("@app/hooks/useWallet"); + +/** Full enough for the hook's deep-compare, which reads every field. */ +function walletWith(freeRemaining: number) { + return { + data: { + teamId: 1, + status: "free", + role: "leader", + billingPeriodStart: "2026-08-01", + billingPeriodEnd: "2026-08-31", + billableUsed: 500 - freeRemaining, + billableLimit: 500, + freeAllowance: 500, + freeRemaining, + pricePerDocMinor: 2, + bundleRatePerCreditMinor: null, + currency: "usd", + estimatedBillMinor: 0, + capUsd: null, + noCap: false, + stripeSubscriptionId: null, + spendUnitsThisPeriod: 0, + docsProcessedThisPeriod: 0, + uniquePdfsThisPeriod: 0, + sizeMultiplierPdfsThisPeriod: 0, + billingMode: "metered", + prepaidUnitsRemaining: 0, + prepaidUnitsTotal: 0, + prepaidExpiresAt: null, + recent: [], + members: [], + categoryBreakdown: { api: 0, ai: 0, automation: 0 }, + categoryDocs: { api: 0, ai: 0, automation: 0 }, + }, + }; +} + +describe("useWallet — keeping the figures fresh", () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + get.mockReset(); + get.mockResolvedValue(walletWith(500)); + }); + afterEach(() => vi.useRealTimers()); + + it("re-reads the wallet on the poll interval", async () => { + const { result } = renderHook(() => useWallet()); + await waitFor(() => expect(result.current.wallet).not.toBeNull()); + expect(get).toHaveBeenCalledTimes(1); + + get.mockResolvedValue(walletWith(480)); + await act(async () => { + vi.advanceTimersByTime(30_000); + }); + + await waitFor(() => expect(result.current.wallet?.freeRemaining).toBe(480)); + }); + + it("polls silently, so consumers gating on loading/error don't flicker", async () => { + const { result } = renderHook(() => useWallet()); + await waitFor(() => expect(result.current.wallet).not.toBeNull()); + + // A poll that fails must leave the last good snapshot, and must not raise + // `error` — Plan swaps a working page for an alert on that. + get.mockRejectedValue(new Error("network blip")); + await act(async () => { + vi.advanceTimersByTime(30_000); + }); + + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeNull(); + expect(result.current.wallet?.freeRemaining).toBe(500); + }); + + it("settles loading when a silent poll supersedes an in-flight visible load", async () => { + // The mount load raises `loading`; a poll firing before it lands cancels it. + // If clearing the flag were the silent load's to skip, both would decline + // and `loading` would stay true forever — which permanently suppresses the + // limit modals, since they do `if (loading || !wallet) return null`. + const visibility = vi.spyOn(document, "visibilityState", "get"); + visibility.mockReturnValue("visible"); + + let landMount: (v: unknown) => void = () => {}; + get.mockReturnValueOnce( + new Promise((resolve) => { + landMount = resolve; + }), + ); + const { result } = renderHook(() => useWallet()); + expect(result.current.loading).toBe(true); + + get.mockResolvedValue(walletWith(470)); + await act(async () => { + document.dispatchEvent(new Event("visibilitychange")); + }); + await act(async () => { + landMount(walletWith(500)); + }); + + await waitFor(() => expect(result.current.wallet?.freeRemaining).toBe(470)); + expect(result.current.loading).toBe(false); + visibility.mockRestore(); + }); + + it("clears a stale error once a silent poll succeeds", async () => { + // The visible mount load failing is meant to be logged; only the silent + // retries stay quiet. + expectConsole.warn(/\[useWallet\] fetch failed/); + get.mockRejectedValueOnce(new Error("network blip")); + const { result } = renderHook(() => useWallet()); + await waitFor(() => expect(result.current.error).not.toBeNull()); + + get.mockResolvedValue(walletWith(500)); + await act(async () => { + vi.advanceTimersByTime(30_000); + }); + + await waitFor(() => expect(result.current.error).toBeNull()); + expect(result.current.wallet?.freeRemaining).toBe(500); + }); + + it("stops polling while the tab is hidden and re-reads on return", async () => { + const visibility = vi.spyOn(document, "visibilityState", "get"); + visibility.mockReturnValue("visible"); + const { result } = renderHook(() => useWallet()); + await waitFor(() => expect(result.current.wallet).not.toBeNull()); + const afterMount = get.mock.calls.length; + + visibility.mockReturnValue("hidden"); + await act(async () => { + document.dispatchEvent(new Event("visibilitychange")); + vi.advanceTimersByTime(120_000); + }); + expect(get).toHaveBeenCalledTimes(afterMount); + + visibility.mockReturnValue("visible"); + await act(async () => { + document.dispatchEvent(new Event("visibilitychange")); + }); + await waitFor(() => expect(get.mock.calls.length).toBe(afterMount + 1)); + visibility.mockRestore(); + }); +}); From ec3de16c0862c01190bf45896bae87e9f0e10ca7 Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 18 Aug 2026 20:25:55 +0200 Subject: [PATCH 149/154] ci: centralize Gradle caching across GitHub Actions workflows (#7546) ## Summary This pull request restructures Gradle dependency caching across the GitHub Actions workflows. The central `gradle-cache-prime` job is responsible for preparing the shared backend Gradle cache. Reusable workflows restore that shared cache without writing to the same key, while independently triggered workflows use isolated cache namespaces. ## What changed ### Shared Gradle cache - Added a stable `gradle-v1-` cache namespace for the shared backend cache. - The cache key includes the runner OS, runner architecture, JDK version, and the relevant Gradle configuration files. - The cache key is calculated before Gradle runs and reused for the later save step. - The prime job performs a lookup first and resolves backend dependencies only when the exact cache is missing. - This prevents Gradle or Spotless changes during the prime step from producing a different save key from the key used by downstream jobs. ### Reusable workflows - Backend, OpenAPI, license, Docker, E2E, and migration workflows restore the shared cache instead of writing to the shared key. - The backend build matrix includes `matrix.jdk-version` in its cache key. - Enterprise, Tauri, and generated-model workflows support the `use_shared_cache` boolean input. - When `use_shared_cache` is enabled, those workflows restore the shared cache. - When it is disabled, they use workflow-specific cache namespaces. ### Independent workflows Independent workflows now use separate cache prefixes, including: - `gradle-license-report-v1-` - `gradle-swagger-v1-` - `gradle-push-docker-v1-` - `gradle-tauri-releases-v1-` - `gradle-deploy-pr-v1-` - `gradle-playwright-e2e-v1-` - `gradle-generated-models-v1-` This prevents them from creating or affecting the shared backend cache before the prime job. ### Build and E2E flow - Removed the `-PnoSpotless` option from the central Gradle dependency-resolution command. - Removed the separate Gradle dependency prime/retry logic from the live E2E workflow. - Connected the Tauri build and generated-models check to the central cache-prime job. ## Motivation Previously, multiple workflows could use and save the same Gradle cache key independently. The first workflow to save the cache could therefore determine its contents, even if it had resolved a different or incomplete set of dependencies. The cache key was also evaluated after some Gradle tasks had run. If Gradle or Spotless modified a file covered by `hashFiles(...)`, the save key could differ from the restore key used by downstream jobs. This change gives the shared cache a single owner, isolates workflow-specific caches, and makes cache usage deterministic across the CI pipeline. ## Expected result - `gradle-cache-prime` is the single writer for the shared backend Gradle cache. - Downstream jobs restore the same cache without competing cache writes. - Independently triggered workflows remain isolated through their own cache namespaces. - Changes to the monitored Gradle configuration files produce a new cache key. - The normal Gradle/Spotless path is included when the shared cache is populated. ## Validation - Compared the cache key expressions and `hashFiles(...)` inputs across the affected workflows. - Verified that the central restore and save steps use the same precomputed key. - CI should confirm that the prime job populates the shared cache and downstream workflows only restore it. ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my changes - [ ] I have run the relevant CI checks - [ ] I have tested the workflow changes --- .../workflows/PR-Demo-Comment-with-react.yml | 19 +++--- .github/workflows/backend-build.yml | 21 +++---- .github/workflows/build-enterprise.yml | 33 ++++++++--- .github/workflows/build.yml | 59 ++++++++++++++----- .github/workflows/check-generated-models.yml | 30 +++++++--- .github/workflows/check-licence.yml | 19 +++--- .github/workflows/check-openapi.yml | 19 +++--- .github/workflows/coverage-aggregate.yml | 19 +++--- .github/workflows/db-migration-test.yml | 19 +++--- .github/workflows/docker-compose-tests.yml | 19 +++--- .github/workflows/e2e-live.yml | 38 ++++-------- .../frontend-backend-licenses-update.yml | 19 +++--- .github/workflows/multiOSReleases.yml | 57 ++++++++---------- .github/workflows/push-docker.yml | 19 +++--- .github/workflows/swagger.yml | 19 +++--- .github/workflows/tauri-build.yml | 33 +++++++---- .github/workflows/test-build-docker.yml | 19 +++--- 17 files changed, 235 insertions(+), 226 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index fe3f28a637..410aa82dc9 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -191,22 +191,19 @@ jobs: # untrusted tree gets built below - never leave credentials in .git/config persist-credentials: false - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-deploy-pr-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 54bd4cb907..6623940bce 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -35,23 +35,20 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK ${{ matrix.jdk-version }} uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: ${{ matrix.jdk-version }} distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Check Java formatting (Spotless) @@ -156,7 +153,7 @@ jobs: STIRLING_FLAVOR: ${{ matrix.flavor }} # Configure the Gradle daemon explicitly; GRADLE_OPTS alone only # configures the Gradle client JVM. - GRADLE_OPTS: '-Dorg.gradle.jvmargs=-Xmx4g -XX:+UseG1GC' + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -XX:+UseG1GC" - name: Check Test Reports Exist if: always() diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml index 0604f7176f..b4a8373ccc 100644 --- a/.github/workflows/build-enterprise.yml +++ b/.github/workflows/build-enterprise.yml @@ -15,6 +15,11 @@ name: Enterprise E2E (Playwright) on: workflow_call: + inputs: + use_shared_cache: + required: false + type: boolean + default: false push: branches: ["main"] schedule: @@ -56,21 +61,31 @@ jobs: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + + - name: Restore cache Gradle User Home + if: inputs.use_shared_cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - java-version: "25" - distribution: "temurin" - - name: Cache Gradle User Home + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Restore cache Gradle + if: inputs.use_shared_cache == false uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-playwright-e2e-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" + - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f50249099..566262d24f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,29 +73,48 @@ jobs: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + + - name: Calculate Gradle cache key + id: gradle-cache-key + shell: bash + run: | + echo "key=gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}" >> "$GITHUB_OUTPUT" + + - name: Cache Gradle (lookup-only) + id: cache-gradle-restore + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: ${{ steps.gradle-cache-key.outputs.key }} + lookup-only: true + + - name: Set up JDK 25 + if: steps.cache-gradle-restore.outputs.cache-hit != 'true' + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" + - name: Resolve backend dependencies - run: ./gradlew :stirling-pdf:classes -PnoSpotless --no-daemon + if: steps.cache-gradle-restore.outputs.cache-hit != 'true' + run: ./gradlew :stirling-pdf:classes --no-daemon env: STIRLING_FLAVOR: saas MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} + - name: Save cache Gradle User Home + if: steps.cache-gradle-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ steps.gradle-cache-key.outputs.key }} + build: needs: [files-changed, gradle-cache-prime] permissions: @@ -170,6 +189,8 @@ jobs: contents: read uses: ./.github/workflows/build-enterprise.yml secrets: inherit + with: + use_shared_cache: true check-licence: if: needs.files-changed.outputs.build == 'true' @@ -193,7 +214,14 @@ jobs: test-build-docker-images: if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true' - needs: [files-changed, build, check-generateOpenApiDocs, check-licence, gradle-cache-prime] + needs: + [ + files-changed, + build, + check-generateOpenApiDocs, + check-licence, + gradle-cache-prime, + ] permissions: contents: read packages: read @@ -205,7 +233,7 @@ jobs: tauri-build: if: needs.files-changed.outputs.tauri == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read pull-requests: write @@ -219,6 +247,7 @@ jobs: with: platform: windows-macos sign: true + use_shared_cache: true ai-engine: if: needs.files-changed.outputs.engine == 'true' @@ -242,6 +271,8 @@ jobs: pull-requests: write uses: ./.github/workflows/check-generated-models.yml secrets: inherit + with: + use_shared_cache: true pre-commit: needs: [files-changed] diff --git a/.github/workflows/check-generated-models.yml b/.github/workflows/check-generated-models.yml index 39c6467889..fafffcc241 100644 --- a/.github/workflows/check-generated-models.yml +++ b/.github/workflows/check-generated-models.yml @@ -9,6 +9,11 @@ name: Check generated models # post-merge safety net. on: workflow_call: + inputs: + use_shared_cache: + required: false + type: boolean + default: false push: branches: [main] @@ -39,22 +44,29 @@ jobs: engine/uv.lock cache-suffix: generated-models - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + - name: Restore cache Gradle User Home + if: inputs.use_shared_cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - java-version: "25" - distribution: "temurin" + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - - name: Cache Gradle User Home + - name: Restore cache Gradle + if: inputs.use_shared_cache == false uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-generated-models-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index 2eec970b8f..17c64d5c64 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -21,23 +21,20 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Check licenses for compatibility diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index f224ce18cf..ed83447335 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -22,23 +22,20 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Generate OpenAPI documentation diff --git a/.github/workflows/coverage-aggregate.yml b/.github/workflows/coverage-aggregate.yml index a97b579f15..61ef8793c4 100644 --- a/.github/workflows/coverage-aggregate.yml +++ b/.github/workflows/coverage-aggregate.yml @@ -40,23 +40,20 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index d6a61b45c4..ccb46d3988 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -25,23 +25,20 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: 25 distribution: temurin - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - # Keep the normal formatting path here so this smoke test exercises the # same Gradle configuration as the backend build. - name: Build Stirling-PDF JAR diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index 9d5911404f..039c73e5db 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -33,23 +33,20 @@ jobs: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - # When the PR changes the base image, test.sh builds it locally # (stirling-pdf-base:local) into the daemon image store. A buildx # container builder can't see that store, so skip it here and let diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 7bc95df05e..43d66dd1cf 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -21,39 +21,21 @@ jobs: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - # Gradle does not retry 429s, and a cold cache resolving the buildscript - # classpath is exactly where Maven Central rate-limits us. Retry it here, - # where a failure is cheap, instead of inside the backgrounded bootRun. - - name: Prime Gradle dependencies - env: - MAVEN_USER: ${{ secrets.MAVEN_USER }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} - run: | - for attempt in 1 2 3; do - if ./gradlew --quiet -PnoSpotless :stirling-pdf:classes; then - exit 0 - fi - echo "::warning::Gradle dependency resolution failed (attempt $attempt of 3)" - sleep $((attempt * 30)) - done - echo "::error::Gradle could not resolve dependencies after 3 attempts" - exit 1 + - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index 458766660f..7fd0136718 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -348,22 +348,19 @@ jobs: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-license-report-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index d9477f722f..0f0b2d3585 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -52,22 +52,19 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -145,22 +142,19 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Setup Node.js if: matrix.variant.build_frontend == true @@ -238,6 +232,14 @@ jobs: toolchain: stable targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + # x86_64 JDK is set up first so the aarch64 step below can leave its # JAVA_HOME as the active one. The macOS universal JRE build needs # jmods from both arches; the x64 path is captured into the env @@ -261,17 +263,6 @@ jobs: java-version: "25" distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index ec9d14822c..ea379cf7c6 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -58,22 +58,19 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-push-docker-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Set up Docker Buildx id: buildx diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 115de87d4e..1bfc94be5b 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -36,22 +36,19 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- + key: gradle-swagger-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" - name: Generate Swagger documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index ddf1104bac..e3f3122773 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -26,6 +26,10 @@ on: required: false type: boolean default: false + use_shared_cache: + required: false + type: boolean + default: false workflow_dispatch: inputs: platform: @@ -168,6 +172,24 @@ jobs: # Save the dependency cache even if a later step fails cache-on-failure: true + - name: Restore cache Gradle User Home + if: inputs.use_shared_cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Restore cache Gradle + if: inputs.use_shared_cache == false + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-tauri-build-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up x86_64 JDK 25 (macOS universal JRE) if: matrix.platform == 'macos-15' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 @@ -187,17 +209,6 @@ jobs: java-version: "25" distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Setup Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index 4a79cb3733..12d5a35a1f 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -79,23 +79,20 @@ jobs: docker system prune -af || true echo "Disk space after cleanup:" && df -h + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Build application From 6f7f28946c6643aecd96da043e9a1d1549193437 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:19:26 +0000 Subject: [PATCH 150/154] Set deployment: false on environment jobs that do not deploy (#7562) # Description of Changes thanks ludy for the tip :P --- ## 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/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .github/workflows/PR-Auto-Deploy-V2.yml | 5 ++++- .github/workflows/PR-Demo-cleanup.yml | 5 ++++- .github/workflows/backend-build.yml | 4 +++- .github/workflows/build-enterprise.yml | 8 ++++++-- .github/workflows/build.yml | 4 +++- .github/workflows/check-licence.yml | 4 +++- .github/workflows/check-openapi.yml | 4 +++- .github/workflows/db-migration-test.yml | 4 +++- .github/workflows/docker-compose-tests.yml | 4 +++- .github/workflows/e2e-live.yml | 4 +++- .github/workflows/frontend-backend-licenses-update.yml | 8 ++++++-- .github/workflows/multiOSReleases.yml | 8 ++++++-- .github/workflows/nightly.yml | 4 +++- .github/workflows/tauri-build.yml | 4 +++- .github/workflows/test-build-docker.yml | 4 +++- 15 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.github/workflows/PR-Auto-Deploy-V2.yml b/.github/workflows/PR-Auto-Deploy-V2.yml index 0f07aabbe5..4375b1b8b0 100644 --- a/.github/workflows/PR-Auto-Deploy-V2.yml +++ b/.github/workflows/PR-Auto-Deploy-V2.yml @@ -462,7 +462,10 @@ jobs: }); cleanup-v2-deployment: - environment: pr-preview + # Tearing a preview down is not a deployment - no deployment object. + environment: + name: pr-preview + deployment: false if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index 1407939994..098f8d7803 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -9,7 +9,10 @@ permissions: jobs: cleanup: - environment: pr-preview + # Tearing a preview down is not a deployment - no deployment object. + environment: + name: pr-preview + deployment: false if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 6623940bce..596be96e8c 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -20,7 +20,9 @@ permissions: jobs: build: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml index b4a8373ccc..194d86d9da 100644 --- a/.github/workflows/build-enterprise.yml +++ b/.github/workflows/build-enterprise.yml @@ -42,7 +42,9 @@ jobs: uses: ./.github/workflows/_runner-pick.yml playwright-e2e-enterprise: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false needs: pick # Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE, # so the suite can't boot premium and would fail. See the header comment. @@ -325,7 +327,9 @@ jobs: # Multi-node regression: builds + seeds the clustered stack (testing/compose/docker-compose-multinode.yml) # and runs behave features/multinode. Licence-gated, so it runs after the Playwright job (not in parallel). multinode-e2e: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false needs: [pick, playwright-e2e-enterprise] # Nightly cron + manual dispatch only (heavy build), fork-gated for the licence secret. if: >- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 566262d24f..1feaff2560 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,9 @@ jobs: filters: .github/config/.files.yaml gradle-cache-prime: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false name: Prime shared Gradle cache needs: [files-changed] runs-on: ubuntu-latest diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index 17c64d5c64..4e04a83656 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -10,7 +10,9 @@ permissions: jobs: check-licence: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest steps: - name: Harden Runner diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index ed83447335..bc9b302857 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -11,7 +11,9 @@ permissions: jobs: check-generate-openapi-docs: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest steps: - name: Harden Runner diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index ccb46d3988..785073944e 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -13,7 +13,9 @@ permissions: jobs: migration-test: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index 039c73e5db..439d4240b2 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -17,7 +17,9 @@ permissions: jobs: docker-compose-tests: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest permissions: actions: write diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 43d66dd1cf..844d26a3d1 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -11,7 +11,9 @@ permissions: jobs: playwright-e2e-live: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index 7fd0136718..9aef2316d2 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -43,7 +43,9 @@ jobs: generate-frontend-license-report: # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only. - environment: ci-bot + environment: + name: ci-bot + deployment: false if: needs.files-changed.outputs.licenses-frontend == 'true' name: Generate Frontend License Report needs: files-changed @@ -319,7 +321,9 @@ jobs: generate-backend-license-report: # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only. - environment: ci-bot + environment: + name: ci-bot + deployment: false if: needs.files-changed.outputs.licenses-backend == 'true' needs: files-changed name: Generate Backend License Report diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 0f0b2d3585..0ac94ffe68 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -38,7 +38,9 @@ permissions: jobs: determine-matrix: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest outputs: @@ -116,7 +118,9 @@ jobs: env: INPUT_PLATFORM: ${{ github.event.inputs.platform }} build-jars: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false needs: determine-matrix runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c92d17f027..65c25b7b66 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -127,7 +127,9 @@ jobs: # Runs the @nightly tag (conversion scenarios) plus a 10-shard concurrency run # of every other feature. cucumber-nightly: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false name: Cucumber (nightly scenarios + full concurrency) runs-on: ubuntu-latest # Fork pull requests get no MAVEN_* secrets, so the image build cannot work. diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index e3f3122773..0a82647690 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -63,7 +63,9 @@ jobs: determine-matrix: # Only probes APPLE_CERTIFICATE for presence, so it stays on the unrestricted # signing environment - release-signing would block every PR run. - environment: ci-signing + environment: + name: ci-signing + deployment: false if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index 12d5a35a1f..37cb7cb546 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -37,7 +37,9 @@ jobs: # spring-security=true matrix entry if `task backend:build` and # `task backend:build:ci` produce equivalent JARs (verify before wiring). test-build-docker-images: - environment: ci-unsigned + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest strategy: fail-fast: false From 0f8803f35f14cf8e9c191cfc5b6101a33586674c Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:27:39 +0000 Subject: [PATCH 151/154] Require the policy-management role to run a policy against its sources (#7565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Running a stored policy against its **configured sources** (`POST /api/v1/policies/{id}/trigger`, the manual "run now") now requires the policy-management role — global admin self-hosted, team leader on SaaS — alongside the existing team scoping. ## Why A source sweep operates on the team's configured sources using the server's stored connection credentials, so it belongs with the other policy-management capabilities rather than with ordinary use. Team scoping on its own didn't express that distinction. ## Not changed - `POST /{id}/run` — running a policy over documents the **caller supplied** stays open to every team member. That's ordinary editor enforcement on upload and export, and gating it would break it. - Ad-hoc pipelines (`/run`, `/run/stream`). - The scheduled, folder-watch and webhook triggers. - Single-user deployments (login disabled), which have no roles. ## Implementation `PolicyManagementAuthority` gains `canTriggerPolicies()`, kept separate from `canEditPolicies()` so the two capabilities can diverge later. Both current implementations grant it to the same principals that may edit policies. ## Tests - role absent → 403, rejected before any run starts - role present → 202 - login disabled → check skipped entirely - `/{id}/run` asserted to consult neither authority method, so the gate can't quietly extend to the editor path later --- .../AdminPolicyManagementAuthority.java | 5 ++ .../config/PolicyManagementAuthority.java | 11 +++ .../policy/controller/PolicyController.java | 30 +++++++- .../AdminPolicyManagementAuthorityTest.java | 12 +++ .../controller/PolicyControllerTest.java | 74 +++++++++++++++++++ .../TeamLeaderPolicyManagementAuthority.java | 5 ++ ...amLeaderPolicyManagementAuthorityTest.java | 12 +++ 7 files changed, 145 insertions(+), 4 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java index a49c8e5aa9..6d8229aa26 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java @@ -26,6 +26,11 @@ public class AdminPolicyManagementAuthority implements PolicyManagementAuthority return userService.isCurrentUserAdmin(); } + @Override + public boolean canTriggerPolicies() { + return userService.isCurrentUserAdmin(); + } + @Override public Long currentUserTeamId() { String username = userService.getCurrentUsername(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java index 0ea3c298ad..d7e4f50ad1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java @@ -12,6 +12,17 @@ public interface PolicyManagementAuthority { /** Whether the current user may create, edit, or delete policies (for their own team). */ boolean canEditPolicies(); + /** + * Whether the current user may run a policy against its configured sources (the manual + * "run now" sweep). Kept separate from {@link #canEditPolicies()} because the two are distinct + * capabilities, even where a deployment grants both to the same people: a sweep operates on the + * team's configured sources using the server's stored connection credentials, which makes it a + * policy-management capability rather than ordinary use. Running a policy over the caller's + * own uploaded files is not covered by this and stays open to every team member — that + * is ordinary editor enforcement. + */ + boolean canTriggerPolicies(); + /** * The team that scopes the current user's policies — the team a new policy is stamped with and * the only team whose policies the user may see/run/edit. {@code null} when it can't be diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java index 506bb75578..778a04e169 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java @@ -432,9 +432,10 @@ public class PolicyController { * admin gets no say on SaaS. Team scoping (which team's policies) is enforced separately by * {@link PolicyAccessGuard}. Every mutation routes through {@link #savePolicy} (pause/resume * re-save with a flipped {@code enabled} flag) or {@link #deletePolicy}, so gating those two - * covers them all; runs ({@code /run}) stay open to the team. Single-user deployments (login - * disabled) have no such role, so they trust the local operator. The path allowlist for folder - * sources/outputs is enforced separately by {@link PolicyValidator} at validation time. + * covers them all; runs over the caller's own files ({@code /{id}/run}) stay open to the team, + * while source sweeps are gated by {@link #requirePolicySweepAllowed}. Single-user deployments + * (login disabled) have no such role, so they trust the local operator. The path allowlist for + * folder sources/outputs is enforced separately by {@link PolicyValidator} at validation time. */ private void requirePolicyEditingAllowed() { if (!applicationProperties.getSecurity().isEnableLogin()) { @@ -447,6 +448,25 @@ public class PolicyController { } } + /** + * Sweeping a policy's configured sources requires the same role as managing policies: the sweep + * operates on the team's configured sources using the server's stored connection credentials, + * which makes it a policy-management capability rather than ordinary use, and team scoping on + * its own does not express that. Deliberately narrower than it looks: it gates only the sweep, + * not {@link #runStoredPolicy}, because running a policy over documents the caller supplied is + * ordinary editor enforcement that every member performs on upload and export. + */ + private void requirePolicySweepAllowed() { + if (!applicationProperties.getSecurity().isEnableLogin()) { + return; + } + if (!policyManagementAuthority.canTriggerPolicies()) { + throw new ResponseStatusException( + HttpStatus.FORBIDDEN, + "Not permitted to run this policy against its configured sources"); + } + } + @GetMapping @Operation( summary = "List policies", @@ -571,8 +591,10 @@ public class PolicyController { + " the enabled flag (which only gates automatic triggering). Returns" + " the ids of the runs started (poll the run-status endpoint for each)" + " plus what the sweep skipped - already-processed, parked-by-failure," - + " and in-flight counts - so an empty result explains itself.") + + " and in-flight counts - so an empty result explains itself. Requires" + + " the policy-management role.") public ResponseEntity trigger(@PathVariable String policyId) { + requirePolicySweepAllowed(); Policy policy = policyStore .get(policyId) diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java index 811aae6b4f..0f97fcaf62 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java @@ -39,6 +39,18 @@ class AdminPolicyManagementAuthorityTest { assertFalse(authority().canEditPolicies()); } + @Test + void adminMayTriggerPolicies() { + when(userService.isCurrentUserAdmin()).thenReturn(true); + assertTrue(authority().canTriggerPolicies()); + } + + @Test + void nonAdminMayNotTriggerPolicies() { + when(userService.isCurrentUserAdmin()).thenReturn(false); + assertFalse(authority().canTriggerPolicies()); + } + @Test void currentUserTeamIdResolvesFromTheCurrentUsersTeam() { Team team = new Team(); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java index 84e9998b90..2fa675597c 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java @@ -2,6 +2,7 @@ package stirling.software.proprietary.policy.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -738,5 +739,78 @@ class PolicyControllerTest { assertThat(((ResponseStatusException) e).getStatusCode()) .isEqualTo(HttpStatus.NOT_FOUND)); } + + @Test + @DisplayName("trigger is forbidden for a team member who cannot manage policies") + void triggerForbiddenForMember() { + // Sweeping a policy's configured sources is a policy-management capability, so being + // in the policy's team is not on its own enough to perform it. + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canTriggerPolicies()).thenReturn(false); + + assertThatThrownBy(() -> controller.trigger("a")) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN)); + // Rejected before the policy is looked up, so no run starts. + verify(policyRunner, never()).run(any()); + verify(policyStore, never()).get(any()); + } + + @Test + @DisplayName("trigger runs for a caller who may manage policies") + void triggerAllowedForLeader() { + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canTriggerPolicies()).thenReturn(true); + Policy p = policy("a", 1L); + when(policyStore.get("a")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + SweepOutcome outcome = new SweepOutcome(List.of("run-a"), 1, 0, 0, 0); + when(policyRunner.run(p)).thenReturn(outcome); + + ResponseEntity response = controller.trigger("a"); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + assertThat(response.getBody()).isEqualTo(outcome); + } + + @Test + @DisplayName("trigger skips the role check when login is disabled") + void triggerTrustsTheLocalOperator() { + // Single-user deployments have no roles at all; the gate must not lock them out of + // their + // own sweeps. + applicationProperties.getSecurity().setEnableLogin(false); + Policy p = policy("a", null); + when(policyStore.get("a")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + SweepOutcome outcome = new SweepOutcome(List.of("run-a"), 1, 0, 0, 0); + when(policyRunner.run(p)).thenReturn(outcome); + + assertThat(controller.trigger("a").getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + verify(policyManagementAuthority, never()).canTriggerPolicies(); + } + + @Test + @DisplayName("running a policy over the caller's own files stays open to any member") + void storedRunIsNotGatedByRole() { + // Editor enforcement: every member's upload/export runs the team's stored policies on + // their own documents. Gating this the way the sweep is gated would break the editor. + applicationProperties.getSecurity().setEnableLogin(true); + Policy p = policy("a", 1L); + when(policyStore.get("a")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + when(policyRunner.runWith(eq(p), any(), eq(PolicyProgressListener.NOOP))) + .thenReturn(handle("run-9")); + + ResponseEntity> response = + assertDoesNotThrow(() -> controller.runStoredPolicy("a", new PolicyRunFiles())); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + verify(policyManagementAuthority, never()).canTriggerPolicies(); + verify(policyManagementAuthority, never()).canEditPolicies(); + } } } diff --git a/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java b/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java index e2f5b65b47..0ce5e8e308 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java +++ b/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java @@ -25,6 +25,11 @@ public class TeamLeaderPolicyManagementAuthority implements PolicyManagementAuth return teamSecurity.isCurrentUserTeamLeader(); } + @Override + public boolean canTriggerPolicies() { + return teamSecurity.isCurrentUserTeamLeader(); + } + @Override public Long currentUserTeamId() { return teamSecurity.currentUserTeamId(); diff --git a/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java b/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java index 70cd360d7c..2c37980a5c 100644 --- a/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java +++ b/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java @@ -32,6 +32,18 @@ class TeamLeaderPolicyManagementAuthorityTest { assertFalse(authority().canEditPolicies()); } + @Test + void teamLeaderMayTriggerPolicies() { + when(teamSecurity.isCurrentUserTeamLeader()).thenReturn(true); + assertTrue(authority().canTriggerPolicies()); + } + + @Test + void nonLeaderMayNotTriggerPolicies() { + when(teamSecurity.isCurrentUserTeamLeader()).thenReturn(false); + assertFalse(authority().canTriggerPolicies()); + } + @Test void currentUserTeamIdDelegatesToTeamSecurity() { when(teamSecurity.currentUserTeamId()).thenReturn(9L); From 6bae9d516dc029869fee80ec5c2e92da89a5d541 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:46:00 +0000 Subject: [PATCH 152/154] chore(saas): one task per environment, and make the frontend follow it (#7483) ## The problem The `dev` profile hardcoded one project ref (`qacaivhsjtftfwtgjvva`) in five places: the ref, the Supabase URL, the publishable key, the datasource host and the meter endpoint. That made it both the shared environment everyone relies on *and* the only thing you could point the backend at. Testing an open SaaS PR meant hand-overriding all five via env just to reach that PR's Supabase preview branch, which is the only place the PR's migrations have actually been applied. Get it wrong and you see `relation "stirling_pdf." does not exist` for a table the PR added, which is what happened on [#7414](https://github.com/Stirling-Tools/Stirling-PDF/pull/7414). ## One task per environment ```bash task dev:saas # backend + frontend + engine, against this PR's preview branch task staging:saas # backend + frontend + engine, against the shared v3 project task backend:dev:saas # backend only, preview branch task backend:staging:saas # backend only, v3 ``` | | how | vars | project | |---|---|---|---| | prod | `PROFILES=none` | `SAAS_DB_*` | the live one | | staging | `PROFILES=staging` | `SAAS_STAGING_*` | pinned to v3, always there | | dev | `PROFILES=dev` | `SAAS_DEV_*` | follows a SaaS PR's preview branch | `PROFILES` is still the underlying switch, so the old spelling keeps working. Production deliberately has no named task: reaching it should take a conscious `PROFILES=none`, not a tab-complete. **staging** is the old `dev` configuration, moved and kept pinned. The value of a shared environment is that it is still there tomorrow: reproduce a bug, paste a link to a colleague, share data. **dev** is parameterised by `SAAS_DEV_PROJECT_REF` and derives the Supabase URL, JWT issuer, JWKS, meter endpoint and (unless overridden) the database host from it. Switching which PR you are testing is one variable instead of five. With no ref set, `task backend:dev:saas` stops and says what to set rather than falling back. ## The frontend was the real gap `frontend/editor/.env` is committed and pins the **production** Supabase project, and nothing in the frontend knew about dev or staging. So `task dev:saas` gave you a backend on a preview branch and a login against prod, unless you happened to have hand-written `frontend/editor/.env.saas.local`. The dev tasks now read the backend's env files and derive `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` from the same project ref the backend resolved, so the two halves cannot point at different projects. Nothing to keep in sync by hand, no new vite mode, and `SAAS_ENV=prod` opts back out to the committed values. ## Where to put your local values Two files, both gitignored, neither ever committed: **`app/.env.saas.local`** is the only one you normally need. The tasks load it for the backend *and* the frontend. ```bash # staging: everything else is already defaulted, so this is all it takes SAAS_STAGING_DB_PASSWORD=... # dev: the preview branch of the PR you are testing, from its "Supabase Preview" check. # A branch has its OWN password and API keys; the parent project's will not authenticate. SAAS_DEV_PROJECT_REF=... SAAS_DEV_DB_PASSWORD=... SAAS_DEV_PUBLISHABLE_KEY=... # prod, if you ever need it SAAS_DB_PROJECT_REF=... SAAS_DB_URL=... SAAS_DB_PASSWORD=... SUPABASE_EDGE_FUNCTION_SECRET=... ``` **`frontend/editor/.env.saas.local`** is no longer needed for choosing a Supabase project, and is best left empty or deleted. If you have one from before this PR, note that the task-supplied values now win, which is the point: the frontend follows the backend. **A blank is not the same as absent.** A dotenv line with an empty value still *sets* the variable, and Spring's `${VAR:default}` only falls back when a variable is absent. So `.env.saas` lists what you must set as blanks, and leaves out the two `*_DB_URL` overrides, which have real defaults to fall back to. This is not theoretical, see below. Committed `app/.env.saas` holds non-secret defaults only. Real secrets are passwords, the edge-function secret and service-role keys. Project refs and publishable keys are neither: a ref is the public `.supabase.co` subdomain and a publishable key ships in the browser bundle by design, which is why `frontend/editor/.env` has always carried prod's. ## Three bugs found while building the tasks All three were in this PR's own earlier commits, and all three were caught by actually booting things rather than by reading the config. **staging could not boot at all.** A blank `SAAS_STAGING_DB_URL=` in `.env.saas` set the variable to empty, so `${SAAS_STAGING_DB_URL:jdbc:...}` resolved to `""` and startup failed with `spring.datasource.url is required when the saas profile is active`. The file already carried a comment warning about exactly this; it had only been applied to the dev block. The original verification for this PR was "placeholders resolve" and "the task parses", neither of which boots anything. **The dev to staging fallback ran `ddl-auto=update` against shared v3.** The dev profile sets `update`, which is right for a disposable preview branch, and separately fell back to staging's project ref. Together that meant Hibernate was free to reconcile tables that RLS policies depend on. `application-staging.properties` pins `none`, but that only applies when the staging profile is the active one, which it was not on the fallback path. There is no fallback now: with no ref the task stops before gradle, and the frontend fails the same way, both naming the variable. **`PROFILES=` never selected production.** Go template `default` treats `""` as absent, so it silently resolved back to `dev`. It is `PROFILES=none` now. ## Two choices worth reviewing **Staging keeps its committed project ref**, now as a `${SAAS_STAGING_PROJECT_REF:...}` default in one place, with the URL, database host and meter endpoint all derived from it. So staging still works with zero setup, and repointing it is one variable. Nothing in CI referenced the ref or the profile. Its publishable key default carries no inline `gitleaks:allow`: a trailing comment in a `.properties` file is part of the value, so the pragma ended up inside the key. It is in `.gitleaksignore` instead. **`SAAS_DEV_DB_URL` still overrides the whole URL**, so a branch needing the pooler host rather than the direct one is reachable without touching committed config. ## Verification - `task backend:staging:saas` boots against v3 and serves `200`. It could not boot before this commit. - `task backend:dev:saas` with no ref stops before gradle naming the variable, and `PROFILES=none` still reaches production. `task frontend:dev:saas` fails the same way; `SAAS_ENV=staging` still resolves with no local config. - Frontend routing picks the SaaS runner for dev/staging and the plain runner for prod; the derivation returns the right URL and key for each. - Vite's `process.env` precedence and Task's dotenv/env semantics were measured, not assumed. That is how one trap surfaced: Task sets an `env:` key even when its value resolves to empty, and Vite treats an empty `process.env` `VITE_*` as authoritative over a committed `.env`. Putting the Supabase vars on the shared `dev:_run` would have blanked Supabase config for the core, proprietary and desktop dev servers, so the SaaS path has its own runner. - `:saas:spotlessApply` and `:saas:compileJava` green. `DevProfileProjectNotice` becomes `SaasProjectNotice` and covers both profiles, stating the project ref and `ddl-auto` at startup so which environment you are on is never a guess. No behaviour change for prod: the `saas` profile is untouched. --- .gitleaksignore | 5 ++ .taskfiles/backend.yml | 55 ++++++++++++-- .taskfiles/frontend.yml | 74 ++++++++++++++++--- Taskfile.yml | 21 +++++- app/.env.saas | 52 ++++++++----- .../saas/config/SaasProjectNotice.java | 53 +++++++++++++ .../main/resources/application-dev.properties | 38 ++++++---- .../resources/application-staging.properties | 39 ++++++++++ 8 files changed, 286 insertions(+), 51 deletions(-) create mode 100644 app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java create mode 100644 app/saas/src/main/resources/application-staging.properties diff --git a/.gitleaksignore b/.gitleaksignore index 12d98aebeb..c3917e985f 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -27,3 +27,8 @@ app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase. # Supabase publishable key (public by design, RLS-protected) used as a CI fallback # default in the tauri-build workflow when the GitHub secret is unset - not a real secret. .github/workflows/tauri-build.yml:generic-api-key:402 + +# Staging Supabase publishable key (public by design). Ignored here rather than with an +# inline gitleaks:allow because a trailing comment in a .properties file is part of the +# value, so the pragma would end up inside the key. +app/saas/src/main/resources/application-staging.properties:generic-api-key:16 diff --git a/.taskfiles/backend.yml b/.taskfiles/backend.yml index 63773f61fc..08a12b9535 100644 --- a/.taskfiles/backend.yml +++ b/.taskfiles/backend.yml @@ -57,16 +57,57 @@ tasks: - cmd: ./gradlew clean bootRun -PbuildWithFrontend=true platforms: [linux, darwin] + # SaaS backend. dev:saas -> the PR's preview branch, staging:saas -> shared v3, + # PROFILES=none -> production against your own SAAS_DB_*. Production has no named + # task on purpose. Use `none`, not an empty value: Go template `default` treats "" + # as absent and would resolve back to dev. + dev:saas: - desc: "Start backend in SaaS flavor against Supabase" - # `dotenv:` reads from the root Taskfile's directory (".") because this - # subtaskfile is included with `dir: .`. + desc: "Start SaaS backend against the current PR's Supabase preview branch" + dotenv: ['app/.env.saas.local', 'app/.env.saas'] + vars: + PROFILES: '{{.PROFILES | default "dev"}}' + cmds: + # Don't move this check into a `sh:` var: dotenv is visible in cmds but not + # during var evaluation, so the test would always see an empty value. + - cmd: | + if [ "{{.PROFILES}}" = "dev" ] && [ -z "${SAAS_DEV_PROJECT_REF:-}" ]; then + echo ">> SAAS_DEV_PROJECT_REF is not set." + echo ">> Testing a SaaS PR? Put its ref, DB password and publishable key in app/.env.saas.local." + echo ">> Wanted the shared v3 project? Use 'task backend:staging:saas' instead." + exit 1 + fi + - task: _run:saas + vars: + PORT: '{{.PORT}}' + PROFILES: '{{.PROFILES}}' + AIENGINE_URL: '{{.AIENGINE_URL}}' + AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' + AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' + + staging:saas: + desc: "Start SaaS backend against the shared v3 staging project" + cmds: + - task: _run:saas + vars: + PORT: '{{.PORT}}' + PROFILES: staging + AIENGINE_URL: '{{.AIENGINE_URL}}' + AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' + AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' + + _run:saas: + internal: true dotenv: ['app/.env.saas.local', 'app/.env.saas'] ignore_error: true vars: PORT: '{{.PORT | default "8080"}}' - # Override to "" to run the pure `saas` profile against your own SAAS_DB_*. PROFILES: '{{.PROFILES | default "dev"}}' + # Built here rather than inline in the cmds below: the Windows line is an + # unquoted YAML scalar wrapping a cmd.exe string, so a nested {{if ne .X + # "none"}} needs escaped quotes that reach the Go template as literal + # backslashes and fail with `unexpected "\" in operand`. + PROFILE_ARGS: '{{if ne .PROFILES "none"}}--spring.profiles.include={{.PROFILES}}{{end}}' AIENGINE_URL: '{{.AIENGINE_URL | default ""}}' AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}' AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}' @@ -77,9 +118,11 @@ tasks: AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' cmds: - - cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}" + # PROFILE_ARGS is empty when PROFILES=none, i.e. the bare `saas` profile + # against SAAS_DB_* (production). + - cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args=\"{{.PROFILE_ARGS}}\"{{end}}" platforms: [windows] - - cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}} + - cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args='{{.PROFILE_ARGS}}'{{end}} platforms: [linux, darwin] build: diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml index 481a225ce1..f5325c9ed7 100644 --- a/.taskfiles/frontend.yml +++ b/.taskfiles/frontend.yml @@ -5,6 +5,14 @@ version: '3' # mode flag) or use `--project editor/...` for tsc — so the editor lives # under frontend/editor/ without each task needing a cd. +vars: + # Dev-only browser-tab label so concurrent worktrees are distinguishable. Only + # the worktree folder basename (e.g. "wt1") is exposed — never the full path, + # hostname, or user. Dropped from production builds. + DEV_LABEL: + sh: >- + {{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}} + tasks: install: desc: "Install dependencies" @@ -80,16 +88,52 @@ tasks: OPEN: '{{.OPEN | default ""}}' env: BACKEND_URL: '{{.BACKEND_URL}}' - # Dev-only browser-tab label so concurrent worktrees are distinguishable. - # Only the worktree folder basename (e.g. "wt1") is exposed — never the - # full path, hostname, or user. Consumed at dev-serve time by vite.config - # and dropped from production builds. - STIRLING_DEV_LABEL: - sh: >- - {{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}} + STIRLING_DEV_LABEL: '{{.DEV_LABEL}}' cmds: - npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}} + # Separate from dev:_run rather than a flag on it: Task sets an `env:` key even + # when its value resolves to empty, and Vite treats an empty process.env VITE_* as + # authoritative over the committed editor/.env, so folding these in blanks Supabase + # config for the core, proprietary and desktop dev servers. + dev:_run:saas: + internal: true + ignore_error: true + # The backend's own env files, so both halves target one project. Paths are + # relative to this taskfile's dir, `frontend`. + dotenv: ['../app/.env.saas.local', '../app/.env.saas'] + vars: + PORT: '{{.PORT | default "5173"}}' + BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' + OPEN: '{{.OPEN | default ""}}' + SAAS_ENV: '{{.SAAS_ENV | default "dev"}}' + env: + BACKEND_URL: '{{.BACKEND_URL}}' + STIRLING_DEV_LABEL: '{{.DEV_LABEL}}' + SAAS_ENV: '{{.SAAS_ENV}}' + # A real process.env VITE_* beats a committed .env in Vite (loadEnv applies + # process.env last), which is what lets this override editor/.env. + # + # These must stay `sh:`, not Go templates: dotenv values are visible to Task's + # embedded shell but not to templates, where {{.SAAS_DEV_PROJECT_REF}} is + # always empty. + VITE_SUPABASE_URL: + sh: | + case "${SAAS_ENV:-dev}" in + staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;; + *) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or run task staging:saas}" ;; + esac + echo "https://${ref}.supabase.co" + VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: + sh: | + case "${SAAS_ENV:-dev}" in + staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;; + *) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;; + esac + cmds: + - 'echo ">> frontend Supabase target: $VITE_SUPABASE_URL"' + - npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}} + dev: desc: "Start frontend dev server" cmds: @@ -111,13 +155,23 @@ tasks: vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } dev:saas: - desc: "Start frontend dev server in SaaS mode" + desc: "Start frontend dev server in SaaS mode (SAAS_ENV=dev|staging|prod)" deps: - task: prepare vars: { MODE: saas } + vars: + SAAS_ENV: '{{.SAAS_ENV | default "dev"}}' + # prod routes to the plain runner, which sets no VITE_SUPABASE_* and so leaves + # the committed editor/.env alone. + RUNNER: '{{if eq .SAAS_ENV "prod"}}dev:_run{{else}}dev:_run:saas{{end}}' cmds: - - task: dev:_run - vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + - task: '{{.RUNNER}}' + vars: + MODE: saas + PORT: '{{.PORT}}' + BACKEND_URL: '{{.BACKEND_URL}}' + OPEN: '{{.OPEN}}' + SAAS_ENV: '{{.SAAS_ENV}}' dev:desktop: desc: "Start frontend dev server in desktop mode" diff --git a/Taskfile.yml b/Taskfile.yml index fc7a564032..92dcdcc742 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -99,11 +99,22 @@ tasks: BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' OPEN: "true" + # Set SAAS_DEV_PROJECT_REF in app/.env.saas.local to pick the PR. dev:saas: - desc: "Start SaaS backend + frontend concurrently on free ports" + desc: "Start SaaS backend + frontend + engine against the current PR's preview branch" cmds: - task: dev:_all - vars: { FRONTEND: saas, BACKEND: saas } + vars: { FRONTEND: saas, BACKEND: saas, SAAS_ENV: dev } + + staging:saas: + desc: "Start SaaS backend + frontend + engine against the shared v3 staging project" + cmds: + - task: dev:_all + vars: + FRONTEND: saas + BACKEND: saas + BACKEND_TASK: backend:staging:saas + SAAS_ENV: staging dev:all: desc: "Start backend + frontend + engine concurrently on free ports" @@ -115,6 +126,9 @@ tasks: vars: FRONTEND: '{{.FRONTEND | default "proprietary"}}' BACKEND: '{{.BACKEND | default "proprietary"}}' + BACKEND_TASK: '{{.BACKEND_TASK | default (printf "backend:dev:%s" .BACKEND)}}' + # Only meaningful to the saas frontend; every other flavor ignores it. + SAAS_ENV: '{{.SAAS_ENV | default ""}}' PORTS: sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}' BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' @@ -124,7 +138,7 @@ tasks: - task: engine:dev vars: PORT: '{{.ENGINE_PORT}}' - - task: 'backend:dev:{{.BACKEND}}' + - task: '{{.BACKEND_TASK}}' vars: PORT: '{{.BACKEND_PORT}}' AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}' @@ -134,6 +148,7 @@ tasks: PORT: '{{.FRONTEND_PORT}}' BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' OPEN: "true" + SAAS_ENV: '{{.SAAS_ENV}}' # ============================================================ # Build diff --git a/app/.env.saas b/app/.env.saas index fb5feec559..25eefb84c5 100644 --- a/app/.env.saas +++ b/app/.env.saas @@ -1,15 +1,16 @@ -############################################################################### -# Stirling-PDF SaaS environment defaults. +# Stirling-PDF SaaS environment defaults. Committed, non-secret. Real values for secrets go in +# .env.saas.local, which is loaded first and wins. Do not commit that file. # -# This file is committed and provides non-secret defaults loaded by -# `task backend:dev:saas`. Put real values for secrets (passwords, project -# refs, edge function secrets) in `.env.saas.local` - any variable set there -# takes precedence over what's defined here. +# Three environments, each deriving its Supabase URLs, JWT issuer and JWKS from one project ref: # -# DO NOT commit `.env.saas.local`. Only `.env.saas` is checked in. -############################################################################### +# prod PROFILES=none SAAS_DB_* the live project +# staging PROFILES=staging SAAS_STAGING_* pinned to v3, always there +# dev PROFILES=dev SAAS_DEV_* follows a SaaS PR's preview branch +# +# dev is the default for `task backend:dev:saas`. Use staging for somewhere stable; use dev when +# testing an open SaaS PR, since its preview branch is the only place those migrations are applied. -# ---------- Supabase project ---------- +# ---------- Supabase project (prod / no-profile) ---------- # Project reference (the subdomain part of .supabase.co). Required. # Set in .env.saas.local. SAAS_DB_PROJECT_REF= @@ -17,18 +18,35 @@ SAAS_DB_PROJECT_REF= # Edge function secret used by billing/license rollup calls. Set in .env.saas.local. SUPABASE_EDGE_FUNCTION_SECRET= -# ---------- Database (saas profile) ---------- -# Direct JDBC URL to the Supabase Postgres. Required when running the plain -# `saas` profile (i.e. without `--spring.profiles.include=dev`). +# ---------- Database (no profile) ---------- +# Direct JDBC URL to the Supabase Postgres. Required when running without +# `--spring.profiles.include=...`. # Example: jdbc:postgresql://db..supabase.co:5432/postgres SAAS_DB_URL= SAAS_DB_USERNAME=postgres SAAS_DB_PASSWORD= -# ---------- Database (dev profile overrides) ---------- -# Used when `--spring.profiles.include=dev` is active. The dev profile -# defaults the URL/username to the shared dev Supabase project, but the -# password must still be provided in .env.saas.local. -SAAS_DEV_DB_URL= +# ---------- staging profile ---------- +# The shared long-lived v3 project. application-staging.properties defaults the ref, +# URL, database host and meter endpoint, so staging needs only the password, in +# .env.saas.local. Set SAAS_STAGING_PROJECT_REF to repoint it; everything derives. +# +# The ref and publishable key are duplicated here because the task derives the +# frontend's VITE_SUPABASE_* from them and a shell cannot read a Spring default. +# Neither is secret: the ref is a public subdomain, the key ships in the bundle. +SAAS_STAGING_PROJECT_REF=qacaivhsjtftfwtgjvva +SAAS_STAGING_PUBLISHABLE_KEY=sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY # gitleaks:allow +SAAS_STAGING_DB_USERNAME=postgres +SAAS_STAGING_DB_PASSWORD= + +# ---------- dev profile ---------- +# The SaaS PR's Supabase preview branch. Take the ref from that PR's "Supabase +# Preview" check; the profile derives URL, JWT issuer, JWKS, meter endpoint and +# database host from it, so this one value follows a different PR. +# +# A preview branch has its own password and keys; the parent project's will not +# authenticate. Both go in .env.saas.local, along with the ref. +SAAS_DEV_PROJECT_REF= +SAAS_DEV_PUBLISHABLE_KEY= SAAS_DEV_DB_USERNAME=postgres SAAS_DEV_DB_PASSWORD= diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java b/app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java new file mode 100644 index 0000000000..39305fcbcd --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java @@ -0,0 +1,53 @@ +package stirling.software.saas.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +/** Logs which Supabase project this backend is talking to, and its schema policy. */ +@Slf4j +@Component +@Profile({"dev", "staging"}) +public class SaasProjectNotice { + + private final Environment environment; + private final String projectRef; + private final String ddlAuto; + + public SaasProjectNotice( + Environment environment, + @Value("${app.supabase.project-ref:unknown}") String projectRef, + @Value("${spring.jpa.hibernate.ddl-auto:none}") String ddlAuto) { + this.environment = environment; + this.projectRef = projectRef; + this.ddlAuto = ddlAuto; + } + + @EventListener(ApplicationReadyEvent.class) + public void announceProject() { + boolean staging = environment.matchesProfiles("staging"); + if (staging) { + log.info( + """ + SaaS staging profile: Supabase project {}, ddl-auto={}. This is the SHARED \ + long-lived environment, so its data and schema are not yours alone. Testing an \ + open SaaS PR? Use that PR's preview branch instead \ + (SAAS_DEV_PROJECT_REF in app/.env.saas.local); staging will not have its \ + migrations.\ + """, + projectRef, + ddlAuto); + return; + } + log.info( + "SaaS dev profile: Supabase preview branch {}, ddl-auto={}. Disposable, so Hibernate" + + " is allowed to add the inherited tables the migrations do not create.", + projectRef, + ddlAuto); + } +} diff --git a/app/saas/src/main/resources/application-dev.properties b/app/saas/src/main/resources/application-dev.properties index ee8bf80ff2..b289fc95cc 100644 --- a/app/saas/src/main/resources/application-dev.properties +++ b/app/saas/src/main/resources/application-dev.properties @@ -1,32 +1,40 @@ -# SaaS dev profile. Points at the dev Supabase project. -# Boot: java -jar stirling-pdf.jar --spring.profiles.include=dev +# SaaS dev profile: follows the Supabase preview branch of the SaaS PR under test. +# One variable switches PR, SAAS_DEV_PROJECT_REF; everything else derives from it. +# Want a stable shared environment instead? Use the staging profile. + spring.config.import=optional:classpath:application-dev-local.properties -app.supabase.project-ref=qacaivhsjtftfwtgjvva +# Let Hibernate reconcile the entity tables so a fresh preview branch heals itself. A branch is built +# from the Supabase migrations, which cover the SaaS-owned tables but not the ~28 inherited from the +# self-hosted app -- those have only ever been created by ddl-auto. Safe here because a preview branch +# is disposable and `update` only ever adds; staging pins `none`, so keep this profile-scoped. +spring.jpa.hibernate.ddl-auto=update -stirling.supabase.url=https://qacaivhsjtftfwtgjvva.supabase.co -stirling.supabase.publishable-key=sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY # gitleaks:allow +# From the PR's "Supabase Preview" check. Required with no fallback: ddl-auto=update above must never +# be aimed at the shared project. +app.supabase.project-ref=${SAAS_DEV_PROJECT_REF} -spring.datasource.url=${SAAS_DEV_DB_URL:jdbc:postgresql://db.qacaivhsjtftfwtgjvva.supabase.co:5432/postgres?ApplicationName=stirling-consolidation-${user.name}} +stirling.supabase.url=https://${app.supabase.project-ref}.supabase.co +# Per-branch, not derivable. Dashboard > Settings > API. +stirling.supabase.publishable-key=${SAAS_DEV_PUBLISHABLE_KEY} + +# Override the whole URL if the branch needs the pooler host rather than the direct one. +spring.datasource.url=${SAAS_DEV_DB_URL:jdbc:postgresql://db.${app.supabase.project-ref}.supabase.co:5432/postgres?ApplicationName=stirling-dev-${user.name}} spring.datasource.username=${SAAS_DEV_DB_USERNAME:postgres} -# Password not committed; export SAAS_DEV_DB_PASSWORD or pass --spring.datasource.password=... +# A preview branch has its own password; the parent project's will not authenticate. spring.datasource.password=${SAAS_DEV_DB_PASSWORD:} -# Conservative dev pool sizing. spring.datasource.hikari.maximum-pool-size=2 spring.datasource.hikari.minimum-idle=1 spring.datasource.hikari.idle-timeout=60000 spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.keepalive-time=300000 -spring.datasource.hikari.data-source-properties.ApplicationName=stirling-consolidation-${user.name} +spring.datasource.hikari.data-source-properties.ApplicationName=stirling-dev-${user.name} logging.level.stirling.software.saas=DEBUG logging.level.org.springframework.security.oauth2.jwt=WARN logging.level.org.springframework.security.oauth2.server.resource=WARN -# Supabase meter edge fn the Java backend calls (server-to-server, on job close). -# URL is not a secret; auth rides the existing SUPABASE_EDGE_FUNCTION_SECRET (same -# shared secret the team-invitation flow uses — no service-role key in the Java env). -# Blank secret → the meter service no-ops with a WARN, so the app still boots. -# The billing portal is NOT here — the FE calls create-customer-portal-session directly. -payg.meter.endpoint=https://qacaivhsjtftfwtgjvva.supabase.co/functions/v1/meter-payg-units +# Server-to-server meter call. Auth rides SUPABASE_EDGE_FUNCTION_SECRET; blank secret means the meter +# service no-ops with a WARN rather than failing the boot. +payg.meter.endpoint=https://${app.supabase.project-ref}.supabase.co/functions/v1/meter-payg-units diff --git a/app/saas/src/main/resources/application-staging.properties b/app/saas/src/main/resources/application-staging.properties new file mode 100644 index 0000000000..4874ed3fb5 --- /dev/null +++ b/app/saas/src/main/resources/application-staging.properties @@ -0,0 +1,39 @@ +# SaaS staging profile: the long-lived shared v3 project, pinned so it is still there tomorrow. +# For work on an open SaaS PR use the dev profile, which follows that PR's preview branch. + +spring.config.import=optional:classpath:application-staging-local.properties + +# Stated rather than inherited: application-saas.properties defaults to `update`, and staging's +# schema is shared and RLS-dependent, so it must not be reconciled by Hibernate. +spring.jpa.hibernate.ddl-auto=none + +# Committed as a default rather than a literal, so staging needs no setup but stays repointable. +# Neither the ref nor the publishable key is secret: the ref is a public subdomain, the key ships in +# the browser bundle. Everything below derives from the ref, so an override follows through. +app.supabase.project-ref=${SAAS_STAGING_PROJECT_REF:qacaivhsjtftfwtgjvva} + +stirling.supabase.url=https://${app.supabase.project-ref}.supabase.co +stirling.supabase.publishable-key=${SAAS_STAGING_PUBLISHABLE_KEY:sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY} + +spring.datasource.url=${SAAS_STAGING_DB_URL:jdbc:postgresql://db.${app.supabase.project-ref}.supabase.co:5432/postgres?ApplicationName=stirling-staging-${user.name}} +spring.datasource.username=${SAAS_STAGING_DB_USERNAME:postgres} +# Password not committed; export SAAS_STAGING_DB_PASSWORD or pass --spring.datasource.password=... +spring.datasource.password=${SAAS_STAGING_DB_PASSWORD:} + +# Conservative pool sizing: this is a shared project, so don't hold connections others need. +spring.datasource.hikari.maximum-pool-size=2 +spring.datasource.hikari.minimum-idle=1 +spring.datasource.hikari.idle-timeout=60000 +spring.datasource.hikari.max-lifetime=1800000 +spring.datasource.hikari.keepalive-time=300000 +spring.datasource.hikari.data-source-properties.ApplicationName=stirling-staging-${user.name} + +logging.level.stirling.software.saas=DEBUG +logging.level.org.springframework.security.oauth2.jwt=WARN +logging.level.org.springframework.security.oauth2.server.resource=WARN + +# Supabase meter edge fn the Java backend calls (server-to-server, on job close). +# URL is not a secret; auth rides the existing SUPABASE_EDGE_FUNCTION_SECRET (same +# shared secret the team-invitation flow uses — no service-role key in the Java env). +# Blank secret → the meter service no-ops with a WARN, so the app still boots. +payg.meter.endpoint=https://${app.supabase.project-ref}.supabase.co/functions/v1/meter-payg-units From 088e0ef4e25e06ae5f911f8be74912fef8673240 Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 19 Aug 2026 18:29:02 +0000 Subject: [PATCH 153/154] deps(frontend): upgrade Cantoo PDF library to 2.8.2 (#7493) # Description of Changes This pull request upgrades the frontend PDF dependency from `@cantoo/pdf-lib` 2.6.5 to 2.8.2. - Updated `frontend/package.json` to require `@cantoo/pdf-lib` `^2.8.2`. - Regenerated `frontend/package-lock.json` with `@cantoo/pdf-lib@2.8.2`, `pako@2.2.0`, and `node-html-better-parser@1.5.9`. - Added the root npm `pako` override recommended by the upstream release. - The upgrade brings upstream parser, object-stream, encryption, form, PNG, and PDF serialization fixes into the frontend dependency. - No application API migration was required because the project does not use the newly added PDF/A, XFA, Factur-X, incremental-update, fontkit, or page-content-extraction APIs. The main challenge was validating the broad upstream change set against the project's actual usage. The frontend typecheck and a direct PDF create/save/load smoke test passed. The complete `frontend:check` and `frontend:test` tasks exceeded the available execution timeout without reporting a test failure. No related issue. --- ## 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/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/HowToAddNewLanguage.md) (if applicable) - [x] 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/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) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --- frontend/package-lock.json | 38 ++++++++++++++++++++++++++------------ frontend/package.json | 3 ++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d50868bd60..37bca97b95 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,7 @@ "license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@cantoo/pdf-lib": "^2.5.3", + "@cantoo/pdf-lib": "^2.8.2", "@dnd-kit/core": "^6.3.1", "@embedpdf/core": "^2.14.4", "@embedpdf/engines": "^2.14.4", @@ -606,18 +606,22 @@ } }, "node_modules/@cantoo/pdf-lib": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.5.tgz", - "integrity": "sha512-3eMHEaqKHt/G/q+6QjT06A3lz0S/a8x3+myiSN7FNeL3uWcedO0lpfs6TWofa4C03Z1wz3tWeHoa4CsI7DrTSA==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.8.2.tgz", + "integrity": "sha512-f0BJM3uPOjbPR3YriSEUIaTM0qnqthjFmTZX9NGI0NDM2Tj4a8xv7Z5Hb6jzUrhfb3/9Y77+xxoOln0IIiYq+w==", "license": "MIT", "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "color": "^4.2.3", "crypto-js": "^4.2.0", - "node-html-better-parser": ">=1.4.0", - "pako": "^1.0.11", + "html-entities": "^2.3.2", + "node-html-better-parser": ">=1.5.9", + "pako": "^2.2.0", "tslib": ">=2" + }, + "peerDependencies": { + "html-entities": "^2.3.2" } }, "node_modules/@csstools/color-helpers": { @@ -12459,9 +12463,9 @@ } }, "node_modules/node-html-better-parser": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.8.tgz", - "integrity": "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw==", + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.9.tgz", + "integrity": "sha512-z1I5UINMezJXYL9cH3h0a9KBth2G978gSLlfkpQ+CQzzVHVQy9gpARgm9eDsz1O4gn1HtgUqjdAIYxKFZm6uHQ==", "license": "MIT", "dependencies": { "html-entities": "^2.3.2" @@ -12751,9 +12755,19 @@ "license": "MIT" }, "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { diff --git a/frontend/package.json b/frontend/package.json index 90a0e10b05..f874dedfa3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "proxy": "http://localhost:8080", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@cantoo/pdf-lib": "^2.5.3", + "@cantoo/pdf-lib": "^2.8.2", "@dnd-kit/core": "^6.3.1", "@embedpdf/core": "^2.14.4", "@embedpdf/engines": "^2.14.4", @@ -171,6 +171,7 @@ }, "overrides": { "devalue": "^5.8.1", + "pako": "^2.2.0", "tsconfck": { "typescript": "$typescript" } From 1690cc25ccbf100f3be1699ac098bca419f1ca6a Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 05:15:37 +0000 Subject: [PATCH 154/154] Update Frontend 3rd Party Licenses (#7573) Auto-generated by stirlingbot[bot] This PR updates the frontend license report based on changes to package.json dependencies. Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- frontend/editor/src/assets/3rdPartyLicenses.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/editor/src/assets/3rdPartyLicenses.json b/frontend/editor/src/assets/3rdPartyLicenses.json index 110a6c88a5..e17cef8f8b 100644 --- a/frontend/editor/src/assets/3rdPartyLicenses.json +++ b/frontend/editor/src/assets/3rdPartyLicenses.json @@ -10,7 +10,7 @@ { "moduleName": "@cantoo/pdf-lib", "moduleUrl": "https://github.com/cantoo-scribe/pdf-lib", - "moduleVersion": "2.6.5", + "moduleVersion": "2.8.2", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -255,14 +255,14 @@ { "moduleName": "@stripe/react-stripe-js", "moduleUrl": "https://github.com/stripe/react-stripe-js", - "moduleVersion": "4.0.2", + "moduleVersion": "6.8.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@stripe/stripe-js", "moduleUrl": "https://github.com/stripe/stripe-js", - "moduleVersion": "7.9.0", + "moduleVersion": "9.10.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -287,6 +287,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@tanstack/react-table", + "moduleUrl": "https://github.com/TanStack/table", + "moduleVersion": "9.1.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@tanstack/react-virtual", "moduleUrl": "https://github.com/TanStack/virtual",