tests/backend-new/specs/** are mock-heavy unit tests that need
per-file isolation (vi.mock must apply before the SUT is loaded,
and a shared module graph defeats it). tests/backend/specs/**
share rustydb and need the old isolate:false sequential model.
Split via test.projects (vitest 4): unit gets isolate:true +
parallelism; integration keeps the existing serial config.
Different sequence.groupOrder values (1 vs 2) are required by
vitest when projects have different maxWorkers settings.
Fixes 8 mock-related test failures in updateStatus, firstAuthorOf,
and updateCheck-optout that came in from the develop merge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When internal code imported via the exports map (ep_etherpad-lite/node/x)
AND via a relative path (../../node/x), vite-node resolved two distinct
module instances. Prom-client top-level Counter() calls ran twice and
threw "metric already registered", cascading ~35 test failures. Fix adds a
resolve.alias in vitest.config.ts that rewrites ep_etherpad-lite/<subpath>
to the .ts source, so the two spellings collapse to one module instance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CJS plugins keep working unchanged via the require condition; ESM
plugins are an opt-in track using extension-explicit imports.
Documents two known limitations: trailing-slash node/eejs/ and CJS
require() of db modules (ueberdb2 is ESM-only).
The 'with plugins' jobs install ep_markdown / ep_readonly_guest / etc.
which require ep_etherpad-lite at install-time. The dist + dist-cjs
twins must exist before pnpm resolves those subpath imports.
Also run check:exports as a fast canary before plugin install.
predev builds once before the dev server starts; dev:watch keeps
tsdown running alongside the server. The pretest hook (added in
the tsdown setup commit) auto-builds before vitest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins that ship CJS-only entries (e.g. ep_readonly_guest's
ep_readonly_guest.cjs) and ESM-only entries previously hit the loader's
extensionless fallback path and failed because only .ts and .js were
tried. Add .cjs and .mjs to the candidate list.
The verifier walks the exports map and asserts each target exists.
First run caught that the '.' entry's require condition pointed at
dist-cjs/node/server.cjs which is never built (server.ts has
top-level await, excluded from the CJS build). Drop the require
sub-condition and the now-dead 'main' field; plugins consume
subpaths, not the package root.
The previous attempt added stub .cjs files at dist-cjs/node/eejs/.cjs
to make the trailing-slash require() resolve. But the stubs were
empty — plugins would resolve and then immediately crash calling
methods on an empty module. Worse than failing fast.
Accept that 'require("ep_etherpad-lite/node/eejs/")' (trailing
slash) is not supported by the exports map. Affected plugins must
drop the trailing slash to migrate. The bare form
'require("ep_etherpad-lite/node/eejs")' works as before.
Previous commit dropped the './node/eejs/' entry to silence DEP0155.
That warning applies only to folder mappings (string target ending in
/), not to exact-match keys with conditional object values. Real
plugins use the trailing-slash form (see PR #7605 CI logs).
Implementation notes:
- Re-adds './node/eejs/' exports key with object-condition value.
Node 24/26 still fires DEP0155 for the trailing-slash specifier
(the warning is tied to the caller's import path, not just the
exports key format), but resolution succeeds.
- tsdown build:done hooks emit dist-cjs/node/eejs/.cjs and
dist/node/eejs/.mjs stubs that Node's folder-pattern expansion
resolves to when the import suffix is empty ('eejs/' + '').
- Adds explicit './node/eejs/index.js' and './node/eejs/index'
entries so the ESM import('ep_etherpad-lite/node/eejs/index.js')
is not hijacked by the trailing-slash folder-prefix match.
- Adds 'ep_etherpad-lite/node/eejs/' to cjsResolvableSubpaths in
the exports_map spec.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes CJS plugins' require() calls to dist-cjs/*.cjs twins while
keeping ESM consumers on dist/*.mjs (tsdown emits .mjs for ESM). The
trailing-.js wildcard handles plugins that wrote require(...'.js')
with an explicit extension. tests/backend has only an import
condition because CJS build excludes it (top-level await).
Also fixes Settings.ts getEpVersion() to use a static JSON import
instead of a build-path-relative requireFromHere() call, which broke
when resolved from dist-cjs/node/utils/. Test file updated: split
cjsSubpaths into resolvable vs loadable sets since DB modules
transitively depend on ueberdb2 (ESM-only, no require condition).
Builds .ts sources to dist/*.mjs (ESM) and dist-cjs/*.cjs (CJS) so the
upcoming exports map can route plugins' require() calls to the CJS
twin while ESM consumers use the .mjs originals. No source code is
moved or rewritten. tsdown 0.22.0 emits .mjs for ESM regardless of
the outExtension callback; accept that convention.
The CJS entry set excludes node/server.ts (top-level await is not
valid in CJS) and tests/backend/** (common.ts transitively imports
server.ts). The ESM entry set includes both.
vi.setConfig({hookTimeout: N}) inside a before() callback is a no-op —
vitest reads hook timeouts before the hook runs. padLoadFilter genuinely
needs 120s for its setup, so pass it as the second arg. The two 60s
files drop the override entirely (matches the global default already
set in vitest.config.ts).
Three admin test files brought in by the develop merge (5afd466bb) still
used mocha-shape this.timeout/this.skip. Converts them to vi.setConfig
and ctx.skip per the pattern established in 95f753c80.
Both symbols are already exported at lines 300 and 348. The trailing
re-export at line 438 caused esbuild 'Multiple exports with the same
name' build errors in PR #7605 (Backend tests / Linux without plugins).
Captures the design for landing PR #7605 without breaking the existing
plugin ecosystem. Covers the three CI failure causes (duplicate export,
develop merge, plugin resolution) and concretes the dual-emit
ep_etherpad-lite package with an exports map, tsdown build, plugin
loader updates, and a fixture-based testing strategy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related operator-facing docs gaps, both surfaced by #7819:
1. settings.json on disk is a *template*; env-var substitution happens
at load time in memory only. Operators repeatedly mistake the
templated file for a stale config because the docs never spell out
that the on-disk file is intentionally unchanged by env vars.
2. The default docker-compose.yml puts settings.json in the container's
writable layer with no host mount, which means admin /settings edits
are silently lost on `docker compose down && up`, `pull`, or
watchtower — but preserved across plain `restart`. Operators don't
reliably know which compose verbs recreate the container.
Adds two prose sections to doc/docker.md (explaining both gotchas, with
a recreate-vs-restart table) and a commented-out `./settings.json:…`
bind mount in both docker-compose.yml and the README compose example.
Bind mount is opt-in so existing setups behave identically.
No runtime change.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: page sessionstorage cleanup to avoid OOM (#7830)
SessionStore._cleanup() previously called `findKeys('sessionstorage:*',
null)`, materialising every session key into a single array. On decade-
old MariaDB installs with millions of sessions this OOMs the node
process within ~15 minutes — see #7830.
Switch to ueberdb2 6.1.0's findKeysPaged with a 500-key page size, and
yield to the event loop between pages so the DB driver can release each
page's buffered rows and request handlers can interleave.
The break is now driven by `page.length === 0` rather than `page.length
< CLEANUP_PAGE_SIZE` so a stubbed/throttled paged source still iterates
the full keyspace.
Adds a regression test that seeds 50 sessionstorage rows, monkey-patches
`DB.findKeysPaged` to use a 4-key page, runs cleanup, and asserts every
expired row is removed plus every valid row preserved across page
boundaries.
Closes#7830
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Qodo review on #7831
Four follow-ups raised by Qodo on the session cleanup paging fix:
- DB.ts: fail-fast at init() if any required wrapper method (incl.
findKeysPaged) is missing, so a stale ueberdb2 pin surfaces at boot
rather than crashing the first cleanup run an hour later.
- SessionStore: bound a single _cleanup() run to 10 minutes. Under
sustained session creation the keyspace can grow faster than cleanup
drains it; without a budget the next scheduled run would never fire.
When the budget hits, log a warning and let the next run continue.
- SessionStore: log the defensive `page[0] <= after` cursor-stall break.
Previously the loop exited silently, leaving expired rows behind with
no operator-visible signal of the backend regression.
- Tests: the paged-cleanup regression test now removes both expiredSids
AND validSids in finally, so a failed assertion doesn't leak rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: note paged session cleanup in CHANGELOG + settings template
CHANGELOG.md picks up an entry under 3.1.0 Notable fixes describing the
OOM cause, the paged iteration, the 10-minute per-run budget, the
cursor-stall logging, and the fail-fast init guard.
settings.json.template's sessionCleanup comment adds the page-size,
budget, and pointer to #7830 so admins can reason about the new
behaviour from the template alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate lockfile against ueberdb2 6.1.2
Now that ether/ueberDB#983 unblocked the publish workflow (OIDC trusted
publishing), ueberdb2 6.1.2 is live on npm and the `^6.1.0` pin in
src/package.json resolves cleanly. Resolves the ERR_PNPM_OUTDATED_LOCKFILE
that was blocking CI on this PR.
29 SessionStore backend tests still green against the published tarball.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(docker): admin save persists across container restart (#7819)
The OP reports the symptom on the official Docker image specifically.
Adds two layers of coverage to docker.yml's build-test job, driven from
inside a container started against the same TEST_TAG the existing
test-container step uses:
1. New mocha spec adminSettings_7819.ts under tests/container/specs/api
— authenticates against /admin, opens the /settings socket, saves an
augmented JSON with an ep_oauth-shaped top-level block, and asserts
the next load reply contains the marker. Intentionally leaves the
marker on disk so the workflow can inspect it.
2. docker.yml now `docker exec test grep`s for the marker after
test-container, then `docker restart`s the container, waits for the
health probe, and re-greps. Both checks must pass — the first proves
the socket-driven save actually touched the file inside the
container layer; the second proves an in-place restart doesn't reset
it. A recreate (docker rm + docker run) would wipe the file, but
that's expected (image layer) and out of scope.
Container is started with `-e ADMIN_PASSWORD=changeme1` so the existing
settings.json.docker provisions the admin user; pad.js doesn't touch
/admin so the existing API specs are unaffected. test-container timeout
bumped 5s → 30s to cover socket connect + save round-trip, and the
mocha discovery extension list now includes `ts` so the new spec is
picked up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(docker): authenticate via /admin-auth/ POST, surface auth/load failures fast (#7819)
CI failed on #7821 with a generic 20s mocha timeout because the spec
hit GET /admin/ to grab a session cookie. webaccess.ts only treats
paths starting with /admin-auth as requireAdmin — and the container
runs with REQUIRE_AUTHENTICATION=false (default), so GET /admin/ never
issued a Basic challenge and Set-Cookie was empty. The socket then
connected unauthenticated, adminsettings.ts's connection handler
returned early without binding any listeners, and the load() promise
hung until mocha killed the test with no useful diagnostic.
Switch to POST /admin-auth/ (always-requireAdmin, regardless of
settings.requireAuthentication). Assert a 2xx with at least one
Set-Cookie before proceeding. Add an 8s timeout + meaningful error
message to load() so the "session was not admin" failure mode reports
immediately instead of burning the suite budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(docker): replace splice with hand-built payload (#7819)
Last CI failed because the splice-after-last-} approach landed a comma
between an existing trailing-comma-before-comment and the close brace
of settings.json.docker, producing `, /* … */, "ep_oauth"` — invalid
JSON.
settings.json.docker uses jsonc `/* */` and `//` comments and a
trailing-comma-before-comment-before-close shape that's annoying to
patch from the test side, and the existing isJSONClean has zero
backend coverage so the splice is going through Etherpad's lenient
write path anyway.
Switch to a hand-built minimal-but-viable settings document containing
the ep_oauth block. Three properties hold:
- We're testing the WRITE path, not the synthesis path. Whatever
bytes we send, the next `load` must return verbatim.
- The post-save document must survive `docker restart` (the next
step in docker.yml) — minimal-but-viable means port/users/dbType
are present so Etherpad boots back up and HEALTHCHECK passes.
- The next `load` reply must equal the bytes we saved
(`reply.results === augmented`) — stronger than `.includes()`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The admin saveSettings socket had zero direct backend coverage and the
e2e 'restart works' test only checked the page renders after restart —
neither catches a deployment that resets settings.json on restart, nor
the user-visible workflow that triggered #7819 (add a top-level plugin
block via Raw, save, watch it disappear).
Adds three backend specs for the saveSettings socket:
- payload is written byte-for-byte to settings.settingsFilename
- augmenting existing JSON with a new top-level block round-trips
through the next load reply
- /* */ comments survive the write path
Adds one e2e spec mirroring the #7819 workflow: open Raw, prepend an
ep_oauth-shaped top-level block, save, restartEtherpad(), re-login,
confirm the block is still in Raw and surfaces as its own Form-view
section ('Ep oauth' from humanize()).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ep_readonly_guest is archived (read-only on GitHub) and its
authenticate hook unconditionally swaps req.session.user with a
read-only guest, even when the request carries an HTTP Authorization
header. That silently demoted admin login attempts and stalled the
anonymizeAuthorSocket tests for 14 min/run on every with-plugins CI
matrix (#7795). The pre-fix theory blamed ep_hash_auth.handleMessage;
the actual hook trace is a red herring — handleMessage only fires on
the /pad namespace and never on /settings.
ep_guest is the maintained successor (same authors, same purpose).
1.0.72 on npm already includes the "defer to basic auth / admin
paths" fix backported to ep_readonly_guest by intent here. Swapping
the matrix unblocks the anonymizeAuthorSocket suite on Linux,
Windows, and the upgrade-from-latest-release workflow.
The runtime probe added in #7796 stays — it still catches any other
authenticate-hook plugin that rejects the test's plain-text
credentials (e.g. a future ep_hash_auth-style hashed-only plugin).
Reattribute its comment so future readers don't chase ep_hash_auth.
Closes#7795.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: design for URL base-path support (#7802)
Spec covers the architecture, header handling rules, components touched,
backwards-compatibility story, risks, and test plan for honoring
X-Forwarded-Prefix / X-Ingress-Path under trustProxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: amend #7802 spec with discovery of pre-existing proxy-path helpers
After exploring the codebase, much of the proposed architecture is
already in place (sanitizeProxyPath, padBootstrap.js basePath derivation,
admin SPA rewrite). Spec now reflects the actual delta: header source
expansion, /manifest.json prefix-awareness, socialMeta proxyPath honoring,
and template URL touch-ups for index/timeslider/pad/export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(plan): implementation plan for URL base-path support (#7802)
Adds the bite-sized TDD task list to ship X-Forwarded-Prefix /
X-Ingress-Path support: extends sanitizeProxyPath, makes /manifest.json
and socialMeta prefix-aware, touches up the remaining leading-slash
URLs in index/pad/timeslider/export templates, fixes a pre-existing
manifest .. count bug. Drops the originally-proposed <base href>
belt-and-braces after discovering it'd break the existing relative
URLs in pad.html/timeslider.html and wouldn't help plugin DOM
injection anyway (path-absolute URLs ignore <base>'s path component).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(proxy): accept X-Forwarded-Prefix and X-Ingress-Path under trustProxy (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pwa): make /manifest.json honor sanitised proxy-path (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(social-meta): honor proxyPath in from-request og:url and og:image (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(templates): index.html manifest + jslicense links honor proxyPath (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(templates): pad.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(templates): timeslider.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(templates): export_html.html manifest honors proxyPath when available (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: end-to-end coverage for X-Forwarded-Prefix / X-Ingress-Path (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(settings): trustProxy also enables X-Forwarded-Prefix / X-Ingress-Path (#7802)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: spec for admin/settings resolved runtime values (#7803)
Side-channel resolved+redacted settings alongside raw file blob.
Form view dropdowns and env pill chips reflect actual runtime values
instead of falling back to template defaults. Save round-trip is
unchanged so ${VAR:default} literals stay intact on disk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: implementation plan for admin/settings resolved runtime (#7803)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): add redactor for resolved settings payload (#7803)
Pure helper that walks the live settings module and replaces known
sensitive paths (users.*.password, dbSettings.password,
sso.clients[*].client_secret, sessionKey, …) with [REDACTED] sentinel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): emit redacted runtime settings on /settings socket load (#7803)
Existing 'results' raw-file blob is unchanged so the textarea editor
and saveSettings round-trip continue to preserve \${VAR:default}
literals on disk. New 'resolved' field carries the in-memory settings
module run through the redactor — admin SPA can use it to show actual
runtime values next to env-var placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): show resolved runtime value on EnvPill (#7803)
Admin SPA now stores the resolved field from the /settings socket
payload and exposes useResolvedAt(path) to walk it. EnvPill renders a
"→ active value" chip when the path is resolved, or "→ ••••••" with a
redacted tooltip when the server returned the [REDACTED] sentinel.
Old-server fallback (undefined resolved) keeps current behaviour.
The admin test script glob now picks up .test.tsx alongside .test.ts
so the new EnvPill tests run under tsx --test.
Closes#7803.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(updater): resolve pad author from token cookie, not session.user
Etherpad does not populate an authorID into the express-session user
object for pad visitors, so resolveRequestAuthor() always returned null
in production, causing computeOutdated() to return EMPTY and the
pad-side gritter to never fire.
Replace the session-based lookup with a cookie-based path that mirrors
how the socket.io handshake resolves pad-visitor identity: read the
HttpOnly `token` (or `<prefix>token`) cookie and call
authorManager.getAuthorId(token, user) via dynamic import (same
circular-init guard pattern as the PadManager import).
Update the test harness to mock AuthorManager instead of injecting a
fake req.session.user.author, and to set req.cookies.token directly.
All 9 cases continue to pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(openapi): clarify admin spec scope includes pad-side endpoints
/api/version-status is a public pad-side endpoint but lives in the
admin OpenAPI document because it shares the same internal route
registration. Add a note to info.description so downstream tooling
consumers are not misled into treating it as an admin-only route.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: design spec for #7799 outdated-notice redesign
Per-pad first-author gating, dismissable gritter, minor-or-more rule, drop vulnerable UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: implementation plan for #7799 outdated-notice redesign
12 bite-sized tasks, TDD-first where applicable; closes the spec end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): add isMinorOrMoreBehind, drop major/vulnerable helpers
Adds isMinorOrMoreBehind(current, latest) which returns true only when
the latest release is at least one minor version ahead (patch-only deltas
return false). Removes isMajorBehind, parseVulnerableBelow, and
isVulnerable from versionCompare.ts — callers in updateStatus.ts,
VersionChecker.ts, and index.ts will be updated in subsequent tasks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(updater): drop vulnerable-below directive and state field
Remove VulnerableBelowDirective type, UpdateState.vulnerableBelow field, and
all related scraping/checking logic (parseVulnerableBelow, isVulnerable imports).
Clean up Notifier, OpenAPI schema, and all test fixtures to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(updater): drop residual EmailSendLog vulnerable fields
Remove `vulnerableAt` and `vulnerableNewReleaseTag` from the
`EmailSendLog` interface, `EMPTY_STATE`, and the `isValidEmail`
validator — these backed the removed `vulnerable`/`vulnerable-new-release`
email kinds and are now dead code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add firstAuthorOf helper
Export firstAuthorOf() from updateStatus.ts — finds the lowest-numbered
author attrib in a pad's pool, skipping empty-string placeholders.
Covered by 6 vitest cases in tests/backend-new/specs/hooks/express/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add resolveRequestAuthor helper for HTTP GET
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): pad-aware /api/version-status with first-author gating
Replace global badge cache with a per-(padId, authorId) LRU cache. The
new response shape is {outdated: 'minor' | null, isFirstAuthor: boolean};
the old 'severe'/'vulnerable' enum is dropped entirely. computeOutdated
now resolves the pad's first author and compares it against the session
author before returning outdated:'minor', so the notice is only shown to
the person who created the pad.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): switch isSevere signal from major-only to minor-or-more behind
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(updater): end-to-end coverage for /api/version-status
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(openapi): /api/version-status pad-aware shape and gating
Add the /api/version-status GET operation to the admin OpenAPI spec with
the new pad-aware response shape: outdated enum reduced to [minor]|null,
isFirstAuthor boolean, and an optional padId query param.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(pad): remove unused #version-badge template and CSS
* feat(pad): replace persistent badge with first-author outdated gritter
Renames pad_version_badge.ts → pad_outdated_notice.ts and rewrites it
as a fire-and-forget gritter notice that only shows when the API reports
outdated=minor AND the current user is the pad's first author. Wires
the new maybeShowOutdatedNotice() call into pad.ts immediately after
showPrivacyBannerIfEnabled().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(pad): playwright coverage for outdated notice gritter
Six Playwright specs exercise maybeShowOutdatedNotice: null response,
isFirstAuthor:false guard, positive appearance + text, X-dismiss,
500 server error tolerance, and 8 s auto-fade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(pad): outdated-notice redesign + drop vulnerable-below docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(test): remove stale specs for deleted #version-badge surface
Delete the GET /api/version-status describe block from the legacy mocha
spec (asserted outdated:null and outdated:'severe' — both no longer match
the new response shape). The new vitest spec at
tests/backend-new/specs/hooks/express/updateStatus.test.ts covers this
surface comprehensively.
Delete src/tests/frontend-new/specs/pad-version-badge.spec.ts entirely:
all three tests reference the #version-badge DOM element removed in Task 8
and stub 'severe'/'vulnerable' enum values that no longer exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: clean stale references to vulnerable/severe in types, emails, docs
- Remove OutdatedLevel type (null|'severe') from types.ts — no consumers
remain after the badge redesign removed the severe tier.
- Fix Notifier severe-email body: was "more than one major release behind"
but isSevere now fires on minor-or-more, so update to "at least one
minor release behind the latest published version".
- Drop "vulnerability directives" from the /admin/update/status OpenAPI
description; replace with the actual response fields.
- Remove stale vulnerableBelow field from UpdateStatusPayload in
admin/src/store/store.ts — server no longer sends it.
- Fix docs/admin/updates.md: "pad-side badge" → "pad-side notice".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(l10n): short-circuit form controls in translateNode (no spurious warning)
`<select data-l10n-id="...">` with `<option>` element children — the
pattern used by ep_headings2, ep_align, ep_font_size, ep_font_family, …
— used to drop into the textContent branch of html10n.translateNode and
hunt for a text-node child to overwrite. There is none, so the loop
exited with `found = false` and emitted:
Unexpected error: could not translate element content for key ep_headings.style
The SELECT/INPUT/TEXTAREA aria-label fallback already lived inside the
same else-branch, *after* the warning, so the accessible name landed
correctly but the noisy console line still fired on every pad load.
Move the form-control case into its own `else if`, before the text-node
hunt: aria-label is the only sensible localization target for these
elements (a <select>'s text is its <option> labels, not its own name).
Closes the console warning reported on Etherpad 3.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): actually exercise the form-control short-circuit branch
Qodo's review on #7797 caught two real test bugs:
1. `pad.toolbar.bold.title` ends in `.title`, which is in html10n's
attribute allowlist. translateNode picks `prop = 'title'`, takes
the first branch (node[prop] = str.str), and never reaches the
textContent path where the warning lives. The test would pass
without my fix.
Switched the key to `pad.loading` — same stable pad-bundle
translation, but the suffix isn't in the allowlist, so `prop`
defaults to `textContent` and the test actually exercises the
regression path.
2. `page.waitForTimeout(50)` is a fixed sleep, but `html10n.localize`
runs its work inside a `build()` callback, so completion timing
depends on the loader. Replaced with a deterministic `html10n.mt
.bind('localized', …)` await.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin/pads): apply filter chip server-side, before pagination
Before: PadPage's filter chip (`active`/`recent`/`empty`/`stale`) ran
on the client AFTER the 12-row page slice was already on screen. On a
deployment with hundreds of pads it produced obviously wrong results
— click "empty pads" on page 1 with 100 empties and only the 0–12
empties within the current page passed the filter. thm reported this
on a 3.1.0 deployment.
Move the filter into `PadSearchQuery` so the `/settings` socket can
apply it before slicing:
1. pattern filter on names (cheap)
2. hydrate metadata for the matching pad universe iff a non-`all`
filter is set or a non-`padName` sort is requested
3. apply filter chip on the hydrated set
4. sort + slice → `total` reflects the filtered universe so the
pagination footer makes sense
The original handler also had a 4-way `if/else if` that duplicated the
hydrate-and-sort loop per `sortBy`. Folded those into one pipeline
with a single comparator switch.
Client side, `PadPage.tsx`:
- drop the client-side `filteredResults` filter (server already filters)
- chip click writes `filter` into searchParams (debounced refetch) and
resets `currentPage` to 0
- older clients that don't send `filter` keep working — server defaults
to `all`
Stats cards (totalUsers/activeCount/emptyCount) still count the visible
page only — that's a pre-existing UI limitation tracked separately.
Closes the regression thm reported.
Test plan
- `tsc --noEmit` clean (server + admin)
- New backend spec `padLoadFilter.ts` exercises filter:empty with
small `limit` to lock in the bug-fix, plus all/active/omitted cases
- `5 passing` locally on Node 25
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* address Qodo review on #7798
1. Functional setState updaters for every searchParams mutation
(Qodo bug 1). The debounced pattern handler captured a render-time
snapshot of searchParams; a faster chip click or sort change in
between would be silently reverted when the debounce fired. Now
every mutation merges against the latest state.
2. Concurrency-limited hydration (Qodo bug 3). The earlier draft
issued Promise.all over the full candidate set, fanning out to
thousands of in-flight padManager.getPad() reads on busy
deployments. New mapWithConcurrency() caps concurrent loads at 16
— empirically enough to saturate a single ueberDB driver without
pushing the event loop into back-pressure.
3. Test cleanup deletes the injected test-admin (Qodo bug 4). The
original snapshot/restore pattern saved `settings.users` by
reference; reassigning the same reference in after() left the
inserted key in place and could leak into later backend specs.
4. Document the new `filter` field on the `padLoad` socket query in
admin/README.md (Qodo rule violation 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>