Every listing bumped `thumbnailStoredAt` on every record it returned, and a bump
rewrites the WHOLE record - bytes included - so a 60-file library rewrote 60 full
records per refresh, several times per page load.
Debounced to once a day. On a 30-day TTL that is indistinguishable from bumping on
every read, and expiry is unchanged: a stale thumbnail is still cleared the moment
it's seen.
Rebased onto #7366 rather than main: that PR rewrote these exact call sites to keep
maintenance writes away from blob-bodied records (which can wedge the object store
on WebKit), so the two changes now compose - skip the risky records, and debounce
the rest.
Review pass, no behaviour change:
- Drop `isRecordUnreadable()`. Nothing in the app called it - listings read the
set directly - so it was public API existing only for two test assertions.
Those now assert the user-visible contract (`stub.dataUnavailable`) instead.
- Revert `createBlobUrl` to its previous shape. It has ZERO production callers,
so rewriting it (and auditing inside it) was diff noise on a dead method.
- Remove a dead guard in `reportIfUnreadable`: every probed record is added to
`auditedRecords` before probing, including ones that turn out unreadable, so
the `unreadableRecords` check could never be the one to short-circuit.
The infinite "Loading files..." after a Safari reload, finally caught in the act:
the sidebar listing's thumbnail TTL bump opens a readwrite transaction that
re-reads every record - and in WebKit a `get` touching a blob-bodied record with
a damaged backing store HANGS rather than errors. One pending request keeps that
transaction alive forever, and every later transaction on the store queues behind
it: hydration reads, new uploads, policy-output persists. One wedge explained the
whole screen - files that won't open, fresh uploads spinning, runs that never
deliver.
Proof from the session log: after the TTL bump's refused-put warning, reads of
even a just-rescued (ArrayBuffer) record never settled - a healthy record's read
hanging means the store is blocked, not the record.
Maintenance now skips blob-bodied records on a browser whose durable verdict is
"blobs unsupported": their rewrite would be refused anyway, so they are all risk
and no value. Chrome (blobs genuinely supported) is unchanged.
The library must tell the truth per row instead of listing files that pretend to
open. Timers and toasts were treating the symptom; this makes the sidebar
represent actual IndexedDB status, so legacy WebKit damage is visible and
actionable (re-upload), and stops the remaining data-loss vector.
- Listings audit every blob-backed record's bytes out of band (never awaited -
the probe itself can hang on WebKit). A record whose bytes are gone flags
`dataUnavailable` on its stub, renders a "Data lost" badge with a tooltip, and
its click explains instead of failing.
- RESCUE: on a browser whose durable verdict is "blobs unsupported", a legacy
blob record that is still readable today is rewritten as an ArrayBuffer copy
while the bytes still exist - closing the loss vector for pre-verdict records
instead of waiting for WebKit to lose them too.
- The v6/v7 version probe runs before the guarded open and a versionless open can
be delayed indefinitely by another tab mid-versionchange; it now proceeds
without an answer rather than hanging every storage consumer ahead of the
blocked guard.
Records are never auto-deleted: one NotFoundError is not proof the bytes are
gone forever, and the flag is session-scoped so a reload re-tests.
The library spinner ran forever with an empty console after uploading several
files and reloading. A blocked open fires `blocked` and then NOTHING - no success,
no error - so the open promise never settled and every caller hung. FileSidebar's
try/catch can't help: it guards a rejection, not a promise that never settles.
- `blockedGuard`: warn, wait out a grace period, then reject with something the
user can act on. Rejecting doesn't cancel the request, so a connection that
arrives late is closed rather than held - otherwise we become the next blocker.
- Registration hoisted above everything async. A map written after a yield point
can't dedupe callers racing into it in the same tick, so every context that
opened the files database during boot got its own connection, and per spec only
the first request ever receives `blocked`. This only affected
stirling-pdf-files: the one config with an await before registration.
- `onversionchange` closes and forgets our connection, so a release that bumps the
schema no longer bricks every open tab. Forget before close: a cached but closed
handle is worse than none, because every transaction on it throws.
- `deleteDatabase` gets the same guard; it blocks the same way and is awaited on
the files open path.
Re-derived from #7416, which was closed unmerged and is in neither main nor this
branch. Whichever lands second is a no-op for the overlapping parts.
The stub/shadow pattern needs both layers to share an interface: core code passes
onDismiss, and in a core build the overlay resolves to the stub, which didn't
declare it. Broke `frontend:typecheck:core` - the proprietary typecheck passes
because the real overlay has always accepted it.
Handing dead bytes over stopped the open path from stalling, but left the viewer
rendering a document that never loads - an endless spinner, reported on Safari and
the DuckDuckGo browser with the "File data is unavailable" toast alongside it.
- fileStorage notifies listeners when a record's bytes are confirmed unreadable,
and refuses to hand that record out again for the rest of the session.
- FileContext subscribes and removes the file, so nothing keeps waiting on bytes
that can't arrive.
The record itself is kept and the refusal is session-scoped, so a reload re-tests
it: WebKit throwing NotFoundError once isn't proof the file is gone forever, and
deleting on that evidence risks destroying recoverable data.
A policy that changes nothing (redaction matching no text, say) completes with no
output file. The import effect skipped those runs entirely, so `imported` never
flipped - and the badge treats `imported` as the settle signal, so the file's
spinner and its blocking overlay ran forever.
Engine-agnostic: it needs a document the policy's patterns don't match, which is
why it looked Safari-specific. Confirmed from a backend log showing "Redaction
scan: 0 occurrences across 0 pages" for every spinning file.
The enforcement overlay swallows clicks, and the card's had no dismiss - so a run
that never settles left the file permanently unusable with no way out. The
viewer's equivalent has always been dismissible.
getFiles() during render doesn't subscribe to the state it reads, so the panel
kept showing the pre-hydration (or pre-version) file. The app's own guard was
logging this while opening a file.
Deleting a file removed one record; its older versions kept their full bytes and
were invisible, because listings filter on isLeaf. Observed live: three uploads
produced six records (a policy versioned each), and deleting one file left its v1
behind.
The lineage expansion belongs at the user-facing delete sites, NOT in
removeFiles: VersionHistoryModal deletes individual versions through the same
low-level path, so expanding there would wipe a whole chain when the user removes
one version.
Deleting a library file that was never in the workbench dispatches REMOVE_FILES
for an id the reducer doesn't hold. It rebuilt `files` and `ui` anyway, which the
dev identity guard flags: every file and UI consumer re-renders for nothing. The
console noise also buried the errors we were hunting.
The workbench renders from hydrated bytes held in a ref, so a file is invisible
until hydration dispatches - and the only dispatch sat after a full pdfium parse
of the whole PDF. Loading also shared the parse queue's two slots, so a stalled
parse kept other files from loading at all. Observed in Chrome: nine seconds of
the upload drop zone on a cold engine, with the sidebar row showing as open.
- The File is published as soon as it loads; the parse is still queued and now
only refines the stub (page metadata, thumbnail).
- The workbench shows progress instead of the drop zone while files are loading.
- A load that hasn't settled after 8s names the file in the console. Reporting
only: the read is never abandoned, because large files legitimately take time.
`instantiateWasm` reports success by callback, so a rejection inside it is
invisible to emscripten: init() simply stays pending, and with it every
thumbnail, page parse, form read and policy delivery - silently, for the rest of
the session. The hand-rolled streaming fallback had no rejection handling at all.
Failures are now raced into the init promise, the fallback is gone (with no
override emscripten fetches the WASM itself and rejects properly), and a failed
load is no longer cached so the next call can retry.
The viewer was unaffected throughout because it uses embedpdf's own engine, which
is why this presented as "I can view files but see no thumbnails".
WebKit refuses Blob/File values in IndexedDB outright - proven with raw
IndexedDB and no app code: the write fails with UnknownError and aborts its
transaction, where Chromium commits and reads back cleanly. It can also accept
one and later lose the backing store, leaving a record that looks valid and
whose bytes are gone.
- The "this browser loses blob values" verdict is now durable. Session-scoped,
every reload re-decided optimistically and wrote another batch of files the
engine would lose, so Safari never converged on a shape that works.
- Readability is reported, never awaited. The probe read of a lost backing store
can stay pending forever in Safari, so awaiting it stalled EVERY file open
rather than the one consumer that would have failed anyway. The store-time
probe, which must run while the source file is still in hand to repair the
record, gets a deadline instead.
- deleteStirlingFile resolves on commit with an abort guard: callers refresh
their list as soon as it resolves, and an aborted delete put the row back.
- orphanedAncestorIds() collects the versions nothing else needs, keeping any
ancestor a surviving leaf still descends from (split siblings).
Storybook builds through its own Vite config, so the worker.plugins entry added
to editor/vite.config.ts didn't reach it: the app build resolved the alias and
the Storybook preview build failed on the same import.
Same root cause, same fix. The tsconfigPaths plugin is now built by a helper so
the main pass and the worker pass each get their own instance instead of the
alias map being restated.
Caught by CI rather than locally because `task frontend:check` stops at
lint/typecheck/test - storybook:build only runs under `check:all`.
The multi-tab IndexedDB lifecycle fixes and the thumbnail TTL write
amplification fix are not WebKit bugs - they were found while chasing the same
symptom, not the same cause. They now live in their own PRs so each can be
reviewed against its own evidence:
- fix/indexeddb-multitab-lifecycle: blocked opens/deletes, the concurrent-open
race, onversionchange, and the sidebar/picker resting state.
- perf/thumbnail-ttl-write-amplification: the once-a-day bump debounce.
What stays here is WebKit-caused, including the parts that look like generic
refactoring: a refused blob write aborts its transaction, so `settleOnAbort`
and the single-transaction `updateRecord` are the fix for tool outputs silently
failing to persist on Safari, not tidy-up.
FileSidebar's try/catch appears in both this branch and the lifecycle branch -
a WebKit rejection and a blocked-open rejection both have to stop stranding the
spinner. Whichever lands second is a no-op for that file.
Worker bundles are a separate Rollup pass and do not inherit `plugins`, so
`@app/*` - provided by vite-tsconfig-paths - resolved in the app and failed
in a worker. The workaround was a relative import plus an oxlint exemption,
which left the next value import into a worker to rediscover the same thing.
Give the worker pass the same tsconfigPaths plugin. Verified both ways: the
build resolves the alias and inlines the probe into the worker chunk, and
removing the block fails with "Rollup failed to resolve @app/utils/
canvasImageEncoding from pixelCompareWorker.ts".
#7304 made CI run the whole stubbed suite once per engine, which removed the
only use of this task that e2e:cross-browser didn't already cover. Run the
capability specs alone with:
task e2e:cross-browser -- --grep @engine-capability
The @engine-capability tag stays, so the specs are unchanged.
Resolves the e2e CI conflicts against #7304, which landed a per-browser
matrix that already runs the whole stubbed suite on chromium, firefox and
webkit for every PR.
- .github/workflows/e2e-stubbed.yml: take main's matrix. It supersedes this
branch's dedicated @engine-capability step, which existed only to get
cross-engine coverage into PRs without paying for the full suite.
- .taskfiles/e2e.yml: keep both tasks. `stubbed-project` is what the matrix
calls; `capabilities` stays as the local shortcut for checking all three
engines without the whole cross-browser run.
# 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.
## What
Makes Storybook render components with the same CSS the app gives them.
- The preview loaded the token primitives but **not the editor's
semantic token layer** (`styles/theme.css`), so components styled on
those variables rendered unthemed — three onboarding stories were
importing it by hand to stop their modal surfaces rendering transparent.
It's now loaded in the preview and the workarounds are gone.
- **Portal stories render inside the `.portal-scope` wrapper** PortalApp
mounts, so the portal's scoped reset and typography apply to them
exactly as in the app — and, deliberately, to nothing else.
- The folder stories invented their own hex colours, two of which aren't
values the app's `FOLDER_COLOR_PALETTE` can produce. They now use the
palette, so they can't drift from what a user can actually pick.
Deliberately does **not** load `tailwind.css` — tailwind is on its way
out of the editor, so matching the token layer alone is the target
state.
## Story colours route through the tokens, enforced
Stories were exempt from the `code-colors` lint, and it showed:
hardcoded hexes for surfaces the tokens already name (chat bubbles,
borders, demo backgrounds), `var(--x, #hex)` fallbacks that mask a
renamed token by silently painting the stale colour, and mocked category
accents for which real `--color-cat-*` tokens exist.
- Styling literals now use tokens; the dead fallbacks are stripped.
- The stories exemption is removed from `theme-lint`, so this can't
regress.
- Colours that are **the datum itself** — `ColorInput` values, signature
ink, per-policy accents, brand-mark swatches — stay literal via
`theme-allow-color`, hoisted to named consts so the exemption and its
reason sit together.
A practical side effect: stories styled on tokens actually respond to
the dark-mode toolbar toggle, which is what makes a dark-theme a11y pass
meaningful later.
## The a11y gate now runs dark as well as light
Contrast is most of what axe reports and it is theme-dependent, so a
light-only gate left half the surface unmeasured — and it only becomes
measurable at all once the tokens above actually flip. `SCAN_THEME=dark`
pins the theme for a whole scan run, every a11y task runs both themes,
and each theme has its own baseline:
- **light** re-recorded against the themed rendering (the old baseline
measured colours the app never shows): 831 stories with violations
- **dark** recorded for the first time: 798 stories with violations, 980
story-rule pairs, zero render failures across the full sweep
Verified end to end: dark scans measure against dark surfaces (`#18181b`
vs `#ffffff`), both baselines self-check clean, and a live scan of
stories that changed on main after recording passes both gates.
Nightly's timeout doubles for the second sweep.
## Testing
Typecheck (all variants), ESLint and Prettier pass. Onboarding, folder,
portal and control stories render in the browser scan (39/39) with the
per-story CSS imports removed; every story touched by the colour sweep
renders too (58/58). `task frontend:lint:colors` passes with stories
included.
# Description of Changes
Currently when calculating the output file type for some tools, the
system will get it wrong because it doesn't know about what the default
parameters in tools are, so if it doesn't have a value for some key,
it'll just bail out and say "it might not be compatible". This PR adds
logic to `ToolIO` to read the default values set for the parameters if
the tool has `ToolIOCase`s and takes them into account when figuring out
the output type. I've built it with horrible Java reflection magic to
avoid having to specify the default for params twice, which will make it
impossible for the defaults to disagree with each other. This just runs
once at startup so there's negligible performance impact.
The change is easily tested with Change Parameters, which is just
`add-password` behind the scenes but with the password params omitted
(so Change Password is always PDF->PDF, never encrypted like Add
Password).
Also (somewhat hackily) fixes a bug I noticed where saving a Change
Permissions step then leaving and returning to the pipeline will cause
the step to be reloaded as Add Password. I've added a system to
disambiguate tools which share the same endpoint (which is only these
two currently).
## Currently
<img width="455" height="135" alt="image"
src="https://github.com/user-attachments/assets/c8867d6a-599b-4f21-a2db-4a1b6ac22d73"
/>
## Now
<img width="415" height="122" alt="image"
src="https://github.com/user-attachments/assets/bd8d1bab-f00a-421b-8c91-5af0d2ad5335"
/>
# Description of Changes
Nightlies keep failing because the Playwright tests only run on Chrome
in PRs. This PR changes it so that we run all 3 browsers in all
(frontend) PRs so we catch these things before they merge in. They run
in parallel so it won't take any more time for the CI to finish.
# Description of Changes
Continued effort towards removing all uses of the any type in our
frontend code (last PR was #7326). This PR fixes 7 more folders and
removes them from the exclude list. All of them were localised within
the folder in the exclude list so again were pretty easy to fix.
# Description of Changes
This PR replaces multiple hardcoded English UI strings with translation
keys to improve localization consistency throughout the editor.
### What was changed
- Added new translation entries for:
- AI chat panel header and empty state
- Generic dropdown placeholders and empty states
- Generic input placeholder (`Enter value`)
- Tool renderer "tool not found" message
- Signature pen size placeholder
- Updated shared components to use translated fallback placeholders
instead of hardcoded English text:
- `DropdownListWithFooter`
- `EditableSecretField`
- `GroupedFormatDropdown`
- `LanguagePicker`
- `PenSizeSelector`
- Localized the AI chat panel:
- Assistant title
- Empty state message
- Input placeholder
- Localized the fallback error message displayed when a tool cannot be
resolved.
### Why the change was made
Several shared UI components and the AI assistant interface contained
hardcoded English strings, preventing proper localization and creating
an inconsistent multilingual experience. Moving these strings into the
translation system ensures they can be translated alongside the rest of
the application and provides reusable defaults for shared components.
---
## 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.
Follow-up to #7314. That PR fixed the IndexedDB blob rejection itself; this
one fixes the ways the same failures surfaced as a permanent spinner, and adds
the cross-browser CI signal that would have caught them on the pull request
instead of six weeks later in a nightly run.
Storage no longer hangs:
- Blocked IndexedDB opens and deletes now time out with an actionable error
instead of never settling, and the app sets `onversionchange` so an open tab
yields its connection rather than blocking every other tab forever.
- The in-flight open promise is registered before the first await, so
concurrent boot-time callers share one connection. Only the first request
receives `blocked`; the others were getting no events at all.
- Transactions settle on abort. Read-modify-write moves to a single
`updateRecord` helper that owns its transaction, guards it once, and resolves
on commit - the previous two-promises-over-one-transaction shape left the
write with no abort handler, which could hang output persistence silently.
- The blob-value refusal is remembered from any write, not just the initial
add: WebKit reports it per-operation, so an engine that accepted the add can
still refuse the rewrite.
WebKit engine gaps:
- ReadableStream async iteration, which pdf.js uses for all text extraction.
Without it Compare, read-aloud and the text editor were dead on Safari.
- requestIdleCallback, installed once at the entry point instead of guarded at
each call site.
- convertToBlob silently returns PNG for a format it can't encode, so canvas
output now probes what the engine really produced and picks the best lossy
format it honours.
CI:
- A small @engine-capability suite runs on Chromium, Firefox and WebKit on
every pull request. It asserts the primitives actually work (a counted
comparison, a raster thumbnail, a byte round-trip through a reload) rather
than that the UI rendered, which is how two total WebKit outages passed.
- The cross-browser projects now share the stubbed project's viewport so a
layout difference can't read as an engine outage.
Auto-generated by stirlingbot[bot]
This PR updates the frontend license report based on changes to
package.json dependencies.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Bumps the react group with 2 updates in the /frontend directory:
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react)
and
[@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).
Updates `@types/react` from 19.2.17 to 19.2.18
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />
Updates `@types/react-dom` from 19.2.3 to 19.2.4
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom">compare
view</a></li>
</ul>
</details>
<br />
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)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
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 <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> 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 <dependency name> 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 <dependency name>` 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 <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
# 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 `<button>` and stuff in specific components to try and
encourage them to use our shared UI. This is a very recent addition to
our linting config, and personally I'm willing to drop it for now at
least. We can still ban specific imports in files, so the files which we
were trying to enforce shared UI will still ban directly importing
Mantine, so that'll probably be most of the cases still caught, but I
think there are other ways we can encourage using the shared UI beyond
just using the linter for it.
I did try building a custom TS rule for it and it only slowed it down a
tiny bit (it took 1.1s) but it had to be built on an unreleased alpha
API which just sounds like a maintenance headache we don't need to deal
with for a rule that we don't really need.
# 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.
# 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>
Fix#6121
# Description of Changes
<!--
Please provide a summary of the changes, including:
- What was changed
- Why the change was made
- Any challenges encountered
Closes (#6121 )
-->
This PR expands the viewer ruler/measurement tool with real-world scale
support. Users can now apply preset scales, define custom scales,
calibrate a scale by drawing a reference measurement and entering its
known real-world distance, and view measurements with scaled real-world
values.
It also refactors PDF `/Measure` and `/VP` scale extraction out of
`EmbedPdfViewer` into a dedicated utility, centralizes ruler state
management in a dedicated hook, persists ruler measurements and selected
scales per file during the browser session, remembers the last
calibration unit locally, and updates the ruler overlay so measurements
remain aligned with the PDF page during rotation and scrolling.
**New Files**
- `RulerMeasurementLayer.tsx` - Renders ruler measurements in the SVG
overlay, including lines, points, labels, page/scaled values, delete
controls, live previews, clear controls, and label visibility modes.
- `RulerScaleSettingsButton.tsx` - Adds the scale settings
button/popover to the viewer toolbar.
- `ScaleCalibrationDialog.tsx` - Provides the calibration modal where
users enter a known real-world distance to calculate the scale
automatically.
- `ScaleSettingsPanel.tsx` - Provides preset scales, custom scale input,
calibration entry point, active scale display, and reset controls.
- `useMeasurementManager.ts` - Centralizes ruler state, custom scale
state, calibration flow, per-file measurements, session persistence, and
loading of PDF-derived scale data.
- `measurementPreferences.ts` - Persists the last calibration unit in
`localStorage`.
- `measurementTypes.ts` - Defines shared measurement, point, scale, page
scale, and viewport scale types.
- `measurementUtils.ts` - Provides unit conversion, scale calculation,
validation, formatting, calibration helpers, and session storage
helpers.
- `measurementUtils.test.ts` - Adds unit tests for scale calculations,
unit conversion, preset parsing, ratio derivation, and calibration.
- `pdfMeasurementExtraction.ts` - Moves PDF `/Measure` and `/VP` scale
extraction into a dedicated utility.
**Changed Files**
- `EmbedPdfViewer.tsx` - Removes inline PDF scale extraction and
delegates ruler/measurement state to `useMeasurementManager`; integrates
the ruler overlay, custom scale support, restored measurements, and
calibration dialog.
- `LocalEmbedPDF.tsx` - Adds page-level metadata used by the ruler
overlay, including page width, height, and native page rotation.
- `RotateAPIBridge.tsx` - Adds immediate rotation update propagation so
ruler measurements can update their page-anchored positions during
rotation changes.
- `RulerOverlay.tsx` - Refactors the ruler overlay to use shared
measurement types/utilities, support custom scales, calibration
measurements, restored measurements, measurement change listeners,
rotation-aware positioning, and scroll compensation, also holding Alt
key will activate pass-through behavior so labels do not block ruler
interactions.
- `useViewerWorkbenchBarButtons.tsx` - Adds the ruler scale settings
action and coordinates ruler, pan mode, and calibration behavior.
- `ViewerContext.tsx` - Adds immediate rotation notification support
used by ruler measurements while viewer rotation changes are applied.
- `en-GB/translation.toml` and `en-US/translation.toml` - Add UI text
for scale settings, calibration actions, ruler measurement values, and
ruler label controls.
---
## 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 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)
- [x] 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)
Current scale panel :
<img width="1316" height="515" alt="Captura de tela de 2026-05-31
21-57-53"
src="https://github.com/user-attachments/assets/df0ffb7d-4546-450c-8380-409658852a87"
/>
Current calibration input :
<img width="1021" height="522" alt="Captura de tela de 2026-05-31
21-59-52"
src="https://github.com/user-attachments/assets/e17c14e7-fec7-42e9-af48-33ef825acb6c"
/>
Example of usage :
<img width="1316" height="760" alt="Captura de tela de 2026-05-31
22-31-01"
src="https://github.com/user-attachments/assets/fcd3578e-1eaa-42d3-b30b-3fc87c796f90"
/>
### 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>
# Description of Changes
The restructuring of the frontend PR (#7062) has highlighted a couple of
issues with the source which are currently hidden due to the structure
of the code. This PR fixes the issues in Watched Folders:
- There's a circular dependency in TS code
- The `.gitignore` file excludes the watched folders dir by accident
# Description of Changes
> Stacked on #7264 — review that first. This diff is against its branch.
## The problem
`AppConfigContext` hand-rolled a query client: a `fetchCountRef` dedupe
guard, an exponential-backoff retry loop with its own `sleep()`, a
`hasResolvedConfig` flag, and manual 401/5xx branching. All to fetch one
endpoint that 80 files read.
## End state
Same provider, same public contract, React Query underneath. **250 lines
to 142**, and no consumer file changes.
| | Before | After |
|---|---|---|
| Dedupe | `fetchCountRef` guard | query key |
| Retry | `for` loop + `sleep()` + backoff maths | `retry` +
`retryDelay` |
| 401 | caught in the component, sets default config | `fetchAppConfig`
returns the default — the retry predicate and error state only see real
failures |
| Auth pages | early return inside the fetch | `enabled` |
| Resolved-yet tracking | `hasResolvedConfig` state | derived from the
query |
`fetchAppConfig` moves to `core/api/config.ts` with the simulation hook
and request options, so the context no longer knows how config is
fetched.
**Behaviour change:** config survives a provider remount instead of
refetching. That matters on desktop, where a connection-mode switch
remounts the tree — and #7264's cache reset already clears it on exactly
that transition.
## Testing
The existing 12-case contract test passes unchanged apart from the query
wrapper. It caught a real mistake: `failureCount` is 0-based in v5, so
`<= maxRetries` gave one attempt too many.
Four cases added — cached remount, `maxRetries` honoured, 4xx not
retried, `autoFetch` off.
`task frontend:check` green: 1672 tests across 191 files, typecheck on
all five flavours, eslint, dpdm, prettier.
## Coming next
| PR | Scope |
|---|---|
| 3 | `useEndpointConfig` — core (251 lines) plus a 482-line desktop
override with its own dependency polling. Split out of this PR;
different risk profile, and it deserves its own review. |
| 4 | `useAdminSettings` (20 consumers) and the config sections |
| 5 | Polling loops → `refetchInterval` |
| 6 | Finish the Processor, collapse to one client |
| 7 | Tool execution — mutation state only |
---------
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
# 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.
## 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)
```
Bumps com.sun.xml.bind:jaxb-core from 4.0.7 to 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)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
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 <dependency name> 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)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps org.sonarqube from 7.2.3.7755 to 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)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
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 <dependency name> 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)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
# 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>
# 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.
# 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.
# 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.
## Goal
Three related improvements to how policies and file state behave in the
editor: classification no longer blocks the user, policy enforcement
pipelines across a batch upload instead of waiting for the whole drop,
and file-state changes no longer re-render the entire UI.
## 1. Classification never blocks (and never versions)
Classification is metadata-only — it reads a document and records
labels; it never rewrites the file. Previously it ran like an
enforcement policy: it blocked viewing/editing behind the "Enforcing
policy…" overlay, forked a new versioned child (an `automate` entry in
version history), and could run before other policies — letting the user
in, then a later enforcement policy would fork a version and drop their
edits.
Now classification:
- **Never blocks.** A classification run never marks a file `enforcing`
(badge map + viewer overlay both skip it), so the file stays fully
viewable/editable while it runs.
- **No version bump, no history entry.** Its result is stamped onto the
file's existing stub in place (workspace + IndexedDB) — the labels just
appear as tags. It targets the document's *current leaf*, so an edit
made during the async run still gets the tags; a run that completes with
no outputs settles cleanly instead of pinning in-flight.
- **Always runs last** in an enforcement chain (regardless of configured
order, pinned at persist-time too), so every enforcement policy finishes
forking versions before the user is let in.
## 2. Pipeline policy enforcement across a batch upload
Dropping ~50 files enforced policies only *after the whole drop finished
scanning* — every file got the "Enforcing policy" overlay together, then
processing began. Root cause: the chunked `ADD_FILES` dispatches in
`addFiles` were never separated by an event-loop yield, so React batched
them into a single commit and the enforcement effect fired once over the
full list.
**Fix** (`core/contexts/file/fileActions.ts`): after each chunk, `await`
that chunk's IndexedDB writes, then yield a macrotask so React commits
the rows and runs the enforcement dispatch *before* the next chunk
scans. Files start enforcing as their rows land, overlapping with the
rest of the drop. Persistence is streamed per chunk (the policy auto-run
reads bytes from IndexedDB with no in-memory fallback).
**Second fix — bounded dispatch window**
(`proprietary/components/policies/usePolicyAutoRun.ts`): even with
streamed dispatch, the drop still *looked* serial — each dispatch POSTs
the file's bytes, and firing them all at once saturates the browser's
per-origin connection pool, so the status polls and output downloads of
already-running files queued behind the pending uploads; nothing visibly
progressed until the last upload drained. Dispatch is now gated behind a
small concurrency window (4), keeping connections free so early files
run, poll, and complete while later ones are still dispatching. The
first status poll also fires at 500ms (then the normal 2s cadence) so
fresh runs show real progress immediately. The batch test asserts the
window (dispatches overlap but never exceed 4).
## 3. Selector subscriptions for file state (no more whole-UI
re-renders)
`FileContext` published `{state, selectors}` through a plain React
context, so **every** consumer re-rendered on **every** state change —
one file's new version re-rendered the entire workspace.
**Phase 1 — infra** (`file/contexts.ts`, `file/fileHooks.ts`,
`FileContext.tsx`): the state context is replaced by a stable
subscription store (`FileStoreContext`); hooks are rebuilt on
`useSyncExternalStoreWithSelector` (the `use-sync-external-store` shim
react-redux uses — new direct dep, React 19 compatible). Each consumer
now re-renders only when its selected slice changes:
- `useStirlingFileStub(id)` → only that file's record
- `useAllFiles` → file-list changes only (immune to selection/UI churn)
- `useFileSelection`/`useSelectedFiles` → selection + the *selected*
files' records only
- `useFileUI` → its three UI scalars; `useFileContext` → files + pinned
slices
- `useFileState` keeps its whole-state contract for existing broad
consumers
A render-count test (`fileHooks.selector.test.tsx`) locks the bail-out
contract.
**Phase 2 — hot-path rows**: sidebar `FileItem` is memoized (with stable
empty-array props), so one file's change re-renders one row, not the
list. Active Files thumbnails were already memoized.
**Phase 3 — narrow the hottest consumers**: always-mounted whole-state
consumers migrated to slices — `Workbench`, `EmbedPdfViewer`, `Viewer`,
`NonPdfViewer`, `WorkbenchBar`, `ViewerContext`, `ViewerShareButton`,
`ZoomAPIBridge`, `ViewerAnnotationControls`, `ConvertSettings`,
`DismissAllErrorsButton`, `FileEditorThumbnail`,
`usePageEditorDropdownState`, `useSaveShortcut`, plus a new
non-subscribing `useFileSelectors()` for event-time reads
(`ReviewToolStep`, `useViewerReadAloud`, `useExitWarning`). Net effect:
selection/UI churn no longer re-renders the viewer/workbench, and a
version landing touches only components observing the files slice.
Broad readers (`FileSidebar`, `PageEditor`, `FileEditor`, `Redact`,
`FormFill`) deliberately stay on `useFileState` — they read most of the
state anyway.
**Hardening**: store notifications run in a layout effect (subscribers
re-render before paint — no stale frames), and outside production
`useFileSelectors()` wraps its selectors to `console.error` if one is
invoked during render (those reads don't subscribe, so render-time use
would silently go stale — not statically lintable, so it's guarded at
runtime; the full test suite passes under the guard).
## 4. Policy indicators: shared icons, non-blocking run chip, no pulse
- Badges and enforcement overlays now take their glyph from the shared
`policyCategoryIcon` map (the same source the processor's catalogue
uses) — label icon for classification, shield for security — instead of
a hardcoded shield everywhere.
- A non-blocking run (classification) shows a small accent-tinted pill
in the top-right of the Active Files card (category icon + loader) and
the normal spinning badge in the sidebar, via a new `background` badge
flag that nothing gates on. When the run finishes, the tagged files keep
a plain category badge.
- The post-run pulse/glow on sidebar badges is gone (with its `recent`
plumbing): spinner while running, static category icon when done.
## Verification
Full CI gate locally: `og:check`, `typecheck:all` (all variants),
`lint`, `format:check`, `build`, `test` (1366 — incl. the render-count
contract test, the classification-order/import unit tests, and the
61-file batch integration test driving the real dispatch → poll → import
→ chain effects), `storybook:build` — all green.
## Held for follow-up (not in this PR)
- **Reuse one PDFium engine across viewer file switches** (kills the
per-open "Loading PDF Engine" rebuild). Implemented on branch
`viewer/reuse-pdfium-engine`, but review found a confirmed leak
(orphaned PDFium handles when switching files mid-load); needs an
in-flight-load teardown before shipping.
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/<id>/<version>/*.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):
<!-- home-in-procurement.png -->
Quote builder, step 1:
<!-- quote-builder.png -->
Agreement, ready to sign:
<!-- agreement-signing.png -->
Payment and live:
<!-- stage-payment.png / stage-live.png -->
## 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.
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.
# 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 `<Login />`
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.
# 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.
`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (enforced by ESLint). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
Rule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.