* feat(admin): add a Billing tab for subscriptions and sales
The admin can now follow subscriptions and sales without opening Stripe:
- Summary tiles: active subscriptions (monthly/annual), MRR with annual
plans counted as 1/12, sales and revenue for 7d/30d/90d/12m/all time,
all-time sales, subscriptions cancelling at period end, and refunds
and disputes.
- A stacked chart of sales per day, week or month by plan family.
- A by-plan table: active grants, period and all-time sales and revenue.
- A paginated list of grants with plan, Stripe status, active/expired/
revoked and user filters, linked to the admin user, repository or
conference.
- One row of webhook health: pending, failed, oldest pending event, last
error and unreviewed payment reversals.
A sale is one checkout or subscription, so a paper pack's three grants
count once. Revenue is each sale's Stripe list price, resolved once per
price and cached in memory. Currencies are never added together. Without
Stripe the page shows counts and marks revenue as unavailable.
GET /api/admin/billing/summary and /grants sit behind the admin check.
Grants get a { purchasedAt, _id } index for the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: preserve payment history in admin billing totals
* fix: scope billing history and preserve original sale prices
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ui): browse and check every file in the anonymize preview
The "What reviewers will see" window now redacts the README GitHub actually
shows and lets authors open any file of the commit.
- README: resolved from the file list like GitHub (.github/, root, docs/,
any case, Markdown first) and only that path is tested against the
"Leave files unredacted" rules. Before the list loads, only root names are
assumed, so leaving docs/ unredacted no longer unredacts the root README.
The file bar shows the README's real name.
- New GET /api/repo/:owner/:repo/file?commit=&path= (same access checks as
/tree): raw text, no-store, path traversal rejected, 1 MB cap
(file_too_big, 413), binary files reported as { binary: true }.
/tree also lists binary files (binaryFiles) for the preview's tree.
- Tree rows open files (keyboard accessible, explorer active rail). Text is
redacted through /api/anonymize-preview by the same batcher as the README;
files left unredacted are sent without terms and tagged "Left unredacted"
(opens the dialog). Markdown renders like the README, code is highlighted
with Prism, images/PDF/binary/too-large files get a short note. Loading
and error (Retry) states reuse rp-state.
- File and folder names are redacted through /api/anonymize-preview.
- Caption counts the open file; "<name> left unredacted" when preserved.
- Phones: the Files button opens the tree as a sheet.
- dev-mock: a jsmith/robust-repair source with tree, file and
anonymize-preview endpoints and fixtures carrying the mock's terms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(ui): reuse the explorer in the anonymize preview
Share the explorer file renderer and tree with the author preview. Apply
preserved-file rules on the server and keep the reviewer rendering and
redaction slots consistent across both views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep the reviewer page unchanged by the preview reuse
The preview reuses tree.js and the shared file viewer; its differences now
apply only inside the anonymize preview.
- The narrow-pane rule that hid the file meta and Symbols applied to the
reviewer page too (visible with a widened sidebar); it is scoped to
.reviewer-preview. The token capture moves from body to the page that
holds the preview.
- tree.js: aria-expanded/aria-current and tree-only scrolling apply only
when the tree is embedded (selectedPath set); the reviewer page keeps
scrollIntoView and its original markup.
- file-viewer: the extra menu-closing click listener, the #L hash writes and
the Ctrl+F/G and Alt+Z shortcuts are limited to the preview (shortcuts
only while its code has focus), so the form keeps the browser's Find.
- Preview: the tree's file list is cached between renders, name batches are
chunked under the JSON body limit, /api/anonymize-preview accepts up to
3 MB so files up to the 1 MB preview limit can be redacted, and on phones
the caption's "<name> left unredacted" opens the dialog (the header tag
squeezed the path out).
- dev-mock: long and nested names for the tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): restore the redaction number on the right and level the slots
The redesign centered the term number in every redaction slot; it sits on
the right edge again, in code, Markdown, the tree and the breadcrumb. Tree,
breadcrumb and paragraph slots also sat about 2px below the text; they now
center on the line's letters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: address preview navigation and path review findings
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The UI was uneven: the home page looked finished, but app pages used three
different display typefaces, the anonymize flow changed frame between steps,
and the dashboard, explorer and admin screens each had their own controls,
status colours and edge-case handling.
Make every screen follow one system: shared page header, grid, buttons,
filters and status colours; a repository picker and step trail on anonymize;
a "What reviewers will see" preview of the actual reviewer page; consistent
file headers and viewers for every file type; truthful admin charts and
tables. Fold the accumulated override blocks back into style.css (22%
smaller, no visual change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Subscriptions sold at a replaced price lost their plan, because a
subscription was matched only against the configured price. Resolve
prices through the configured price, STRIPE_LEGACY_PRICES, then a
registry that now also records subscription prices and every configured
price at startup. Store the price on subscription grants.
Review fixes:
- Recover a Never expire add-on found by reconciliation before expiring.
- Open Checkout for the subscription cycle the hosting option shows.
- Report conference hosting only while the package payment stands.
- Keep automatic updates on a paid-slot repository while its owner subscribes.
- Apply the repository expiration limit only to repositories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most authors use the service for one paper a few times a year, so a
subscription does not fit. Add cheap one-time options (Paper pack, Extra
repository, Live updates, Extended expiration, Never expire), cap free
dated expiration at 5 months, keep cheaper subscriptions for heavy users,
and make every new conference a paid review package in three sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: let owners leave chosen files unredacted (#941)
Redacting self-citations in a bibliography reveals which papers are the
authors' own. Owners can now pick files whose content is served as
written; file names are still redacted. Patterns follow .gitignore rules.
The form gets a file picker fed by GitHub's tree API, and the terms box
grows with its content and only warns about regex symbols that can stop a
term from matching.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: honour !patterns, keep picked braces literal, preview unredacted README
- Patterns now apply in order and the last match wins, as in .gitignore,
so "*.bib" then "!private.bib" excludes private.bib instead of
preserving every other file.
- Picked paths escape glob characters, braces included, with a
backslash; "[{]" did not survive brace expansion.
- The README preview drops terms when the README is an unredacted file,
and refreshes when the list changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep rule order when deduplicating unredacted-file patterns
Rules apply in order and the last match wins, so dedupe keeps the last
copy of a repeated pattern instead of the first. The picker also keeps a
plain path in its place when it follows another rule, rather than moving
it to the front as a pick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: apply unredacted-file rules to folders, as in .gitignore
A rule that matches a folder now covers every file inside it, so "*.bib"
then "!private" keeps private/refs.bib redacted. A trailing "/" limits a
rule to folders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: folder unchecks win over broad rules; accept short commit hashes
- Unchecking a folder that a broader rule such as "*.bib" still covers
appends a "!/folder/" exclusion, so its files really go back to being
redacted.
- The tree endpoint accepts the 6+ character hashes the commit field
allows and resolves them to a full SHA before reading the tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: picker counts folder rules exactly as the server does
Rules ending in "/" stay patterns instead of being read as picked files,
and the checkboxes now run the saved rule list (picks, then patterns)
through the server's matcher, so the dialog cannot show files as
redacted that the server leaves unredacted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: insert escaped terms literally in "Match as written"
String.replace expanded "$&", "$'" and "$1" in the term, so the fix
could corrupt the line. Splice by index instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Backend, scripts, docs and their tests take main's newer code where both
sides changed the same lines (the shared blob cache, GitHubStream and
markdown fixes landed on main after the Go migration branch was cut).
The Dockerfile keeps the Go build from the migration, plus main's
owner-alert-protocol label; .gitignore keeps both sides' entries.
Frontend keeps the redesign and folds in main's features: Stripe billing
in the New anonymization submit bar, GitHub access repair, redaction
syntax help, the preview expand toggle, configurable redaction mask,
folder counts, reviewer-route header rules, link expiry (now in the
viewer's snapshot details) and the explorer error card. main's
narrow-only wrap logic is dropped because the redesign wraps on desktop
and scrolls sideways on phones.
Tests: 1534 passing with SESSION_SECRET set (as in CI). Three main tests
were updated for the agreed viewer design (no site header in the viewer,
no repository-wide file count).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Viewer: full-height app bar with a Help menu, search + ZIP sidebar, one
toolbar for README and code, reading mode with a scroll-tracking outline,
docked find for Markdown and code, and redaction slots (a blank stone slot
holding the term number) in prose, headings, code, the tree and the path.
Landing and FAQ: live redaction demo, serif display headings, teal accent.
Author screens: My work table with copy/open/edit actions, a usage meter,
a single-container New anonymization flow with a sticky submit bar, themed
form controls, and consistent GitHub connection cards.
Admin: underline tabs, severity-first errors chart and list, error tiles
that filter (older pages load until matches appear), queue and overview
health states, and shared toolbar heights and borders.
Fixes: guarded localStorage access, failed README images render a
placeholder, phone hover states no longer stick, and UI tests updated for
the new copy and markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>