mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
38d06d3104f355337455238cf73d5c24aac7fa1a
962
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fe33378333 | feat(portal): set a spend cap during PAYG checkout (two-step modal) (#6970) | ||
|
|
5944cd106b | Portal audit: label policy runs by their policy, flag automation sub-steps (#6937) | ||
|
|
fd81bf4cf8 | Tighten whitespace between search bar and tool list (#6977) | ||
|
|
d23318cfa6 | Feature/onboarding updates for policies and portal (#6926) | ||
|
|
142544c9af | Replace portal sidebar brand text with Stirling Processor wordmark (#6978) | ||
|
|
863cad22bd |
Fix policy running of Redact (#6972)
# Description of Changes Policies can currently throw when calling redact: <img width="1186" height="824" alt="image" src="https://github.com/user-attachments/assets/bdcc09fe-5bf4-4b0a-b119-bcc33c98c7f2" /> Policies really need to be updated to properly make use of the new bidirectional mappings for this, but this will hopefully fix it for now. |
||
|
|
c06657c8f9 |
Match external-link tool buttons to normal tool button size (#6974)
The external-link "Developer Tools" buttons (API, Automated Folder Scanning, SSO Guide, Air-gapped Setup) used `p="sm"` while normal tool buttons use `p="none"`, making them render larger; this aligns their padding so they match the size of every other tool button. <img width="308" height="196" alt="Screenshot 2026-07-10 at 5 01 40 PM" src="https://github.com/user-attachments/assets/fb125500-28fb-4b83-85ed-2edc12e66fc0" /> |
||
|
|
d06a367b87 | SaaS role-based login landing (team leads → Processor) (#6960) | ||
|
|
ce6abe6e23 |
PAYG: size-scaled units + per-input-file PDF count + run-id grouping (#6957)
Reworks the Processor (PAYG) meter to **size-scaled units** while
keeping a true **PDF count** visible and distinct from units, and
replaces the fragile content+time lineage grouping with **explicit
per-run grouping**. Built as one PR across three slices.
> Status: **all three slices committed + verified.** `:saas` payg suite
green (418 tests, 0 failures); FE green (typecheck 0, 1260 tests, lint
0, format 0). Remaining before it takes effect in prod: run the
size-scaled default-policy SQL (below) in the Supabase SQL editor +
attach the $0.01/unit Stripe price.
## Model (what we're implementing)
- **Size scaling**: 1 unit per 50 MiB (bytes only, no page charge, no
cap). *(policy-row config, applied separately via SQL.)*
- **Charge = number of input files**: split (1→N outputs) = 1 charge;
merge (N→1) = N charges. `doc_count` = input files, fixed at open;
joined steps add 0.
- **Grouping by run id, not time**: a pipeline/policy/AI run = one
`run_id`; its tool sub-steps group into one charge (content-lineage
still maps split/merge journeys *within* the run). Two separate runs on
identical bytes = two charges. The 5-min window survives only as a
stale-job janitor.
- **10-tool split kept**: within a run's single-file lineage, an 11th
tool run opens a 2nd charge (step limit 10).
- **Count vs units surfaced**: usage page shows unique PDFs,
per-category (automation/AI/API) counts + units, and how many PDFs hit a
size multiplier with avg units/PDF.
## Slice 1 — run-id grouping (behavioural core)
- `AutomationRunContext` (common) — thread-scoped run id.
- `InternalApiClient` — stamps `X-Stirling-Run-Id`.
- Orchestrators open a run scope **on the worker thread that
dispatches** (async-safe): `PipelineProcessor.runPipelineAgainstFiles`,
`PolicyEngine.runToCompletion` (uses `run.getRunId()`),
`AiWorkflowService.orchestrate`.
- `ChargeContext` + `JobContext`: add `runId`; the charge interceptor
reads the header.
- `JobService.joinOrOpen`: `runId == null` → always open fresh
(standalone never joins); non-null → match scoped to the same `run_id`.
`JpaJobLineageStore`/`JobArtifactHashRepository`: add `run_id` filter to
the match query. Step-limit 10 unchanged.
## Slice 2 — doc_count + document_fingerprint
- V33 migration + entity fields.
- `JobService.openFresh`: set `docCount = inputs.size()`, compute
`document_fingerprint` from input signatures, and denormalise both onto
the DEBIT row in `JobChargeService.recordLedgerDebit`.
## Slice 3 — usage analytics API + FE
- `WalletLedgerRepository`: per-category `SUM(units)` +
`SUM(doc_count)`, `COUNT(DISTINCT document_fingerprint)`, and count of
rows whose units exceed their doc_count (a size multiplier fired), over
the period.
- `WalletSnapshotResponse` + `PaygWalletController`: add `categoryDocs`,
`docsProcessedThisPeriod`, `uniquePdfsThisPeriod`,
`sizeMultiplierPdfsThisPeriod`.
- FE `types.ts` + `PdfsProcessedCard` + `useWallet` + `walletFixtures` +
i18n: headline is the **PDF count**; a summary line shows "{unique}
unique · {units} meter units · {avg} avg units/PDF"; the split bar is
per-category PDF counts; a size-multiplier line shows how many PDFs
scaled. Count is separated from meter units so a 5-unit large PDF reads
as "1 PDF, 5 units".
## Config (out of PR — run in the Supabase SQL editor)
Wrap in one transaction. The partial-unique `is_default` index only
allows one default, so the old default is flipped off **before** the new
one is inserted. The new policy carries the prior default's
`free_tier_units` forward (change the literal if the launch grant should
differ).
```sql
BEGIN;
-- 1) flip default off the current policy + close its effective window
UPDATE stirling_pdf.pricing_policy
SET is_default = FALSE, effective_to = now()
WHERE is_default = TRUE;
-- 2) new default: 1 unit / 5 MiB, no page charge, no scaling cap.
-- free_tier_units carried from whatever the last policy granted (COALESCE→0).
INSERT INTO stirling_pdf.pricing_policy
(version, effective_from, doc_pages_per_unit, doc_bytes_per_unit,
min_charge_units, file_unit_cap, free_tier_units, is_default, notes, created_by)
VALUES
('v2-size-scaled-2026-07', now(),
2147483647, -- doc_pages_per_unit = INT_MAX → pages never drive units
52428800, -- doc_bytes_per_unit = 50 MiB → +1 unit per 50 MiB
1, -- min_charge_units
2147483647, -- file_unit_cap = INT_MAX → no cap on size scaling
COALESCE((SELECT free_tier_units FROM stirling_pdf.pricing_policy
ORDER BY effective_from DESC LIMIT 1), 0),
TRUE, 'Size-scaled: 1 unit/5MiB, bytes only, no cap', 'connor');
-- 3) per-source step limits: standalone ops = own charge; pipelines split at 10
INSERT INTO stirling_pdf.pricing_policy_step_limit (policy_id, job_source, step_limit)
SELECT p.policy_id, s.src, s.lim
FROM stirling_pdf.pricing_policy p
CROSS JOIN (VALUES
('WEB',1),('API',1),('DESKTOP_APP',1),('LINKED_INSTANCE',1),('PIPELINE',10)
) AS s(src, lim)
WHERE p.version = 'v2-size-scaled-2026-07';
-- 4) attach the $0.01/unit Stripe price (you handle the real price id)
INSERT INTO stirling_pdf.pricing_policy_stripe_price (policy_id, stripe_price_id)
SELECT policy_id, 'price_XXXXXXXX'
FROM stirling_pdf.pricing_policy WHERE version = 'v2-size-scaled-2026-07';
COMMIT;
```
Note: the `free_tier_units` subquery reads the most-recent policy
*before* the insert — run it as written (the new row doesn't exist yet
at step 2's SELECT).
## Self-hosted parity — tracked follow-up (not in this PR)
Combined-billing (`stirling.billing.account-link.enabled`) is a
**separate metering engine** (`app/proprietary/accountlink` —
`UsageMeterService`/`LocalUsageService`/`UsageSyncService`). The unit
*math* is shared (`DocumentUnitCalculator`), so size scaling matches
once the policy is pushed. But run-id grouping, `doc_count`, and
fingerprints must be mirrored there, and the usage-sync protocol
extended to report counts/fingerprints, before the self-hosted usage
page shows the same breakdown. Frozen/deferred, so this PR does SaaS;
self-hosted mirrors when it ships.
|
||
|
|
ece3562dc9 |
Portal: team-scoped Free PDF Editors usage card for SaaS (#6924)
## What Phase 2 of the Free PDF Editors usage card (self-hosted shipped in #6919): make it work on **SaaS**, where one backend serves many teams so every figure must be scoped to the **caller's team**. | Metric | SaaS (per team) | |---|---| | **Editors deployed** | team member count (`team_memberships`) | | **Active this month** | distinct members with a free-UI (`source='WEB'`, non-`UI_DATA`) audit event in 30d, clamped ≤ deployed | | **PDFs edited** | the team's cumulative free-UI `PDF_PROCESS`+`FILE_OPERATION` events | Cost stays `$0`; uncomputable figures render **N/A**. ## Backend - **Gate the self-hosted controller** `@Profile("!saas")` — its counts are server-wide, which would leak across tenants on SaaS. New team-scoped `SaasFleetUsageController` `@Profile("saas")` owns the same `/api/v1/usage/fleet-stats` path (mutually exclusive profiles → no mapping conflict). - **Team resolution** mirrors `PaygWalletController`: `AuthenticationUtils.getCurrentUser(auth, userRepo)` → `TeamMembershipRepository.findPrimaryMembership` → members via `findByTeamId`. `@PreAuthorize("isAuthenticated()")` (team leaders aren't global admins; any member sees their own team's totals). - **Audit → team join**: on SaaS the audit `principal` is the user's email and `User.username == email`, so principals join cleanly to a team's member usernames (no hashing — only raw-JWT/over-long principals get hashed). Two new `principal IN` count queries do the filtering, served by the `(source, timestamp, principal)` index from #6919. - Billing/ledger is deliberately **not** used — it only records billable ops; free-editor activity comes from audit (same `source='WEB'` signal as self-hosted). - `null`→N/A when EE auditing < STANDARD; 401 on no-auth; empty-fleet guard for the (post-migration-shouldn't-happen) teamless caller. ## Frontend - New `src/portal-saas/api/fleetStats.ts` (rides the `@portal/*` cascade from #6900) reads via **`apiClient.saas`** — the Supabase JWT the SaaS backend uses to resolve the team. Re-exports `FleetStats` via `@portal-proprietary`. **The card and `useAsync` hook are untouched.** ## Tests `STIRLING_FLAVOR=saas` build green — `:proprietary` + `:saas` compile, `SaasFleetUsageControllerTest` (team scoping, audit-off→null, clamp, no-team→empty, unauth→401) and the existing suites pass; spotless clean. ## Notes - Requires SaaS auditing at STANDARD (it is) — else N/A. - Depends on #6900 (merged) for the portal-saas override layer and #6919 (merged) for the audit `source` column + DTO. |
||
|
|
84d4455682 |
Add virtual Editor source (#6959)
# Description of Changes Adds Editor source permanently available in the Sources list. Excludes it from the Pipelines list of available sources currently because it's not a real source on the backend, so attempting to connect to it causes an error. It'd be nice to extend in the future to be able to set up policies in the editor from the pipelines page, but this'll do for now. |
||
|
|
b9a7f2083b |
Portal: realign home hero to the simplified marketing card (#6956)
## What Reworks the free-tier home hero (`WelcomeBanner` + `SetupChecklist`) to match marketing's reworked top card: a compact product header over numbered getting-started steps, dropping the marketing chrome. ## aim attachments/assets/75a80e5f-119e-46bb-80e7-fc4b9a62e5b6" /> <img width="1098" height="646" alt="01-aim-marketing-demo" src="https://github.com/user-attachments/assets/681ca1b8-219e-4afe-9748-89435aafd440" /> ## old hero <img width="1800" height="1338" alt="02-before-old-hero" src="https://github.com/user-attachments/assets/a396cec0-4752-4ac0-9951-9a49f9d50ea7" /> ## new screenshots <img width="1800" height="626" alt="03-after-onboarding-card" src="https://github.com/user-attachments/assets/59332111-6c49-438d-af6d-200d99bf0f8f" /> <img width="1800" height="180" alt="04-after-deployed-header" src="https://github.com/user-attachments/assets/ea631e6c-7878-4159-aaf7-1f0c2bd795ce" /> <img width="1024" height="1396" alt="05-after-install-modal-list" src="https://github.com/user-attachments/assets/125d0a11-d799-40f0-bd8b-42f5dedcfe6d" /> <img width="1024" height="858" alt="06-after-install-modal-docker" src="https://github.com/user- ## Changes - **Compact dark header:** brand mark + "PDF Editor" + social-proof stats (`30M downloads · 60+ PDF operations · Free forever`) + a single **Open in browser** CTA (→ `EDITOR_URL`). - **Dropped** the decorative editor mock, marketing title/subtitle/"Open-source" badge/perks, the two extra banner buttons, and the checklist's dismiss/progress/done tracking. - **Numbered nav steps** (①②③) — each opens its in-app surface: | # | Step | Goes to | Change | |---|------|---------|--------| | ① | Download the editor | `editor` view | was an external `stirling.com/download` link → now in-app | | ② | Confirm your policies | `policies` view | live active/recommended counts retained | | ③ | Invite teammates | `users` view | **replaces** "Connect your sources" (sources dropped to match the demo) | - **Enterprise rung** unchanged (Start Trial / Get Quote → procurement). ## Notes - **Shared hero** — self-hosted sees it too (per decision). - **One deliberate deviation from the demo:** the header CTA is blue (brand primary) rather than the demo's white button. Trivial to flip — say the word. - Behaviour change: the hero is now a quick-start (navigational) rather than a completion checklist — the dismiss control + per-step done chips are gone to match the demo. - Supersedes the incremental #6944 ("add Open in browser" 3-button version) — that can be closed in favour of this. - Portal `tsc` clean; `unusedTranslations` green (removed orphaned welcome/onboarding keys, added the new ones). |
||
|
|
b36f3e0875 |
Remove unused portal UI (#6949)
Removes some cluttered/unused UI from the portal: - Search bar in the header - The top bar entirely (breadcrumb, notification bell, plan switcher, user menu) - The plan/usage indicator in the sidebar footer - The floating assistant badge UI only. Where a component isn't deleted it's just no longer rendered, so anything here is easy to restore. |
||
|
|
e4379184b5 |
fix(portal): translate policy category labels in PolicySummary (#6964)
## What The portal's **"What runs on your PDFs"** table (`PolicySummary`) rendered raw i18n keys instead of text: - `portal.policies.categories.ingestion.label` / `.desc` - `portal.policies.categories.security.label` / `.desc` - …and the other three categories (compliance, routing, retention) ## Why it broke [#6910 "Remove in-app portal mocks"](https://github.com/Stirling-Tools/Stirling-PDF/pull/6910) moved the policy catalogue to real data and converted each category's `label`/`desc` (and each config's `summary`) into **i18n keys** — see the `// values are i18n keys — render with t()` note in `api/policies.ts`. Every consumer was updated to call `t()` (`PolicyCategoryCard`, `PolicyDetailPanel`, `PolicySetupWizard`)… except `PolicySummary`, which was not part of that PR and kept rendering the fields verbatim. The translation keys themselves already exist in `en-US/translation.toml` (`[portal.policies.categories.*]`) — nothing was missing, they just weren't being looked up. ## Fix Wrap the values in `t()` in `PolicySummary.tsx` (the `t` from `useTranslation` was already in scope): - category `label` / `desc` in the Policy column - `config.summary` in the Active-rule column (same keyed-value treatment, latent until a policy is active) ## Test plan - [ ] Open the portal Home / policies summary → each row shows the translated category name + description (e.g. "Ingestion" / "Classify documents…") instead of a dotted key. - [ ] A row with an active policy shows its translated rule summary in the Active rule column. |
||
|
|
5ccb56da2d |
Add S3 policy source (#6948)
# Description of Changes * Adds an Amazon S3 Source & Output * Removes folder source from SaaS * Some miscellaneous UX fixes around pipelines |
||
|
|
16f589448d |
Remove the policies management surface from the editor sidebar (#6932)
## What Removes the policy **management** surface from the editor's right rail — the Policies list above Tools, the open-policy detail takeover, and the collapsed-rail policy icons — along with the whole UI tree only they used: the setup wizard and its tool-config steps (PII / redact / watermark), the detail panel, delete modal, selection store, enforcement-queue status chip, activity/stats derivation, the catalog hook, their i18n keys, dead types, and the admin-gate spec that tested the wizard flow. **Enforcement is untouched.** Auto-run on upload, the viewer blocking overlay, exit-point blocking, file badges, and export-time enforcement all stay. `usePoliciesEnabled` moves to its own module (core stub / proprietary / desktop shadow with the SaaS-connection check) since it still gates mounting the headless `PolicyAutoRunController` from the rail. ## Why Policies are configured in the admin portal now (`src/portal/views/Policies.tsx`). Keeping a second management UI in the editor rail meant two surfaces to maintain for one feature; the editor only needs to *enforce*. ## Notes for review - The rail UI lived in the shared `core` `RightSidebar`, so this removes it from every build flavour at once; the deleted `PoliciesSidebar` module existed at the core (stub) / proprietary / desktop alias layers and all three are gone. - Every deleted module was verified to have zero remaining importers; near-misses that stay: `enforcementQueue` (used by export enforcement), `poll` (test-imported), `usePolicies` (used by auto-run). - Net −3,900 lines. ## Testing - `task frontend:check` green: typecheck, ESLint + dpdm, Prettier, all 1,196 tests. - All build-variant typechecks pass (core / proprietary / saas / desktop). |
||
|
|
75ea3c9a1f |
Portal procurement: pricing realignment, combined accept flow, licence & invoice fixes (#6946)
## What this PR does Brings the enterprise procurement flow in line with the new D71 pricing, tidies up the buyer journey, and fixes a handful of things we found testing it end to end. ### Pricing - Priced on the new run-based model (per PDF, per policy), USD only. Dropped the old currency picker. - Added the policy posture choice (Essentials / Governed / Regulated) and show roughly how many policies each covers (~2 / ~4 / ~7). - The live estimate in the quote builder now matches the real quote the backend produces. - Contracts renew each year with a fixed 3% increase. The agreement shows this plus the first renewal figure, and we save that figure on the quote so it can't drift later. ### Trial and journey - Starting a trial now asks for your deployment (Cloud / Self-hosted / Air-gapped) and team size up front, and that seeds the quote. - Quote and agreement are now one step: you review the quote and the agreement together and click "Accept & subscribe" once. No more accepting a quote and then separately signing. - "Start a trial" on the home page opens the setup popup right there instead of sending you off to another page. - The calculator asks for number of users again and works the volume out from that. - Removed the demo-only buttons (reset, simulate payment) and the "Key documents" button (it wasn't real). - The licence key now lives behind its own "Licence key" button instead of being shown inside every popup. ### Air-gapped licence file - Air-gapped teams can download their licence file (.lic) during the trial, not only after they pay. - The popup warns that a trial file needs re-downloading once the agreement is done, because the file is a snapshot and doesn't refresh itself the way the online key does. ### Fixes found while testing - Accepting a quote now upgrades the licence from trial to full straight away (it wasn't before). - The "Download invoice" button keeps working after a page refresh (we now save the invoice PDF link). - Invoice line items read differently from each other instead of all showing the same name. ### Notes for reviewers - The matching backend changes (Stripe quote/accept functions, database migrations) live in the Stirling-PDF-SaaS repo on `v3`. They ship when we do the full v3 release. - All checks are green. |
||
|
|
7529190587 |
fix(ui): shared Button content-sizing + padding props, and button call-site cleanups (#6914)
## Summary A batch of shared **design-system** fixes (Button, SegmentedControl, Chip, a new CarouselDots) and the consumer/call-site cleanups they unlock, following the button consolidation (#6787). Also includes dark-theme token alignment and some portal/auth polish that rides on the same components. The shared Button now sizes to its content instead of clipping it, gains per-axis padding controls, and no longer misbehaves while loading or disabled; several call sites are then migrated onto the proper component APIs. ## Shared components (`core/ui`) ### Button - **Content-driven height.** `--button-height` is now a `min-height`, not a fixed cap. Single-line buttons still land exactly on the shared control-height scale (pixel-aligned with `ActionIcon` / `SegmentedControl`), while taller content — wrapped labels, stacked title + subtitle rows — grows the button instead of being clipped mid-glyph. Short content is re-centered with `align-content`, **without** overriding the root `display`, so a consumer's own layout (e.g. a full-width list row) isn't disturbed. - **Padding props.** New `p` / `px` / `py` props (`none`/`xs`/`sm`/`md`/`lg`/`xl`) override the size-based padding per axis. Vertical padding is applied through a `--sui-btn-py` CSS variable, so consumers can also set it from their own class. - **Loading no longer collapses.** A `fullWidth` button is never treated as icon-only, so an execute button whose label is momentarily absent while files hydrate (e.g. `ScopedOperationButton`) keeps its full width with a centered spinner instead of shrinking to an icon-sized square for a split second. - **Disabled in dark mode.** A disabled *primary* button keeps a muted version of its own accent fill (`opacity: 0.55`) instead of Mantine's near-black `--mantine-color-disabled`, which blended into dark surfaces and made the button all but disappear. Loading spinners are excluded so they stay full-strength. No breaking API changes — buttons that don't opt in render exactly as before. ### SegmentedControl - Fixed a bug where a segment marked `disabled` that also happened to be the currently-selected value was rendered disabled, leaving the active segment un-selectable/greyed. A disabled option is now only disabled when it isn't the current value. ### CarouselDots (new) - New shared dots indicator component (with Storybook story), used by the login carousel. ### Chip / theme - Dark-theme tokens in `theme.css` aligned to the portal's `tokens.css` so the editor and portal (Processor) dark modes stop drifting (chrome surfaces lift off the darker canvas); plus a Chip dark-mode styling fix and a small `mantineTheme` cleanup. ## Consumer / call-site cleanups - **Compare** tool: the swap control is now a regular shared Button placed **between** the Original and Edited file cards (the bespoke full-height vertical swap button and its CSS were removed), and the file cards fill the full available width. - **Certificate format**: replaced the inline-styled buttons with clean two-state (primary / secondary) buttons. - **ToolPicker**: restored the label selectors that #6787 renamed to the never-emitted `.sui-btn__label`, and fixed the sidebar-search row clipping. - **File sidebar**: "View all files" row fix; `FileSidebarFileItem` migrated off `display:flex` + `gap` on the Button root (which no longer reaches the nested label) onto `leftSection` / `rightSection` + a stacked label. ## Portal / auth polish - Portal button consolidation and styling across Header, SettingsModal, Home, Infrastructure, ApiKeyCard, and PopularUseCases. - **Login**: onboarding text now shows the default starting username / password; login carousel uses the new CarouselDots; desktop OAuth styling tweak. ## Verification - Storybook: button sizes measure exactly on the control-height scale and match `ActionIcon`; icon-only buttons stay square and centered; `fullWidth` loading buttons hold full width; disabled dark-mode primary buttons render as a muted accent rather than grey. - Single-line buttons are pixel-identical before/after; only buttons whose content previously overflowed a fixed height render differently (they now fit rather than clip). - `task frontend:lint` clean; typecheck shows only the pre-existing third-party `node_modules` noise also present on `main`. |
||
|
|
b9f9f84907 |
Route portal Users page to SaasTeamController on SaaS via usersBackend seam (#6940)
## Why
The portal Users page worked on self-hosted but **403'd on SaaS**. It
called the proprietary admin endpoints (`/api/v1/user/admin/*`,
`/api/v1/team/*`, `ui-data/admin-settings`), all `hasRole('ADMIN')`.
SaaS users are always `ROLE_USER` (never `ROLE_ADMIN`), so those
endpoints reject them. This is the last SaaS-release blocker for the
portal.
## What
Route the SaaS build's Users page to the **existing**
`SaasTeamController` (invitation-based team management) - no new
backend. Done via a build-time flavor seam, mirroring the existing
`usersCapabilities` pattern.
- **New seam `@app/portal/usersBackend`** (interface in
`portal/api/usersBackend.ts`) with two impls resolved by the `@app/*`
alias:
- `proprietary/portal/usersBackend.ts` re-exports the existing
admin-endpoint functions - **self-hosted behaves exactly as before**.
- `saas/portal/usersBackend.ts` calls `SaasTeamController`
(`/api/v1/team/*`) via `apiClient.local` (already flavor-aware: SaaS
backend + Supabase JWT). Resolves the leader's team from `GET
/api/v1/team/my`, maps `TeamMemberDTO`/`InvitationDTO` onto the portal
`Member`/`PendingInvitation` types.
- **`manageInvitations` capability** (SaaS `true` / self-hosted `false`)
gates a new **Pending invitations** panel (list from `GET
/{teamId}/invitations`, Cancel via `DELETE
/api/v1/team/invitations/{id}`).
- **Remove re-enabled on SaaS** (was gated off): the roster remove
action now works at team scope against `DELETE
/{teamId}/members/{memberId}`, with a flavor-aware label ("Remove from
team" vs "Remove from org") and confirm copy.
- Invite (email) and rename routed through the seam (`POST /invite`,
`POST /{teamId}/rename`); `fetchAuthConfig` on SaaS is static (no
spurious admin-endpoint 403).
- **MSW handlers** (`mocks/handlers/teamSaas.ts`) mirror the controller
so the SaaS Users page is exercisable in mock mode. Registered in
`handlers` but deliberately **not** `embeddedDataHandlers` (would clash
with the editor's own `/api/v1/team/*` routes when portal shares its
origin).
## Constraints honoured
- No new backend endpoints - reuses `SaasTeamController`.
- Self-hosted path unchanged (proprietary impl re-exports the same
functions).
- No SaaS user is ever `ROLE_ADMIN` - `adminRole`/admin-only UI stay
hidden.
## Notes from an adversarial self-review (both fixed in this PR)
- Solo SaaS users' auto-created **personal team** now hides the Rename
control (the backend rejects renaming personal teams with 400) -
`isPersonal` threaded through `Team`/`TeamGroup`.
- Expired-but-still-`PENDING` invitations are filtered in the adapter,
and the expiry label no longer mislabels a just-expired invite as
"Expires today".
## Testing
- `task frontend:typecheck:all` - all 8 flavors pass.
- Portal vitest project: **122 passing** (added SaaS adapter +
shape-mapping tests via MSW, PendingInvitations panel, and
remove/manageInvitations/personal-team gating).
- `task frontend:lint` (ESLint `--max-warnings=0` + dpdm no circular
deps) and prettier clean.
## Open questions
- **Team resolution on SaaS**: I resolve the leader's single manageable
team (prefer a real non-personal team they lead). If a leader owns
multiple real teams, only the primary is shown - matches the "single
team" framing in the spec; flag if multi-team management is wanted.
- **`isSelf` on SaaS** uses the LEADER role (the portal Users page is
leader-only on SaaS, so the leader row is the viewer). Verified there's
no multi-leader creation path today; revisit if that changes.
Draft - not marking ready until reviewed.
|
||
|
|
68ec176719 |
Portal empty states: add CTAs and hide stat boxes (#6952)
# Description of Changes Empty-state polish across the four processor (portal) list pages, so a fresh workspace gets clear next steps instead of a row of zeroed-out stat boxes. - **Sources / Pipelines** - hide the KPI stat strip when the list is empty; the empty state now shows an icon plus a primary + secondary CTA (Connect source / Read the docs; Create a pipeline / Connect a source). Also closes a gap where a successfully-fetched empty list rendered stat boxes over a blank page with no empty state at all. - **Policies** - hide the summary stat strip until at least one policy is configured; the catalogue cards stay as the "configure a policy" CTAs. - **Documents** - hide the filter-pill + search toolbar on an empty queue; the empty state gains an icon plus Create a pipeline / Connect a source CTAs. - **Storybook** - added `Default` + `Empty` stories for all four views; the preview now loads the real English copy so stories render shipped text rather than raw i18n keys. --- ## 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) - [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) - [ ] 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. |
||
|
|
d17c3f4fec |
Portal: wire remaining hardcoded strings to i18n (#6953)
# Description of Changes - Audited every portal page/component for hardcoded UI strings not routed through `t()` - Wired the remaining ones to i18n (~80 new `portal.*` keys in `en-US/translation.toml`): - Infrastructure status/label maps (deploy, api-key, cert, key-mode, attestation, audit, model, region, environment) + API-key permissions - Procurement "Key documents" modal, editor-admin deploy targets, users seats label, pipeline output-folder placeholder - Follows the existing house pattern: label maps store i18n keys, resolved via `t(MAP[value])` at the render site - Documents CSV export now reuses the on-screen column keys, and fixes a latent bug where the exported status leaked the raw key instead of the translated label - No UI-copy change: en-US values are identical to the previously hardcoded strings; other locales fall back to en-US as before --- ## 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. |
||
|
|
783a51950f |
Update translations for 40 languages via GPT-5.5 (#6954)
# Description of Changes - Adds and updates translations across **40 languages** (~1,400–2,200 keys each) using GPT-5.5, filling previously-missing UI strings. - Switches the translation scripts' default model from the year-old `gpt-5` (5.0) to `gpt-5.5`, adding a `--model` flag and token/cost reporting. - Purely additive and validated: no existing translations changed, all 40 files match the en-US key structure, and no new placeholder issues introduced. --- ## 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) - [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) - [x] 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. |
||
|
|
51d3d27fd3 |
Portal policies: SUI setup forms fixes and improvements (#6927)
## What this does Reworks the portal's policy setup screens so a policy reads as **its own settings** rather than a list of tools you wire together, and rebuilds the forms on the shared design system so they match the rest of the portal. ## Why Setup showed one card per underlying tool (tool name + a toggle), which exposed the "a policy is a pipeline of tools" plumbing. A policy should read in terms of what it does to a document, not which tools run under the hood. ## Changes - **Setup reads as policy settings.** The per-tool cards are now a plain-language list of what the policy does — "Redact sensitive information", "Strip active content", "Apply a watermark", and so on — each with a short description and a toggle, with its options appearing inline when turned on. - **Consistent design system.** The setup and edit forms use the shared components instead of one-off styling. - **Simpler setup.** Removed two sections that aren't part of what ships here: Document Types (scope-by-type) and Retries. - **Watermarks are text-only.** A policy watermark is a text stamp, so the image option and the type picker are hidden. - **The editor always shows as a source** (it used to disappear when no other sources were connected), and the source tiles now lay out correctly. - **Clearer upsell copy.** Locked policies read **"Upgrade to Enterprise"** instead of "Coming soon". ## Scope UI only — no backend changes. Keeping a policy's settings in sync between the portal and the editor is a known, separate issue and is **not** part of this PR. ## Testing Prettier, ESLint, typecheck (proprietary + saas), and the unused-translation guard all pass. Setup screens verified in Storybook. |
||
|
|
ccfd22b2a9 |
port editor settings into portal (#6945)
The portal's `SettingsModal` was a parallel, mock-backed settings implementation. It's replaced by the editor's `AppConfigModal`, mounted via a new `PortalSettingsHost` that supplies the contexts the portal doesn't have (app config, flavor-resolved session, preferences, editor theme). Flavor resolution does the rest: the self-hosted portal gets the admin sections, the SaaS portal gets the saas shell. The self-hosted account-link panel rides in through the existing seam as an extra section. The shell gains three host props (`urlSync`, `initialSection`, `extraSections`); editor behaviour is unchanged. Net −1,300 lines. Manually verified on both flavors against live backends. |
||
|
|
a5ee329c36 |
Further improvements to policies file tracking (#6941)
# Description of Changes Fixes requested in review of #6903 |
||
|
|
2091874050 |
Remove in-app portal mocks (#6910)
The portal no longer uses mock data — it always talks to the real backend. Mocks still power Storybook and tests. - Mocks button and all the in-app MSW machinery removed. - Types the app needs moved out of mock files and into the api layer, so the app no longer depends on `mocks/` at all. - One deliberate exception for the onboarding tour (#6926): `enablePortalDemoData()` fills the views with example data while a tour runs, with zero cost the rest of the time. Heads up: views without a real backend endpoint yet now show empty/error states in dev. |
||
|
|
22e8a82fa1 |
Portal: add 'Open in browser' CTA to the welcome hero (#6944)
## Screenshots <img width="2522" height="1322" alt="image" src="https://github.com/user-attachments/assets/010e2dce-00ae-4c7f-8ec8-7e6519beb4cd" /> |
||
|
|
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). |
||
|
|
f703a67817 | Fix cert sign not showing under certain instances (#6908) | ||
|
|
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) |