mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
01751bf2f0aaeff3a7973ede383516464a064a01
5475
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
01751bf2f0 |
Improve logic for tracking which files have already been processed in policies (#6903)
# Description of Changes Replaces the `.stirling/done` folder and its friends with a ledger in the DB which tracks which documents have been processed. This should scale dramatically better since it's just a few bytes being written for each PDF processed, rather than each PDF being duplicated and held in the folder forever. It's designed to work with the current folder source, but also with S3 buckets and other sources in mind - each source will define its own strategy for ensuring it knows whether the documents have had policies run on them or not, and they all get written to the same ledger. |
||
|
|
119eb1f5ad |
Portal: move the admin route from /portal to /processor (#6933)
## What this changes
Moves the admin portal's browser route from **`/portal`** to
**`/processor`**, to match the "Processor" product name (the in-app app
switcher already says "processor").
- `PORTAL_BASENAME` `"/portal"` → `"/processor"` — the single source of
truth for all portal paths on the frontend, so every `toPortalPath(...)`
link and redirect follows automatically.
- `adminRouteExtensions` now mounts at `` `${PORTAL_BASENAME}/*` ``
instead of a hardcoded `"/portal/*"`, so the route can't drift from the
constant.
- **Backend:** `RequestUriUtils.isStaticResource` now treats
`/processor` (not `/portal`) as the SPA shell, so a direct navigation or
hard refresh to `/processor` serves the app instead of 404ing. (This is
the only backend URL reference — there's no Spring Security matcher for
it.)
- Updated the two portal route tests and the doc comments that named the
old path.
**Not changed:** the `@portal/*` import alias — that's the code layer /
flavor-layering path, not the URL. Renaming it would be a much larger,
unrelated churn.
The portal isn't publicly launched yet, so there are no existing links
to preserve — no redirect from the old path is included.
## Testing
- `task frontend:typecheck:all`, full test suite (1,211), lint, format —
green
- `./gradlew :common:test --tests "*RequestUriUtilsTest"` — green
|
||
|
|
d3638d786d |
Portal home: cut the mock blocks, wire the rest to real data (#6931)
## What this changes Cleaning up the portal home page. Most of what was under the plan card was mock — it hit endpoints that don't exist on any backend, so on SaaS it just showed a wall of "—" and "Nothing here yet". I removed the fake stuff and wired the bits worth keeping to real data. **Scrapped (all mock, no backend anywhere — self-hosted included):** - The "No usage yet" usage chart - The KPI strip (Docs/30d, Pipelines, Agents active, Eval pass rate) - The "Build a pipeline in seconds" fork wizard (fake build animation, deploy was a TODO) - The Sources/Pipelines/Agents product cards - The "Popular use cases" marketing cards - Enterprise region health - The "Try a PDF operation" runner Deleted their component/api/mock/MSW/story files too, plus the now-dead i18n keys and CSS. **Wired to real data (finished, not removed):** - The plan strip and the sidebar footer now show the **real** 30-day processed-PDF count from `/api/v1/usage/fleet-stats` (was a mock KPI). Shows "—" honestly when the backend can't compute it, never a fake number. - **Recent activity** now reads the **real audit log** — the same endpoint the Infrastructure → Audit tab uses. **Result:** one simple layout for all tiers — plan strip → recent activity + quick actions → "What runs on your PDFs" (the real policy summary). Every block is backed by data that actually exists. Net: **−3,221 / +89 lines**, 17 files removed. ## Testing typecheck (all variants), full test suite (1211), saas + proprietary builds, lint, format, storybook build, toml-sort — all green. The `unusedTranslations` test guarantees no orphaned i18n keys were left behind. |
||
|
|
e5a258a648 |
Dev: redirect bare subpath /app → /app/ when RUN_SUBPATH is set (#6934)
## Problem With `RUN_SUBPATH=app`, the app is served under base `/app/`. Vite serves `index.html` at `/app/` and redirects `/` → `/app/`, but a bare **`/app`** (no trailing slash) returns **404** — so you had to type `localhost:5173/app/` to load the app. `/app` should work too. ## Fix A small dev + preview middleware that **301-redirects `/app` → `/app/`** (query string preserved), so either form loads the app. Only active when `RUN_SUBPATH` is set; no-op otherwise. Also routed the vite `base` through the same slash-stripped `runSubpath` value the middleware uses, so a stray `RUN_SUBPATH=/app/` can't produce a doubled `//app//` base. ## Verified (dev server + prod build, `RUN_SUBPATH=app`) | Request | Before | After | |---|---|---| | `GET /app` | 404 | **301 → `/app/`** | | `GET /app?foo=1` | 404 | **301 → `/app/?foo=1`** (query kept) | | `GET /app/` | 200 | 200 (unchanged) | | `GET /` | 302 → `/app/` | 302 → `/app/` (unchanged) | Production build under the subpath still emits `<base href="/app/">` and `/app/assets/...`. Lint + format green. |
||
|
|
c64369e56c |
Classifier Policy (#6898)
## Overview Adds **AI document classification** and a **classification-aware Files sidebar**: uploaded documents are automatically tagged with document-type labels (Invoice, Contract, Lab report, …), and the sidebar groups files under editable parent categories so a large library stays navigable. > [!IMPORTANT] > **This feature only runs in the SaaS build.** Classification depends on the AI engine and team-scoped label storage, so it's gated to SaaS end-to-end: > - The sidebar grouping is a `saas/`-layer override of the `fileSidebarGrouping` seam; every other build (OSS core, self-hosted proprietary, desktop) gets the null stub and renders the **unchanged flat, recency-sorted list** — no categories, no "Other", no picker. > - The classify/labels backend endpoints are gated on `policies.enabled` (on in SaaS) and live in `app/proprietary`, so they're absent from pure OSS and dormant in self-hosted unless explicitly enabled. > - The Python classifier is reached only via that gated path. > > Shared-layer changes that do compile everywhere are inert without the engine (dormant schema/field additions) or intentional (`GetInfoOnPDF` surfacing custom metadata). ## What it does - **Classifier (engine):** reads the first/last two pages of a PDF and assigns document-type labels from an allowed vocabulary. Labels are deliberately document-*type* descriptors — no deep-content/PII detection, since only a page window is read. - **Team label vocabulary:** ~270 built-in defaults across ~15 families, seeded per team. Editable by team leaders/admins in the Classification policy settings (import/export/reset). Team-scoped and shared; **per-user personal labels are intentionally out of scope** — the vocabulary is team-level only. - **Sidebar categories:** files group under parent categories (Financial, Legal, Medical, …), busiest-first, collapsible, with a "Recent" group on top and an "Other" group for anything uncategorised. The category structure (names, icons, membership, custom categories) is **device-local and user-editable** via a "Customize" picker — the only per-user personalization; it never changes the team's label vocabulary. - Classification results are written to PDF metadata (`StirlingPDFClassification`), read back to keep files in their groups without re-parsing. ## Architecture Spans all three layers, mirroring the existing policy/source subsystem conventions: - **`frontend/editor`** — sidebar grouping seam + SaaS override, category manager, labels editor, icon palette, file grouping, tests, `en-US` i18n. - **`app/proprietary` + `app/common` + `app/core`** — `ClassifyLabelController`, team-scoped `ClassificationLabelStore` (Jpa + in-process impls, same shape as `PolicyStore`/`SourceStore`), metadata read/write. - **`engine`** — the document-classifier agent, contracts, routes, tests. ## Screenshots **Files sidebar — grouped by category (SaaS)** ### Loading view <img width="2056" height="1046" alt="Screenshot 2026-07-07 at 5 12 56 PM" src="https://github.com/user-attachments/assets/1d712da5-50ae-4349-b0cd-e62665c3ec0c" /> ### Organized in the sidebar <img width="2056" height="1045" alt="Screenshot 2026-07-07 at 5 14 05 PM" src="https://github.com/user-attachments/assets/3ea4fe21-da51-4cea-bc3a-18ce040d3d05" /> **Customize categories picker** ### Personal settings to change how labels are grouped in an individual users editor <img width="2056" height="1044" alt="Screenshot 2026-07-07 at 5 52 42 PM" src="https://github.com/user-attachments/assets/40be03ce-0f63-4d1e-b58b-cec045d01cb2" /> **Classification labels editor (team settings)** <img width="2056" height="1042" alt="Screenshot 2026-07-07 at 5 53 00 PM" src="https://github.com/user-attachments/assets/337b0739-15c9-4749-9c6b-22e3b20825b8" /> ## Testing - Frontend `task frontend:check` — green (editor + portal tests, typecheck across all flavors, lint, label-drift guard). - Backend `task backend:check` (proprietary) and `:saas:test` — green. - Engine `task engine:check` — green. |
||
|
|
f29500c138 |
Disable Portal UI for guests (#6936)
# Description of Changes Disallow SaaS guests from accessing the portal. One day we might want to make this better so they can go there but then have to sign up before doing anything useful, but this is the easiest way to disallow it for now. |
||
|
|
9d11918bd8 |
Add Storybook preview deploy + changed-stories comment on PRs (#6929)
## What
Adds a **Storybook preview** for PRs. When a PR changes any story
(`*.stories.{ts,tsx,mdx}`) or the `.storybook` config, this builds the
static Storybook, deploys it to the preview VPS on a PR-scoped port, and
comments with the URL plus an **expandable list of exactly which stories
changed**. Torn down automatically when the PR closes.
New file: `.github/workflows/storybook-preview.yml`. Nothing else is
touched.
## How
- **Detect** (`changes` job) - `dorny/paths-filter` with `list-files:
json` flags Storybook changes and captures the exact changed files.
Skipped on close and for fork PRs (which don't get the VPS secrets).
- **Deploy** (`deploy` job, only when Storybook changed) - builds the
static Storybook (`task frontend:prepare` + `frontend:storybook:build`),
tars it, and serves it from an `nginx:alpine` container on the VPS at
port `PR# + 20000` (offset from the app preview's bare-PR-number port to
avoid collisions). Mirrors `PR-Auto-Deploy-V2.yml`'s VPS SSH pattern and
reuses the same secrets.
- **Comment** - a single bot comment (replaced on each push) with the
preview URL and a `<details>` block listing the changed stories (and any
`.storybook` config changes), e.g.:
> ## 📚 Storybook preview
> 🔗 **Preview:** http://<vps>:26911
> <details><summary>2 stories changed (+1 config
file)</summary>…</details>
- **Cleanup** (`cleanup` job, on PR close) - stops the container,
removes the files, and deletes the comment.
## Validation
- Static Storybook builds locally (`task frontend:storybook:build` →
`frontend/storybook-static`, 151 stories).
- Confirmed `task frontend:prepare` regenerates the un-committed
`material-symbols-icons.json` that stories import, so a fresh CI
checkout builds (added it before the build step).
- Comment-markdown logic unit-checked against a sample changed-files
list.
- YAML validated; action pins match the repo (`setup-node` v6.4.0 / node
22, same `paths-filter`, `setup-bot`, `harden-runner`).
## Note
The VPS deploy mirrors the proven `PR-Auto-Deploy-V2` machinery but
couldn't be exercised end-to-end from a dev box (needs the VPS secrets)
- the first live run on a Storybook-touching PR will confirm the
deploy/serve/cleanup path. Everything build- and comment-side is
validated locally.
|
||
|
|
8d2bb14f99 |
Add portal user management and access control (#6913)
# Description of Changes
Portal access control + user management
What this does
- Adds server-side portal access enforcement: a ResourceGrant ACL (owner
→ admin → grant → default policy) gates the portal via
@resourceAccess.canUsePortal(), so access is authoritative on the
backend, not just hidden in the UI.
- New proprietary/access module: ResourceAccessService +
ResourceAccessSecurity, PrincipalResolver (default + SaaS + team-lead
lookup), OwnershipService, ResourceGrantController, and a SecretMasker
for safe config display.
- Exposes an authoritative portalAccess flag on /me (AuthController /
AdminUserSummary); drops the old org-principal shortcut.
- Full portal Users page: team + member management (members table,
invite, move-to-team, new/rename team, reset password, access controls,
confirm modals) wired to real user/team/grant endpoints.
- Per-flavor capabilities seam (usersCapabilities): self-hosted
org-admin gets everything; SaaS is trimmed to what a team leader can do
(no ROLE_ADMIN ever surfaced).
SaaS blockers (separate follow-up PR)
The portal Users page works on self-hosted but 403s on SaaS (it calls
the admin API hasRole('ADMIN'), and SaaS users are ROLE_USER). To ship
the portal on SaaS:
- Add a @app/portal/usersBackend seam and point the SaaS build at the
existing SaasTeamController (no new backend).
- Resolve the leader's team-id on SaaS and map member/invitation shapes
to the portal Member type.
- Add pending-invitation management (list + cancel) - the parity gap vs
the editor.
- Re-enable the roster remove action on SaaS against
SaasTeamController's remove-member endpoint.
<img width="1426" height="464" alt="image"
src="https://github.com/user-attachments/assets/7a441a35-7a57-472f-a8c7-e6d8ae998439"
/>
<img width="492" height="722" alt="image"
src="https://github.com/user-attachments/assets/d4e8a088-b2eb-4326-9e00-7ada6eb72a85"
/>
---
## Checklist
### General
- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings
### Documentation
- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)
### Translations (if applicable)
- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
### UI Changes (if applicable)
- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)
### Testing (if applicable)
- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
---------
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
|
||
|
|
b53aaa7d03 |
Portal procurement: enterprise licence-key mechanism (generate at trial, upgrade on subscription, view/download) (#6902)
Builds on the procurement vertical slice (#6861). Adds the **enterprise licence-key mechanism**: a Keygen licence is generated at trial, upgraded in place when the committed subscription is created, and is viewable/downloadable in the portal. Flag-gated — ships with the mock as default until Keygen env vars are wired. ### What it does - **Offline / air-gapped licence** is a new **priced add-on** on the quote ($12k/yr, flat, alongside indemnification / training / QBR). - **Licence key visible from the trial step** — the portal shows the key with **Copy**, and (when the offline add-on is bought) a **Download offline licence (.lic)** button. - **Real Keygen client, called directly from Java** (`KeygenEnterpriseLicenseService`), behind `stirling.keygen.enabled`; `MockEnterpriseLicenseService` stays the default. All creds are env vars (`STIRLING_KEYGEN_*`) — nothing committed. - **Provisioning is driven by the Stripe `customer.subscription.created` event** (source of truth), not a UI action — so a sales-led deal entered manually in Stripe provisions a licence too. The webhook calls a new admin `POST /api/v1/procurement/provision`, which upgrades the trial licence **in place** to the committed annual term, **valid immediately** (no wait for payment). The deal stays in the payment step so the outstanding invoice remains visible. - Offline `.lic` is checked out `base64+ed25519` (signed, unencrypted) so the self-hosted `KeygenLicenseVerifier` validates it fully offline. ### Not in scope (deliberate, follow-ups) - Cloud entitlement flip — a **cloud** customer sees/downloads the key but the running cloud product doesn't unlock yet (self-hosted/air-gap **are** unlocked by the key). Immediate next PR. - `invoice.paid → fully-live` / `payment_failed → suspend` webhook safety-net. ### Companion change (separate repo) - The Stripe-webhook wiring that calls `/provision` lives in the **Stirling-PDF-SaaS** repo (committed on `v3`, not part of this PR): `stripe-webhook` routes enterprise-committed `subscription.created` → `provisionProcurement()` → the Java admin endpoint. ### Prod setup required - Create the committed-enterprise **Keygen policy with `scheme=ed25519`** under the existing account, set `STIRLING_KEYGEN_ENABLED=true` + account/token/policy env vars. ### Verified saas `:saas:test` (procurement) · portal typecheck / eslint / prettier · 82 portal tests · `deno check` on the webhook handler. ### Review follow-ups (PR review, tracked) Low-hardening fixes applied in `85369633ed`: keep Keygen response bodies out of thrown/logged messages; fail-fast at startup when the flag is on but creds are missing; gate the offline `.lic` on the *accepted* quote (not the latest draft). Deliberately deferred, tracked here: - **Pre-flag verification.** Before `stirling.keygen.enabled=true`, confirm the id-vs-key addressing against live Keygen. (The shipping self-hosted edge addresses licences by URL-safe key in the path and Keygen docs allow it, so the client mirrors that — but confirm empirically with the real committed-enterprise policy.) - **No auto-revoke on non-payment.** Provision issues an immediately-valid annual licence before payment settles; `invoice.paid → live` and `payment_failed → suspend` are out of scope here. Note the offline `.lic`, once downloaded, verifies offline for the full term and **can't be revoked** — so the real mitigation for the offline case is a shorter bridge term until `invoice.paid`, not just wiring `suspend`. Enterprise is sales-led/ADMIN-gated, so this is a collections concern, not mass abuse. |
||
|
|
3fa0f30d43 |
Portal: prep for SaaS launch — hide unfinished sections, fix api client, docs link (#6921)
## What this changes Getting the portal ready to show the world on SaaS. A few things bundled in here: **Developer docs tab** — now opens https://docs.stirlingpdf.com/ in a new tab instead of taking you to an empty page (we haven't built the in-app docs page yet). **Hid the bits that aren't finished yet — SaaS only:** - Took the Agent Builder button off the Sources page. - Removed the Components page. - Infrastructure: the tabs that aren't ready (Deployments, Security, Models, Storage) are greyed out as "coming soon". API keys and Audit stay live. Also dropped the "Manage editor deployment" button. - Removed the floating AI assistant blob. **Fixed the SaaS api client.** Before this, only the usage/billing page actually reached the backend — everything else (sources, users, policies, etc.) was going to the vite dev server with the wrong login, so it never worked. Now every portal call goes to the one SaaS backend using the Supabase login. Self-hosted is left exactly as it was — all the SaaS hides go through the saas override layer, so self-hosted still shows everything. ## Testing typecheck (all variants), full test suite, both builds, lint + format — all green. |
||
|
|
9ea848570f |
Wire portal audit tab and documents to real audit data (#6912)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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. |
||
|
|
d6061eb0aa |
Support tool selection in Pipelines page in Portal (#6905)
# Description of Changes Redesigned the Portal Pipelines page so pipelines are created and edited on their own dedicated builder page, replacing the previous modal composer and inline detail card. ## Screenshots ### Pipelines list The redesigned list with summary KPIs; each row opens that pipeline's own page.  ### Pipeline builder The dedicated create page: pipeline settings (sources, trigger, output) above, operations and per-tool settings below.  ### Tool picker Type-to-filter, category-grouped picker for adding an operation to the pipeline.  ### Editing a pipeline An existing pipeline in the builder: reorderable steps, per-tool settings, and run/delete actions.  |
||
|
|
1759e0bdd5 |
feat(portal): wire procurement "Schedule a call" to Calendly (#6920)
## What The procurement flow's **Schedule a call** action (deal-status hero → side modal) was a mock: a fake "SE" avatar and four hardcoded time-slot buttons that just closed the dialog. This wires it up to the real Calendly booking widget the admin provided. ## How - **New `CalendlyInline` component** (`portal/components/procurement/CalendlyInline.tsx`) - Lazily loads `assets.calendly.com/assets/external/widget.js` via the existing `@app/utils/scriptLoader` — only when the modal actually opens, deduped across reopens. - Calls `Calendly.initInlineWidget()` explicitly so it rebuilds on reopen / theme change. - Colours track the portal's light/dark theme (`useTheme`) via Calendly's `background_color` / `text_color` / `primary_color` params, mapped to the portal design tokens (surface / text-1 / primary), plus `hide_event_type_details=1`. - Graceful fallback to an "open in a new tab" link if the script fails to load. - Base URL overridable via `VITE_CALENDLY_URL` (defaults to the group-discussion link). - **`ScheduleCallModal`** now renders `<CalendlyInline />` instead of the mock; copy moved into i18n (`portal.procurement.schedule.*`). - `SideModal` gains a `wide` variant so the embed has room; removed the now-dead `.portal-se*` / `.portal-slots*` CSS and `SLOTS` constant. ## Notes / follow-ups - No app-level CSP blocks `calendly.com`, so the embed loads without config changes. - Verified with the portal typecheck (`tsc -p src/portal/tsconfig.json`) and ESLint on the changed files; only pre-existing Storybook/msw dev-dep type errors remain. <img width="1160" height="642" alt="image" src="https://github.com/user-attachments/assets/d9b5d92d-ea7e-4862-9f35-a71f65392a2c" /> <img width="3744" height="1990" alt="image" src="https://github.com/user-attachments/assets/0b8002c6-dd3e-41e6-8e8b-6faf6090314c" /> <img width="2620" height="1928" alt="image" src="https://github.com/user-attachments/assets/781b7a60-7065-4a7a-b9f7-1cdb0dece7b3" /> |
||
|
|
514b020f74 |
Portal: real Free PDF Editors usage card (self-hosted) (#6919)
## What Replaces the **mocked** "Free PDF Editors" fleet card on the portal Usage page with live figures. Cost stays a literal `$0`; any figure that can't be computed renders **N/A** (never a misleading 0). | Metric | Self-hosted source | |---|---| | **Editors deployed** | total users (`UserRepository.count()`) | | **Active this month** | distinct `source=WEB` principals active in 30d (excl. `UI_DATA` polling), clamped ≤ deployed | | **PDFs edited** | cumulative `PDF_PROCESS` + `FILE_OPERATION` audit events that are **free UI runs** | ## Why the counting approach "Free operations = UI tool runs." Two dead ends first: - **Billing/PAYG is the wrong source** — it *deliberately discards* free ops (classified `BYPASSED`, no DB row); its tables only hold billable (API/AI/automation). - **Raw audit is also wrong** — a tool controller emits `PDF_PROCESS` for UI **and** API/AI/automation calls, and billable traffic exists on every tier. So the count is **audit filtered to free UI runs**. Audit events gain a `source` column, stamped from the always-on signal `BillingCategoryClassifier.classify(...) == BYPASSED` (not API-key auth, no `X-Stirling-Automation` header, not `/api/v1/ai/`) — zero billing-module coupling. Captured on the request thread (`AuditService.captureCurrentSource`), carried via MDC in `ControllerAuditAspect` (same propagation as `requestId`), persisted by `CustomAuditEventRepository`. The count filters `source = 'WEB'`. ## Endpoint `GET /api/v1/usage/fleet-stats` — admin-gated, EE-only. Returns `null` per field when EE auditing is off (→ N/A). ## Frontend - New `portal/api/fleetStats.ts` → `apiClient.local` (this instance's backend). - `FreePdfEditorsCard` rewired to `useAsync(fetchFleetStats)`; preview badge removed, `null`→"N/A", loading→"—". ## Tests `:proprietary:build` green — `FleetUsageControllerTest` (4) and `CustomAuditEventRepositoryTest` (+2 for source-from-MDC) pass; spotless clean. ## Notes / follow-ups - `deployed` currently counts all users incl. disabled — refine to enabled-only later. - **SaaS** (team-scoped endpoint + a `fleetStats.ts` override) is deferred to a follow-up riding the portal-SaaS layering PR #6900. - Depends on EE auditing running at `AuditLevel ≥ STANDARD` for the audit-derived figures; otherwise they show N/A. |
||
|
|
18b0b19a67 |
Block file exit points while a per-file policy run is enforcing (#6904)
## What
While a per-file policy run is in flight, the editor now blocks every
way the file can leave the app, and shows why:
- **Viewer** — a blocking overlay with live progress ("Enforcing
policy…"). Dismissible: collapses to a corner badge (top right, tinted
with the policy's accent) so the file stays readable while the run
finishes.
- **Workbench bar** — Print / Download / Save As / Share are disabled
with an explanatory tooltip and progress bar. The Ctrl+P shortcut and
the form-fill bar's "Download PDF" button are covered too.
- **File lists** — the file sidebar, file-editor thumbnails, and files
page show a spinning shield badge on the affected file, and thumbnail
hover actions (download / upload to server) are blocked with the same
tooltip.
Once a run settles, everything unblocks — including FAILED and CANCELLED
runs. A failed check surfaces through the run's activity feed; it never
locks the user out of their file.
## Why
Upload-triggered policies exist so the enforced output is what leaves
the app. Before this, a file could be printed, downloaded, or shared
while its policy run was still processing.
## Also in here
- **One shared `PolicyBadges` component** — the sidebar, thumbnails, and
files page each had their own copy of the badge markup/CSS and had
drifted (different sizes, tints, missing spinner and glow on the files
page, hardcoded English tooltips). All badge surfaces now render the
same component: accent-tinted shield, spinner while enforcing, one-off
glow when recent, i18n'd tooltips.
- **Cascade fix:** outputs imported from reconciled
(server-rediscovered) runs are now tagged `derivedFromTool`, stopping an
auto-run → import → auto-run loop that produced ever-growing
`_sanitized_sanitized…` filename chains on fresh devices.
- **Core stub for `policyRunStore`** so the core build compiles —
`WorkbenchBar` and `ViewerShareButton` resolve `usePolicyRuns` via
`@app/*`.
## Testing
- `task frontend:check` green: proprietary typecheck, ESLint + dpdm,
Prettier, 915+ editor + 81 portal unit tests.
- `typecheck:core` / `saas` / `desktop` variants all pass.
- Enforcement flow exercised manually against a live backend with an
upload-triggered policy (overlay + progress during the run, dismiss to
corner badge, unblock on completion).
---------
Co-authored-by: James Brunton <jbrunton96@gmail.com>
|
||
|
|
a8bda9240c |
feat(portal): replace free-tier carousel with static welcome hero (#6901)
## What this PR does Redesigns the portal home to match the marketing demo, across all tiers. - Swapped the old rotating welcome carousel for a static **welcome hero** on the free tier - Subscribed (Processor) + enterprise get a **deployed-editor hero** — shows the live instance (host, version, active users) with an *Open in browser* button, pulled from the real editor-deployment API (same data the Editor admin page uses) - Added a time-of-day **greeting** on the paid tiers - Rebuilt the **"Finish setting up" checklist** so it's real: the counts and tick-offs come from the actual policies + sources (a step is done when there's at least one), not hardcoded numbers - *Download the PDF Editor* → `https://stirling.com/download`; the other steps deep-link to Policies / Sources - **Procurement is now a bolt-on to any tier** — if a deal's in flight the deal-status hero drops into the hero's footer, otherwise you get the setup checklist - All new copy is translated (en-US) and it reuses the shared UI kit, icons and design tokens ## Tidy-ups / fixes found along the way - The subscribed hero was shadowing the real `/v1/editor/deployment` endpoint (broke the Editor admin page) — now reuses it - Renamed the hero's CSS namespace to `.portal-welcome` so it stops clashing with the procurement hero's `.portal-hero` - Refactored the merged procurement component into a shared `useProcurement` hook + banner + flow, so the deal hero can live inside the tier hero — `/procurement` route unchanged ## Screenshots <img width="1258" height="1338" alt="pr-free" src="https://github.com/user-attachments/assets/9bb7db44-5f8e-4388-857a-7f113c2d7d82" /> <img width="1258" height="862" alt="pr-enterprise" src="https://github.com/user-attachments/assets/f1f36faa-1467-404e-9036-dab12e3d0b54" /> <img width="1258" height="944" alt="pr-subscribed" src="https://github.com/user-attachments/assets/775f1228-f182-47b5-82ff-7ac6d5932bd9" /> --- ## Checklist ### General - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### UI Changes - [x] Screenshots demonstrating the UI changes are attached ### Testing - [x] Portal typecheck, ESLint and Prettier pass on the changed files - [x] Verified all states in Storybook and ran the app locally via `task dev:portal` |
||
|
|
0692058602 |
Embed admin portal as its own app in the jar behind buildWithPortal (#6911)
## What
Lets the admin portal ("Stirling Processor") ship **inside the JAR**,
gated by a build flag. On `main` the portal already exists as a lazy
`/portal/*` route in the editor but isn't included in production builds
and isn't reachable in a login-enabled server. This PR makes it a
**flag-gated, directly-navigable** part of the editor bundle, and wires
it into the PR preview deployment so it can be tried live.
It keeps the exact architecture `main` uses (portal = a lazy chunk of
the editor, not a separate app), so it inherits all the editor's global
providers/styles and there's no second build to maintain.
## How
**Frontend - gate the existing lazy route**
([`adminRouteExtensions.tsx`](frontend/editor/src/proprietary/routes/adminRouteExtensions.tsx))
```ts
const includePortal = import.meta.env.VITE_INCLUDE_PORTAL === "true" || import.meta.env.DEV;
const PortalApp = includePortal ? lazy(() => import("@portal/PortalApp")) : null;
```
Vite bakes the env to a literal, so when off the dynamic import is
**tree-shaken out entirely** (no `PortalApp` chunk emitted). Always on
in dev. `VITE_INCLUDE_PORTAL` is typed in `vite-env.d.ts` and declared
(default `false`) in `editor/.env`.
**Gradle** ([`build.gradle`](app/core/build.gradle)) -
`-PbuildWithPortal=true` forces `buildWithFrontend=true` and sets
`VITE_INCLUDE_PORTAL=true` on the editor build. Process-env takes
priority over `.env`, so the flag wins for JAR builds while plain `vite
build` / Cloudflare Pages default to off.
**Backend - make the shell reachable**
([`RequestUriUtils`](app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java))
- permits `/portal` + `/portal/*` as public SPA routes. The editor keeps
its JWT in localStorage (not a cookie), so a direct nav/refresh to
`/portal` isn't authenticated at the server and would otherwise redirect
to `/login` and never load. Serving the shell pre-auth (like the editor
root already is) lets it load; **access control is unchanged** - the
portal has its own auth gate + `RequirePortalAccess`, and its data APIs
stay protected.
**Docker** - the embedded Dockerfiles take `ARG BUILD_PORTAL=false` →
`-PbuildWithPortal=${BUILD_PORTAL}`. Default off, so official
`push-docker` images do **not** bundle the portal.
**CI - scoped to the PR preview deploy only**
([`PR-Auto-Deploy-V2.yml`](.github/workflows/PR-Auto-Deploy-V2.yml)) -
the one job that builds the JAR and comments owns all portal wiring:
passes `BUILD_PORTAL=true`, enables the portal's backend features
(`POLICIES_ENABLED`, `STIRLING_BILLING_ACCOUNT_LINK_ENABLED`), and adds
an "Admin portal included" line (linking `/portal` via the direct IP) to
the deployment comment. `push-docker`, `build.yml`, `test-build-docker`,
and the shared paths-filter are untouched.
## Validation (real, in the JAR)
Built and booted the JAR with `-PbuildWithPortal=true` and login
enabled:
- `/portal` and `/portal/users` load via direct nav and render **fully
themed** (dark surfaces, gradients, filled buttons).
- Editor-only build (`-PbuildWithFrontend=true`, no portal flag) →
editor ships, **0 portal chunks** (tree-shaken).
- `-PbuildWithPortal=true` → `PortalApp` chunk present.
Green: `frontend:check:all` (typecheck all variants, lint, format,
build, tests incl. the `VITE_*` env guard), backend compile,
`RequestUriUtilsTest`, spotless.
## Notes
- **Official images never bundle the portal** (Dockerfile default off);
only the PR preview does. Flip `BUILD_PORTAL` / `-PbuildWithPortal` to
include it elsewhere.
- The `/portal` shell being public is the one deviation from `main`, and
it's required for the route to be reachable at all in a login-enabled
server; data access is still fully gated.
|
||
|
|
8150d16b6f |
Support bidirectional mapping for Change Metadata (#6906)
# Description of Changes The Change Metadata tool was missed from the bidirectional mappings added in #6867. This PR adds it to the list of supported tools. |
||
|
|
c8f238ae60 |
Portal/editor switcher (#6907)
Adds the portal's top-left app switcher to the editor sidebar, so you can jump between the two apps from either side. - Both sidebars render the same shared `AppSwitch` component (sui dropdown). - Switching is client-side (no page reload); portal→editor no longer breaks when `VITE_EDITOR_URL` is unset. - Editor side is admin-gated (`portalAccess`) and only exists in flavors that ship the portal — core/desktop stub it out, same seam pattern as the portal routes. - Fixes en route: dropdown menu stacking in the editor sidebar, sui dropdown item button reset, stale `dist-portal` ESLint ignore. |
||
|
|
8df49ac053 |
feat(portal): build portal for SaaS and self-hosted via file-override layer (#6900)
## What Ground-work so the admin portal can build for the **SaaS flavor** alongside self-hosted, using the editor's existing **build-time file-override** mechanism — no runtime flavor flags. This PR demonstrates SaaS end-to-end (single-login + Usage page loading via the inherited Supabase session) without changing self-hosted behaviour. This is intentionally scoped as foundations, not the whole feature. ## How **Build hook** - `tsconfig.saas.vite.json`: `@portal/*` now cascades (`src/saas/portal/*` → `src/portal/*`); added `@portalCore/*` for the explicit base path. - New `src/portal-saas/` layer (sibling of `src/portal`) holds SaaS-only overrides, so `@app/*` resolves only editor layers and `@portal/*` only portal layers. Self-hosted builds never import it (tree-shaken). **Seams live in the api-client + composition layers — never in page components** - `saasApiBase` — base URL source (self-hosted: `VITE_SAAS_API_URL`; SaaS reuses the single `VITE_API_BASE_URL` backend). - `portalSaasSession` — flavor-agnostic token from the shared Supabase client. - `PortalAuthBoundary` — self-hosted: Spring `AuthProvider` + `AuthGate`; SaaS: Supabase `AuthProvider` + session-only gate (inherits the SaaS session, so no second login). **Link concept pulled out of the Usage page (one clean cut)** - `Usage` is now a link-free wallet renderer with generic `onWalletLoaded` / `onReauth` callbacks; it always loads the wallet and has zero flavor awareness. - `PortalBillingGate` is the single flavor seam: self-hosted gates on link (prompt when unlinked; wires the callbacks onto link/tier + re-auth), SaaS is a passthrough that renders `Usage` directly. - Keeps the flavor switch out of the page entirely (no per-flavor code in `Usage`). ## Testing Green locally and in CI (CI runs the umbrella `task frontend:check:all`): - `task frontend:typecheck:all` — clean across all 7 build variants - `task frontend:test` — vitest suites pass (portal + saas cover this change; 146 tests) - `task frontend:build:saas` and `task frontend:build:proprietary` — both green - `task frontend:lint` and `task frontend:format:check` — clean ## Also in this PR (added after the initial foundations) - **Tier from wallet + full link-layer excision on SaaS.** `TierContext` no longer reads `LinkContext` (via a `usePlanTier` seam: self-hosted from link state, SaaS from `wallet.status`), and the SaaS `PortalProviders` drops `LinkProvider` / `AccountLinkProvider` / `LinkModalHost` entirely — the link machinery is *absent* from the SaaS bundle, not mounted-but-inert. ## Deliberately out of scope (follow-ups) - SaaS-only read-only "connected servers" settings view. - Shared wallet source so the SaaS tier badge and the Usage page don't both fetch `/payg/wallet` (harmless double-fetch today). |
||
|
|
328cd8c664 |
Claude skills walkthrough, feature-walkthrough, and before/after (#6862)
# Description of Changes Add review only Claude skills ### Skills (Using https://github.com/Stirling-Tools/Stirling-PDF/pull/6655 as example for example files) - **`/ui-walkthrough`** - captures every state of a feature's UI (empty/populated/dialogs, light + dark + RTL) via the stubbed Playwright harness, builds a single-image HTML report with a global light/dark slider, then runs visual-consistency + UX review passes. `--fix` auto-applies safe fixes and re-shoots. [REAL-ui-walkthrough-pr6655.html](https://github.com/user-attachments/files/29594945/REAL-ui-walkthrough-pr6655.html) - **`/feature-walkthrough`** — explains a branch end-to-end (Mermaid diagrams, annotated file map, before/after, "try it locally") so a reviewer with no prior context can follow it. [REAL-feature-walkthrough-pr6655.html](https://github.com/user-attachments/files/29594950/REAL-feature-walkthrough-pr6655.html) - **`/ui-before-after`** — generic branch/PR visual diff: derives the changed UI from the diff, screenshots before (base) vs after (head), pixel-diffs, auto-crops each pair to the region that actually changed (full-page only when the change is page-wide), and builds PR-ready before/after montages. [REAL-ui-before-after-pr6655.html](https://github.com/user-attachments/files/29594954/REAL-ui-before-after-pr6655.html) --- ## 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. |
||
|
|
f703a67817 | Fix cert sign not showing under certain instances (#6908) | ||
|
|
105af51100 |
Update Frontend 3rd Party Licenses (#6889)
Co-authored-by: Ludy <Ludy87@users.noreply.github.com> Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> |
||
|
|
57bf17d348 |
Fix missing app icon on Linux/Wayland (#6875)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Co-authored-by: Ludy <Ludy87@users.noreply.github.com> |
||
|
|
11df30b914 | feat(ui): add dedicated third-party license sections to settings (#6820) | ||
|
|
43162c40ad | chore(frontend): remove unused OG images (#6826) | ||
|
|
be57f11747 |
Improve type safety of tool definitions (#6895)
# Description of Changes Followup work requested in review of #6867. Currently, there is nothing enforcing that the endpoint chosen in the tool config is the correct mapping for `toApiParams`, so theoretically it's possible for a tool to be set up to call an endpoint with the wrong API params for it. There's also nothing currently enforcing that `toApiParams` and `fromApiParams` are compatible with each other (using the same types). This PR changes it so that instead of creating the config object directly, tools create it via a generic function, which enforces that all of the relevant mappings are using compatible types. |
||
|
|
8ba8f69252 |
Consolidate buttons and related components (#6787)
SegmentedControl, Chip, ChipFlow. Bring in the portal dark mode theme and other small fixes to issues I found during testing |
||
|
|
be97268a7c |
SUI - setting up mantine backed SUI components (#6890)
## Summary
Converts SUI's existing Select and Slider to Mantine-backed
implementations, and adds three new Mantine-backed SUI components:
MultiSelect, NumberInput, ColorInput.
All five components follow the same contract as the rest of the SUI
catalogue:
- Imported from `@app/ui` — Mantine is an implementation detail
- Explicit prop allowlists: appearance props (color, variant, radius,
classNames, styles) are locked internally to SUI tokens; only
behavioural props are exposed
- Labels and error messages stripped from the interface — callers use
`<FormField>` for both. The components take an `invalid` flag that
applies error styling only; Mantine never renders its own message
element, so the text can't appear twice
- `aria-label` / `aria-invalid` / `aria-describedby` and `FormField`'s
injected `required` are forwarded, so the injected accessibility wiring
reaches the underlying input. Mantine drops some of this wiring
internally (`aria-describedby` on inputs, all aria props on Slider's
thumb, `required` on MultiSelect's field), so `ariaForwarding.ts`
re-applies it to the DOM node and `ariaForwarding.test.tsx` locks the
contract in
- Typed escape hatches (`comboboxProps`, `popoverProps`, `rightSection`)
documented for the z-index-in-modal use case
**Select** — rebuilt from native `<select>` to Mantine combobox. Gains
searchable/clearable. `onChange` now receives the value string directly,
not a DOM event — callers updated.
**Slider** — rebuilt from native `<input type="range">` to Mantine
Slider. Gains accessible keyboard navigation and `marks` support.
**MultiSelect, NumberInput, ColorInput** — new components. The behaviour
(multi-select combobox, number stepper, colour picker) is too complex to
hand-build correctly; Mantine provides it for free behind a locked SUI
interface.
Also wires `suiCssVariablesResolver` into the Storybook
`MantineProvider` so Mantine combobox/popover dropdowns follow the SUI
palette in dark mode, and adds `"neutral"` accent variant to
`IconBadge`.
## Usage
```tsx
import { Select, Slider, MultiSelect, NumberInput, ColorInput } from "@app/ui";
import { FormField } from "@app/ui/FormField";
// Select — onChange receives string | null, not a DOM event
<FormField label="Retention">
<Select options={options} value={value} onChange={setValue} searchable clearable />
</FormField>
// Slider — same external API as before, now with marks support
<FormField label="Confidence">
<Slider value={v} onChange={setV} min={0} max={1} marks={[{ value: 0.5, label: "0.5" }]} />
</FormField>
// New components
<FormField label="PII types">
<MultiSelect data={options} value={value} onChange={setValue} searchable clearable />
</FormField>
<FormField label="Opacity">
<NumberInput value={opacity} onChange={setOpacity} min={0} max={100} suffix="%" />
</FormField>
<FormField label="Watermark colour">
<ColorInput value={color} onChange={setColor} />
</FormField>
```
## Notes
- **Select `onChange` is a breaking change** — receives `string | null`
instead of a DOM event. All existing callers in this repo are updated.
- The policy PR (`main` WIP) depends on this merging first.
- Stories for all five components are under **Primitives / Forms** in
Storybook.
|
||
|
|
7bd3826178 |
Portal procurement: real pricing/trial/quote spine + linked-gated checkout (vertical slice) (#6861)
## What this is
The enterprise procurement flow, built into the customer portal as a
**vertical slice** — one linked account can go the whole way from trial
to a paid, committed subscription, using real Stripe under the hood.
Procurement no longer lives as a nav tab. It sits on **Home** as a
deal-status hero and expands into a full-screen takeover, matching the
marketing prototype.
## The journey (what a customer does)
- **Start a trial** in one click — the deadline and next steps show on
the Home hero (no card, mock licence).
- **Build a quote** — a short form (volume → commitment & service →
details); pricing is computed server-side.
- **Generate the quote** — this creates a real **Stripe Quote** with a
proper **PDF** you can download and share, and it becomes a milestone
you can come back to.
- **Review & sign the agreement** — one combined agreement (MSA + Order
Form + EULA + DPA) with an itemised order form and an "I agree" (no
e-signature yet).
- **Accept** — Stripe creates the committed annual **subscription** and
its **first invoice**, which you can **pay or download right in the
app** (no waiting on email).
- Edit a quote any time — it remembers your inputs and company name; the
old Stripe quote is cancelled so it can't still be accepted.
- The hero also has quick actions: **key documents**, **invite
teammates**, **schedule a call**, and a **trial countdown** you can
extend.
## Architecture — Supabase vs Java
Pricing, deal/quote state, and the commercial journey live in **Java
(`:saas`)**. Everything that touches **Stripe** (writes + PDFs) lives in
**Supabase edge functions** — Java has no Stripe SDK and only reads
Stripe via the sync mirror. The portal calls both.
```mermaid
flowchart LR
Portal["Portal (React · editor/src/portal)"]
subgraph JAVA["Java :saas backend (trusted cloud)"]
Pricing["Pricing engine (volume bands, SLA, term, add-ons)"]
Deal["Deal + quote state, journey, snapshot"]
Trial["Trial (mock Keygen licence seam)"]
Authz["Auth: team resolve + leader gating"]
Mirror["Reads Stripe via sync mirror (stripe.* tables)"]
end
subgraph SUPA["Supabase edge functions (own Stripe)"]
Issue["issue-procurement-quote → create + finalize Stripe Quote"]
Accept["accept-procurement-quote → subscription + finalize invoice"]
Pdf["get-procurement-quote-pdf → proxy the quote PDF"]
RPC["SECURITY DEFINER RPCs (read/write stirling_pdf, enforce team/leader)"]
end
Stripe["Stripe (Quotes · Subscription · Invoice)"]
Portal -->|"price / build / trial / agreement / snapshot"| JAVA
Portal -->|"issue / accept / download PDF"| SUPA
SUPA --> Stripe
SUPA --- RPC
Mirror -. reads .-> Stripe
```
| Top-level feature | Handled in |
|---|---|
| Quote pricing (bands, SLA, term, add-ons) | **Java** |
| Deal + quote state, journey, snapshot | **Java** |
| Trial start / extend (mock licence) | **Java** |
| AuthN/Z (team resolve, leader gating) | **Java** |
| Issue quote → Stripe Quote + PDF | **Supabase edge fn** |
| Accept → subscription + invoice | **Supabase edge fn** |
| Quote PDF download | **Supabase edge fn** |
| Reading Stripe state | **Java** (sync mirror) |
| `stirling_pdf` writes from edge | **SECURITY DEFINER RPCs**
(service-role only) |
## Screenshots
<!-- Drag each PNG into the box below it before publishing. -->
**Home deal-status hero (trial)**
<img width="1920" height="1009" alt="hero-check"
src="https://github.com/user-attachments/assets/7ae21831-9578-4f4d-b91a-d3ab2cb171dc"
/>
**Issued quote milestone (with breakdown)**
<img width="1920" height="1009" alt="milestone-breakdown"
src="https://github.com/user-attachments/assets/3a463c67-3c6e-4f93-a1cc-59b250d54cc9"
/>
**Agreement step (itemised order form)**
<img width="1920" height="1009" alt="agreement-itemised"
src="https://github.com/user-attachments/assets/1a4efa16-d0a3-41d0-849c-a125b1492a34"
/>
**Key documents**
<img width="1920" height="1009" alt="keydocs-modal"
src="https://github.com/user-attachments/assets/5672d1d2-99d9-499e-9edf-d485df378e7f"
/>
**Subscription created (pay / download invoice)**
<img width="1920" height="1009" alt="accepted-check"
src="https://github.com/user-attachments/assets/3a8cbe9f-e72c-4389-b365-0c1749108b6f"
/>
## Mocked for now (scaffolding, not wired to real backends)
- **Key documents** ledger — static demo list.
- **Schedule a call** — static solutions-engineer + time slots.
- **Invite teammates** — routes to the existing Users view.
- **Simulate payment received** / **Reset procurement** — demo controls,
**off by default** in prod (flag-gated), 404 unless enabled.
## Deferred (separate follow-up PRs)
- **Real `invoice.paid` webhook** → go-live (today a demo button stands
in).
- **Keygen licence controller** — real licensing (currently a mock
seam).
- **Document sharing**.
- **Stirling admin / Deal Desk** view.
- **Minimum ACV floor** — pending a number from marketing (server-side
enforcement is a one-liner once decided).
## How to test
- **Frontend, no backend:** runs against MSW mocks (Storybook + mocks-on
dev) — the whole journey is clickable.
- **Real end-to-end:** apply the migrations (Flyway `V27–V29` / Supabase
`20260701–20260707`), deploy the three edge functions, ensure
**Invoicing Plus** is enabled on Stripe, and set
`STIRLING_PROCUREMENT_DEMO_CONTROLS_ENABLED=true` if you want the demo
controls.
- Paired SaaS PR: **Stirling-Tools/Stirling-PDF-SaaS#318**.
## Notes for reviewers
- Pricing is server-authoritative (client sends config, never amounts).
- Security review done: edge functions validate the JWT and enforce
**team membership** (and **leader** for issue/accept) via the RPC; demo
endpoints are flag-gated off. Only open item is the ACV floor (policy).
|
||
|
|
17aa71850c |
Convert to consistently use JS modules (#6854)
# Description of Changes Modernises the codebase and gets rid of warnings where Node complains that it doesn't know what type of JS it's supposed to be reading on `.js` files. We might as well update everything to just use correct JS syntax instead of keeping with some files having Node-specific imports. |
||
|
|
20204f0ddc |
Improve consistency and reliability of tools in Stirling Engine (#6855)
# Description of Changes A few changes to improve things in the engine: - Changed the PDF to Markdown code to be a real tool in Java, to remove the need for the `pdf_ingest` code, which looked a bit like an agent but wasn't behaving as an agent. It's now just covered automatically by the edit agent. - Noticed that 0-parameter-endpoints were previously being ignored by the `tool_models` generator, so some tools which require no params were being mistakenly excluded. - Removed tools which currently never succeed like Add Stamp, Cert Sign, and Overlay, because they require the supporting files to be sent in a different location in the API call, which we don't currently do. Ideally, we'd add proper support for this, but we're better off now removing support for these tools rather than just have them crash. We can re-add these tools in a future PR properly. |
||
|
|
1df6a1759c |
Set App version to v2.14.1 (#6891)
Upped version in build.gradle then ran build so version falls through |
||
|
|
b4f7b1d8a9 |
Add bidirectional API types to frontend (#6867)
# Description of Changes Fix https://github.com/Stirling-Tools/Stirling-PDF-SaaS/issues/281. Add generated backend API mappings to the frontend code, and the logic to convert from a backend API to frontend parameters objects. Previously, it was impossible to tell if changing the backend API would require a change to the frontend to support it because the frontend had no static type information about the backend API. This PR adds autogenerated tool API types to the frontend (in `toolApiTypes.ts`) and adds explicit typed mappings between the frontend parameter types and the backend API types, so theoretically the type checker should be able to catch issues when changing one puts us in an invalid state with the other. During development, it pointed out several inconsistencies that we have between the frontend and backend types, some of which were genuine bugs, and others were only happening to work because the backend is more permissive than its API claims to be. This also unlocks the ability for us to render the frontend settings on saved backend API structures, which we've previously had to avoid doing because we had no reverse mapping. |
||
|
|
f881828cd8 |
Fix intermittently failing Playwright tests (#6886)
# Description of Changes Fixes intermittently failing tests (and replaces one that wasn't useful in its previous state) and also adds a CI check to warn if there are any Playwright tests which failed on their first go and succeeded on retries, to hopefully help find intermittently failing tests more quickly and avoid them being merged in the first place. |
||
|
|
16cfbc170e |
Clean up typos in docs, comments, and UI copy (#6045)
# Description of Changes Fix wording, numbering, path references, and minor grammar issues across project guides, backend comments, and frontend strings. This keeps documentation and user-facing text consistent without changing application behavior. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. Co-authored-by: James Brunton <jbrunton96@gmail.com> |
||
|
|
355d736487 |
Skip enterprise tests for forks since they can't run without secrets (#6888)
# Description of Changes OSS contributions which trigger the enterprise e2e tests will always fail due to missing secrets (see https://github.com/Stirling-Tools/Stirling-PDF/actions/runs/28776342146/job/85337748018?pr=6045). This PR disables them for OSS PRs. |
||
|
|
f201aa5915 |
feat(account-link): Phase 2 — instance metering + daily usage sync (#6839)
## Account-link Phase 2: metering + daily usage sync Phase 1 (already on main) let a self-hosted instance link a SaaS account and blocked billable work when it was over its limit. It blocked, but it never actually charged anything. This PR adds the metering + billing half. **It's off by default.** Everything sits behind `stirling.billing.account-link.metering.enabled`, on top of the existing `stirling.billing.account-link.enabled` master flag. Both have to be on for any of it to run, so it can't touch production. The billing model isn't going live yet — this is a dark merge. ### How it works 1. The instance classifies each billable request (API / AI / Automation — manual PDF editing stays free) and counts it locally into a per-period counter. 2. Once a day it reports its running totals to SaaS. 3. SaaS bills only the delta since the last report, reusing the existing charge path (free grant + wallet ledger + Stripe meter). No new money logic. 4. The portal shows current usage (synced spend plus anything not reported yet), and when you subscribe it now reflects the new plan right away instead of waiting for a cache to expire. ### What's worth a reviewer's eyes - **It can't double-charge.** SaaS only ever bills the delta, refuses a counter that goes backwards, dedups repeat/late reports on a monotonic sequence number, and takes a row lock so a duplicate delivery can't charge twice. - **The cap is enforced at the instance gate**, not in the charge path (same as the in-cloud flow). A $0 cap blocks all metered work. - Page counts use jpdfium so the instance and the cloud agree on the number that gets billed. - New SaaS surface: `POST /api/v1/instance/sync`, migrations V25 (`payg_instance_usage`) and V26 (allow the `LINKED_INSTANCE` job source), and a small `POST /api/v1/payg/wallet/refresh` the portal calls after checkout. ### Companion PR Stirling-PDF-SaaS #314 (on `v3`): the checkout edge function so the embedded Stripe flow finishes in-page instead of reloading, plus a `Deno.serve` migration so the edge functions actually deploy. ### Testing Java unit tests (proprietary + saas), portal vitest, and the SaaS edge-function tests all pass. Branch is merged up to date with main. ### Not done yet (doesn't block this merge — only matters once both flags are on) - V25 Supabase twin in the SaaS repo. - Same in-page checkout fix for the editor's upgrade modal. - A flags-on smoke test in staging (one real sync round-trip). --------- Co-authored-by: James Brunton <james@stirlingpdf.com> |
||
|
|
b69b787d63 |
Fix Playwright tests in Firefox and Safari (#6868)
# Description of Changes Playwright tests currently fail in Firefox and Safari because of inconsistent behaviour across the browsers. This is causing the nightlies to fail every night. This PR fixes the test behaviour to work consistently across browsers (most of the issues were to do with the tests opening the file picker, which was being automatically suppressed in Chromium, but not the other browsers). |
||
|
|
e630d6697b |
Fix tooltip positioning on Add Page Numbers (#6885)
# Description of Changes ## Before <img width="483" height="227" alt="image" src="https://github.com/user-attachments/assets/4bf86eec-a9cc-4f63-84f0-4eb2bd535bab" /> ## After <img width="732" height="235" alt="image" src="https://github.com/user-attachments/assets/101d2ea4-36e8-4e8f-990a-d72b33fa0ac2" /> |
||
|
|
a15e8227b4 |
fix(ci): upload Playwright reports from the correct frontend directory (#6859)
# Description of Changes
This change fixes the artifact upload path used by the Playwright E2E
workflows after the frontend directory structure was updated.
### What was changed
- Updated the Playwright report artifact path from:
- `frontend/editor/playwright-report/`
- to `frontend/playwright-report/`
- Applied the fix to:
- `build-enterprise.yml`
- `e2e-stubbed.yml`
- `nightly.yml`
- Renamed the nightly Playwright artifact from:
- `playwright-nightly-${{ github.run_id }}`
- to `playwright-report-nightly-${{ github.run_id }}`
for consistency with the other workflows.
### Why the change was made
The workflows attempted to upload artifacts from a directory that no
longer exists, causing GitHub Actions to report:
> No files were found with the provided path:
`frontend/editor/playwright-report/`
Updating the upload path ensures Playwright reports are successfully
collected and available for debugging failed E2E runs.
---
## 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.
|
||
|
|
12563050a6 |
Generate frontend license report on push (#6877)
# Description of Changes `app/allowed-licenses.json` has been modified in preparation for when "org.springframework.boot" is upgraded to version "4.0.7". --- ## 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. |
||
|
|
1abd23cf94 |
fix(frontend): respect analytics config before initializing PostHog (#6812)
# Description of Changes Please provide a summary of the changes, including: - What was changed - Moved PostHog startup out of `index.tsx` and into a config-aware initializer inside `AppProviders`. - Added a dedicated `usePosthogTracking` hook that only initializes PostHog when `enableAnalytics` is explicitly `true` and `enablePosthog` is not disabled. - Kept cookie-consent handling in the same flow so consent is applied only after PostHog is actually initialized. - Removed the unconditional `PostHogProvider` and `posthog.init(...)` bootstrap from the app entrypoint. - Added targeted frontend tests covering analytics-disabled and analytics-enabled startup behavior. - Why the change was made - The previous frontend bootstrap initialized PostHog before app config was loaded, so disabling analytics in the UI or via environment settings did not prevent PostHog network activity. - This change makes analytics behavior follow the server-provided config instead of always connecting on page load. Closes #6358 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have run `task check` to verify linters, typechecks, and tests pass - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. |
||
|
|
11ba3814e5 |
Restructure Portal code to be inside Editor (#6857)
# Description of Changes We don't have any strong reasons to keep the Portal as a separate Vite app, and it needs access to so many things from the Editor that it no longer makes sense to keep them separate. This PR moves the Portal code to have direct access to the Editor code and gets rid of the shared folder. |
||
|
|
675afe9b71 |
Disable update check and notification in SaaS mode (#6863)
# Description of Changes In SaaS mode the self-hosted "Update Available" notification could still appear and the update-check code (external call to `supabase.stirling.com/functions/v1/updates`) still ran, even though the cloud owns app versioning. The web `UpdateStartupPopup` was already SaaS-gated via a null override, but two other paths were not: - **Desktop app in SaaS connection mode** - `useDesktopUpdatePopup()` ran its startup check and rendered the `UpdateModal` regardless of connection mode, so a self-hosted update popup appeared while connected to SaaS. - **Settings → General** - the core `GeneralSection` fired `checkForUpdate()` on mount unconditionally, even when the update section was hidden (as SaaS does), so the external call still ran. **What changed** - `useDesktopUpdatePopup.ts` - the startup timer now bails out immediately when `connectionModeService.getCurrentMode() === "saas"`. No mode lookup, no external fetch, no modal. - `core/GeneralSection.tsx` - the mount `checkForUpdate()` now returns early when `hideUpdateSection` is set, so hiding the section (web SaaS, managed-disabled desktop) also stops the external call. - `desktop/GeneralSection.tsx` - passes `hideUpdateSection` when `useSaaSMode()` is true, which (via the above) suppresses the settings check in desktop-SaaS too. **Why** - in SaaS the update check should never be called and no update notification should be shown; the cloud handles versioning. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. |
||
|
|
6c85200eb9 | Add portal access control and S3/MCP/API integration configs (#6795) | ||
|
|
467f3a86c4 | Portal policies (#6852) | ||
|
|
9d3701a585 | Fix rearrange-pages DUPLICATE producing shared page nodes (pypdf cyclic-references CI break) (#6851) | ||
|
|
c22ecc6c09 | Add counts to sources page (#6819) | ||
|
|
b38c849726 |
Portal: Procurement surface — layout rework + stateful mock backend (#6785)
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com> Co-authored-by: Connor Yoh <con.yoh13@gmail.com> |