Commit Graph
5539 Commits
Author SHA1 Message Date
EthanHealy01 449353150d feat(lint): make contrast check blocking + fix dark subtle-text AA
Per review on #7009: the contrast check was warn-only and opt-in, so it would
never catch regressions. Enforce it here (the follow-up PR):
- theme-lint.mjs `contrast` mode now exits non-zero on any sub-floor pair
- wired into `task frontend:lint:colors` (runs in the blocking lint gate)
- fixed the two failing pairs: --c-text-subtle on dark/portal-dark surfaces
  (3.36/3.67 → 4.74/5.18) by adding --p-zinc-250 and pointing dark subtle at it

All text-on-surface / on-primary pairs now clear WCAG AA per theme.
2026-07-13 18:49:46 +01:00
EthanHealy01 0f6f5b4e0b feat(lint): enforce no hardcoded colour in TS/TSX DOM code (code-colors)
Add a blocking 'code-colors' mode to theme-lint: default-deny with layered
exemptions (structural; var()/readColor/canvas/pdf-lib contexts; a
// theme-allow-color opt-out; exempt paths for rendering/vendor/config/
illustration areas). File list from 'git ls-files' (no directory walk).
Wired into 'task frontend:lint:colors'; README + script header updated.

Also removes the temporary color-migration-audit.md scratch file.
2026-07-13 17:42:21 +01:00
EthanHealy01 941dc35cda refactor(ui): route TS/TSX inline-style colours through the palette
Migrate hardcoded colour literals in DOM inline styles to tokens:
- auth screens (OAuthConsent/LoggedInState) + Login/Signup -> fixed --p-*
  (AuthLayout forces light; semantic --c-* would break there)
- FileCard/LanguagePicker/ErrorBoundary/ToolStep/etc -> --c-* / --p-*
- Payg avatar palette + DesktopOnboardingModal gradient -> exact --p-*
- rgba tints -> color-mix(in srgb, var(--p-*) N%, transparent)

Canvas/PDF rendering, colour pickers, vendor brand and self-contained docs
keep numeric colour by design.
2026-07-13 17:40:43 +01:00
EthanHealy01 93bc796e2f add primatives and remove the rgb codes and hex codes that we can remove 2026-07-13 16:36:12 +01:00
EthanHealy01 544a1f02eb feat(lint): enforce zero hardcoded colour across all source CSS
Add a blocking 'css-colors' mode to theme-lint that flags any hardcoded
colour in source .css under editor/src (primitives.css + generated
output.css exempt). File list is sourced from 'git ls-files' rather than a
directory walk, so there's no readdir→readFile path; comments and structural
black/white/transparent are ignored. Wired into 'task frontend:lint:colors'.

Update theme README + script header for the expanded palette and the now
app-wide enforcement.
2026-07-13 15:08:03 +01:00
EthanHealy01 6800db8029 refactor(css): migrate all hardcoded colours to primitives (CSS → zero literals)
Move every non-structural colour literal in source CSS into the primitive
palette and reference via var(--p-*):
- 194 hex + 139 rgba() across 38 files replaced
- 90 new primitives added (brand azure/red, violet/indigo, cyan, pink,
  emerald, slate, navy, Notion paper, illustration art, vendor/OS-media)
- near-duplicate neutrals snapped to existing ramp values
- coloured rgba() alpha tints -> color-mix(in srgb, var(--p-*) N%, transparent)
- structural rgba(0,0,0/255,255,255) and comment breadcrumbs left intact

Source CSS now contains zero hardcoded colour outside primitives.css.
Build + theme-lint pass; every var(--p-*) reference resolves.
2026-07-13 15:03:44 +01:00
EthanHealy01 2748fe3157 refactor(css): consolidate redundant status/brand colours to tokens
- billing status chips (FULL/WARNED/DEGRADED) -> adaptive --c-success/
  warning/danger(-subtle); now theme-aware instead of fixed pale literals
- unify saas guest-button red #9c2f30 -> #af3434 (single Stirling red)
- unify SpendCapControl dark accent #7ab4ff -> #66b8ff (match billing azure)
- disabled plan-button grey #7e7e7e -> var(--p-gray-500)

Distinct non-structural CSS hex: 95 -> 86.
2026-07-13 14:43:02 +01:00
EthanHealy01 b7bcbc9f45 refactor(css): merge adjacent duplicate rulesets into grouped selectors
Merge only ADJACENT rules (consecutive, whitespace-only gap, same nesting) with
identical declaration blocks — cascade-safe (non-adjacent merges can reorder the
cascade, so left alone). 22 merges across 14 files, net -104 lines. Build +
theme-lint green.
2026-07-13 11:47:15 +01:00
EthanHealy01 d7f51d7f72 feat(theme): add --c-warning + subtle status tokens; migrate auth alerts
Adds --c-warning (amber, per-theme) and --c-success/danger/warning-subtle
(pale status surfaces) to colors.css — the recurring tokens the consolidation
agents flagged as missing. Migrates the auth error/success alert boxes onto
them. Remaining status-colour consumers tracked in color-migration-audit.md.
2026-07-13 10:58:56 +01:00
EthanHealy01 2c0728c1a7 refactor(theme): collapse dark-override blocks to adaptive --c-* (agents)
Six parallel passes across component CSS: collapsed manual light/dark override
blocks to the adaptive --c-* tokens (navy overrides → neutral surfaces, per the
neutral-dark direction), migrated semantic colours to --c-*, stripped dead
--c-*/--p-* fallbacks, removed a duplicate ruleset. Brand/data-viz/illustration
colours kept and catalogued in color-migration-audit.md with the --c-* tokens
still needed to finish. Build + theme-lint green.
2026-07-13 10:55:38 +01:00
EthanHealy01 cca8c700d3 refactor(theme): strip dead --c-primary hex fallbacks (partial)
Removes the dead #6366f1 fallback from var(--c-primary, #6366f1) in 4 files
(--c-primary is always defined). Partial progress on the colour consolidation;
the full override-collapse pass is pending (agent fan-out blocked).
2026-07-13 09:41:07 +01:00
EthanHealy01 17b0fae153 refactor(theme): tokenize exact-match hardcoded colours (script)
Auto-replaced 48 hardcoded hex colours that match a primitive within Δ≤2
(visually identical) with var(--p-*), across all component CSS. Generated
color-migration-audit.md listing the 50 near-matches and 93 genuinely-unique
colours for the follow-up agent consolidation pass. Build + theme-lint green.
2026-07-13 02:02:33 +01:00
EthanHealy01 c5fbf85035 refactor(theme): tokenize matching hex + remove dead legacy tokens
- theme.css: convert the 5 hardcoded UI colours that exactly match a primitive
  to var(--p-*) / color-mix(var(--p-*)); bespoke logo/illustration fills, brand
  azure/gradients and the deliberate flash-yellow have no primitive equivalent
  and are left.
- Remove 11 verified-dead legacy tokens (--text-always-*, --landing-drop-*,
  --link-*) — 0 refs and not built dynamically (excluded all --color-*/--accent-*
  since those prefixes ARE constructed at runtime). theme.css 902 → 865 lines.
Build + theme-lint green.
2026-07-13 01:50:36 +01:00
EthanHealy01 ee6684f6a4 refactor(theme): delete compat.css — components reference --c-* directly
Migrated all remaining legacy-alias consumers (incl. stories/tests) to --c-*
(199 refs / 58 files), promoted the computed aliases to first-class tokens
(--c-accent-fg made universal on :root; --c-primary-tint / --c-primary-border
added), then removed compat.css, its @import, and its linter entry. No legacy
colour-alias layer remains; build + theme-lint green.

The larger theme.css/tokens.css legacy definition files are a separate cleanup.
2026-07-13 01:25:38 +01:00
EthanHealy01 925caf6d11 refactor(theme): drop 71 now-dead compat aliases
After migrating consumers to --c-* (prior commit), 71 of the 93 1:1 compat
aliases have zero references anywhere (verified across src + tailwind.config +
index.html; the 22 that remain are kept alive by stories/portal mantine theme).
Removing the dead ones cuts compat.css roughly in half — the real consolidation
payoff, not just a rename.
2026-07-13 01:14:13 +01:00
EthanHealy01 a70f80279a chore: remove one-shot compat-colour codemod after applying it
The migration is permanent in the committed files; the throwaway walker isn't
referenced anywhere and tripped Aikido's readdir→readFile heuristic.
2026-07-13 01:10:39 +01:00
EthanHealy01 7b43bb483e refactor(theme): migrate hardcoded hex in component CSS to tokens
Per-area passes replaced hardcoded surface/text/border/status hex in component
stylesheets with semantic --c-* tokens (--p-* only where an exact value had to
be preserved). Left intentionally: structural black/white/transparent, OAuth
brand colours, deliberate multi-hue gradients, data-viz/status hues without a
semantic fit, and dead var(--c-*, #fallback) fallbacks. ~90 sites across 17
files. Build + theme-lint green.
2026-07-13 00:48:40 +01:00
EthanHealy01 5f6b88005e refactor(theme): migrate compat colour aliases to --c-* tokens
Codemod (editor/scripts/migrate-compat-colors.mjs) rewrites every var(--legacy)
reference to the canonical --c-* token it maps to in compat.css (1:1 aliases
only; computed color-mix/fallback aliases left in place). 218 files, 1947
references. Pure rename — compat.css already guarantees each alias equals its
--c-* target, so no behaviour change.
2026-07-13 00:36:08 +01:00
EthanHealy01 41f507a9f0 chore(theme): dedupe default-blue literal in theme constants
Collapse the two #3b82f6 occurrences (DEFAULT_ACCENT_COLOR + first preset) into
a single documented BLUE_500 const. The accent hexes stay JS literals by
necessity — stored verbatim and parsed by deriveAccessiblePrimary, so CSS
var(--p-*) refs can't be used; theme-lint scopes to CSS only.
2026-07-13 00:25:56 +01:00
EthanHealy01 ffc0755b37 chore(storybook): use DEFAULT_ACCENT_COLOR instead of hardcoded #3b82f6
Replace the literal blue in preview.tsx globalTypes/fallback and manager.tsx
with the DEFAULT_ACCENT_COLOR constant from constants/theme.
2026-07-13 00:23:30 +01:00
EthanHealy01 91e0815f93 fix(theme): lift dark canvas ~2% off pure black (#0a0a0b → #0f0f10)
Dark mode's darkest surface (--c-bg / --p-zinc-950) read as near-black. Lift
it ~2% lighter across every representation (primitive, Tailwind --gray-50/
--background channels, Mantine dark-7) so the canvas is a deep charcoal, never
pure black — consistent, not a one-off override.
2026-07-13 00:04:26 +01:00
EthanHealy01 e3b4bc2412 chore(theme-lint): read a fixed file list, not readdir→readFile
Static scanners (Aikido) flag the directory-listing→file-read flow as a
file-inclusion risk. Read a fixed THEME_FILES list via constant paths instead
(matching the other unflagged scripts); readdir is now only used to fail if a
new theme .css isn't registered, so coverage still can't lapse.
2026-07-12 23:59:31 +01:00
EthanHealy01 e9785b6b2f chore(theme): harden lint script, trim comments, fix CI lint/format
- theme-lint.mjs: resolve root, skip symlinks, refuse reads outside the
  theme dir (addresses Aikido file-inclusion finding)
- remove unused eslint-disable in .storybook/preview.tsx (frontend-validation
  --max-warnings=0)
- condense multi-line comments to one line; drop style-choice CSS comments
- prettier formatting
2026-07-12 23:54:26 +01:00
EthanHealy01 31b7c0069d fix comment 2026-07-12 23:18:33 +01:00
EthanHealy01 a8a5f3a88f initial colors and theme improvements 2026-07-12 23:17:37 +01:00
LudyandGitHub 80febc9993 fix(i18n): localize hardcoded frontend text in English and German (#6993) 2026-07-12 10:16:55 +01:00
LudyandGitHub c500c2fae7 fix(desktop): preserve RGBA format for Tauri app icon (#6990)
# Description of Changes

- Replaced the Tauri application icon with an RGBA-formatted PNG.
- Added a root `.imgbotconfig` that excludes the Tauri icon from
automatic image optimization.
- Fixed the `desktop:test` compilation failure caused by
`tauri::generate_context!()` rejecting the previous non-RGBA icon.
- Prevented ImgBot from potentially converting the icon back to an
unsupported indexed PNG while optimizing its file size.
- Verified that the current icon uses PNG Color Type 6 (`Truecolour with
alpha`).

```sh

[desktop:test] error: proc macro panicked
[desktop:test]    --> src/lib.rs:202:12
[desktop:test]     |
[desktop:test] 202 |     .build(tauri::generate_context!())
[desktop:test]     |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
[desktop:test]     |
[desktop:test]     = help: message: icon /Users/runner/work/Stirling-PDF/Stirling-PDF/frontend/editor/src-tauri/icons/icon.png is not RGBA
[desktop:test] 
[desktop:test] error: could not compile `***-pdf` (lib) due to 1 previous error
[desktop:test] warning: build failed, waiting for other jobs to finish...
[desktop:test] error: could not compile `***-pdf` (lib test) due to 1 previous error
task: Failed to run task "desktop:test": exit status 101
Error: exit status 101

```

---

## 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.
2026-07-12 06:33:09 +01:00
0a1b4ec173 Tidy policy/portal translation keys (#6962)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-07-11 14:10:51 +01:00
LudyandGitHub b8d8f028c9 fix: align portal icons with supported Material Symbols names (#6884) 2026-07-11 13:08:34 +01:00
dependabot[bot]andGitHub cd56367295 build(deps): bump actions/cache from 5.0.5 to 6.1.0 (#6968)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-11 13:04:22 +01:00
ConnorYohandGitHub 40a2d2844f Portal: honour RUN_SUBPATH in editor + login redirects (#6975) 2026-07-11 13:04:09 +01:00
dependabot[bot]andGitHub f79968f336 build(deps): bump docker/build-push-action from 7.1.0 to 7.3.0 (#6969)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-11 12:55:37 +01:00
df43e09eca build(deps): bump form-data from 4.0.5 to 4.0.6 in /frontend (#6676)
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-11 12:55:08 +01:00
cb0f335e8a [ImgBot] Optimize images (#6588)
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2026-07-11 12:48:53 +01:00
James BruntonandGitHub 38d06d3104 Make sqlite backend more resilient when using multiple runners (#6971) 2026-07-11 12:47:29 +01:00
ConnorYohandGitHub fe33378333 feat(portal): set a spend cap during PAYG checkout (two-step modal) (#6970) 2026-07-11 12:47:04 +01:00
Anthony StirlingandGitHub 5944cd106b Portal audit: label policy runs by their policy, flag automation sub-steps (#6937) 2026-07-11 12:46:44 +01:00
EthanHealy01andGitHub fd81bf4cf8 Tighten whitespace between search bar and tool list (#6977) 2026-07-11 12:44:19 +01:00
EthanHealy01andGitHub d23318cfa6 Feature/onboarding updates for policies and portal (#6926) 2026-07-11 12:41:48 +01:00
Anthony StirlingandGitHub 142544c9af Replace portal sidebar brand text with Stirling Processor wordmark (#6978) 2026-07-11 12:40:55 +01:00
LudyandGitHub 99a5f2a1bc chore(ci): include saas module in GitHub file path configuration (#6980) 2026-07-11 12:39:05 +01:00
James BruntonandGitHub 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.
2026-07-10 16:24:25 +00:00
EthanHealy01andGitHub 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"
/>
2026-07-10 16:24:16 +00:00
EthanHealy01andGitHub 532a80211f Test: pin ADMINS_AND_TEAM_LEADS default scoping to the owning team (#6966)
## What this does

Adds one test to `ResourceAccessServiceTest`: a foreign team's lead is
**denied** on a team-owned resource under the `ADMINS_AND_TEAM_LEADS`
default policy, even when an unscoped `isAnyTeamLeader` check would
admit them (stubbed `lenient()` to `true` precisely so the test fails if
the scoped path ever consults it again).

## Why

Main is already correct here — no behaviour changes in this PR. #6913
landed the scoped implementation (`matchesTeamLeadDefault`: ownerless
portal → `isAnyTeamLeader`, team-owned → `isLeaderOfTeam`), which
superseded #6893. The only piece not carried over was #6893's boundary
test, so the cross-team scoping isn't currently pinned by any test. This
adds that pin as cheap insurance for future refactors.

Verified the test does its job: it passes on main as-is, and fails if
the scoped check is swapped back to the unscoped one.

## Test plan
- `:proprietary:test --tests
"stirling.software.proprietary.access.service.ResourceAccessServiceTest"`
— green
- Spotless applied

Closes the loop on #6893.
2026-07-10 16:06:48 +00:00
Anthony StirlingandGitHub d06a367b87 SaaS role-based login landing (team leads → Processor) (#6960) 2026-07-10 15:23:00 +01:00
ConnorYohandGitHub 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.
2026-07-10 13:38:22 +00:00
ConnorYohandGitHub 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.
2026-07-10 13:28:33 +00:00
James BruntonandGitHub 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.
2026-07-10 13:27:40 +00:00
ConnorYohandGitHub 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).
2026-07-10 13:20:18 +00:00
Reece BrowneandGitHub 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.
2026-07-10 13:16:57 +00:00