mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-21 00:59:11 +00:00
6166 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fba4a17fcc
|
fix(API): hide SYSTEM_AUTHOR_ID from listAuthorsOfPad (#7793)
* fix(API): exclude SYSTEM_AUTHOR_ID from listAuthorsOfPad
Pad.SYSTEM_AUTHOR_ID ('a.etherpad-system') is the synthetic author
Etherpad attributes inserts to when the HTTP API receives a call
without authorId (setText, setHTML, appendText, the server-side
import flows, and plugins like ep_post_data). It exists so the
changeset's text and attribs stay in sync — without ANY author
attribute, pad.atext drifts and clients fail setDocAText
reconciliation when loading the pad. See Pad.ts:96-105 for the
full rationale.
That bookkeeping detail was leaking through listAuthorsOfPad: a
pad whose only "contributor" is the system author still reported
one authorID, which the existing tests in pad.ts and
appendTextAuthor.ts (and presumably any caller that uses
listAuthorsOfPad to count real users) treat as a real participant.
Filter SYSTEM_AUTHOR_ID at the API surface so internal attribution
stays internal. getAllAuthors() and downstream callers (copy,
anonymize, atext verification) keep seeing the synthetic id —
this only narrows the public listAuthorsOfPad response.
Fixes #7785
Fixes #7790
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(api): note that listAuthorsOfPad omits the system author
Match the runtime behaviour from the previous commit — the
synthetic 'a.etherpad-system' author used for unattributed inserts
is filtered out of the listAuthorsOfPad response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8c6104c5d5
|
harden: assorted server-side tightening for 3.0.2 (#7784)
* harden: assorted security tightening across server entry points
A bundle of defence-in-depth hardening picked up during an internal
audit pass. Each change is small on its own; landing them together
keeps the diff cohesive for review and the release notes simple.
Production-side changes:
- src/node/handler/APIHandler.ts: tighten the OAuth JWT validation
path on the HTTP API. Verify the signature before reading any
claim off the payload, and require the admin claim to be strictly
true (not just present). Switch the apikey comparison to
crypto.timingSafeEqual.
- src/node/handler/{Import,Export}Handler.ts: derive temp-file path
tokens from crypto.randomBytes(16) instead of Math.random.
- src/node/hooks/express/tokenTransfer.ts: enforce a 5-minute TTL
on transfer records, make redemption single-use (remove before
response), and drop the author token from the response body —
the HttpOnly cookie is the only delivery channel.
- src/node/utils/sanitizeProxyPath.ts (new): shared sanitiser for
the `x-proxy-path` header. Used by admin.ts (HTML/JS/CSS
substitution) and specialpages.ts (legacy timeslider redirect).
Strips characters outside [A-Za-z0-9_./-], collapses leading
`//+` to a single `/`, rejects `..` traversal. admin.ts also
emits Vary: x-proxy-path and Cache-Control: private, no-store.
- src/node/db/Pad.ts + src/node/utils/ImportHtml.ts: centralise
the "every insert op carries an author attribute" invariant in
Pad.appendRevision so all non-wire callers (setText, setHTML,
restoreRevision, plugin paths) get the same check the socket
handler already enforces. Pad.init and setPadHTML now
substitute SYSTEM_AUTHOR_ID when no author is supplied — same
pattern setText/spliceText already use.
Tests:
- src/tests/backend/specs/api/jwtAdminClaim.ts (5 cases)
- src/tests/backend/specs/tokenTransfer.ts (6 cases)
- src/tests/backend/specs/proxyPathRedirect.ts (5 cases)
- src/tests/backend/specs/padInsertAuthorInvariant.ts (4 cases)
- src/tests/backend-new/specs/sanitizeProxyPath.test.ts (14 vitest cases)
- src/tests/backend/common.ts: add generateJWTTokenAdminFalse helper.
Regression sweep across 16 backend spec files: same 5 pre-existing
failures on develop reproduce after this change; +20 new passing
tests; no new failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* harden: rewrite imported .etherpad records to satisfy the insert-op invariant
Legacy .etherpad exports (and exports from older server-internal flows
that didn't substitute SYSTEM_AUTHOR_ID) can contain `+content` insert
ops without an `author` attribute. The previous commit's appendRevision
guard rejects that shape, but setPadRaw bulk-writes records directly
to the DB and never goes through appendRevision -- so a hand-crafted
.etherpad file could persist non-conforming data that any subsequent
setText / setHTML / restoreRevision call would then refuse to extend.
Add a pre-pass over the parsed import that:
- walks revs in numeric order, sanitising each changeset's `+` ops
against the cumulative pad pool (mutating the pool to register
SYSTEM_AUTHOR_ID when needed);
- re-applies each (post-sanitisation) changeset to a running atext
so the head atext and any key-rev meta.atext / meta.pool
snapshots are re-derived in lock-step with the rewritten revs
(otherwise pad.check's deep-equal at the end of setPadRaw would
fail on the attribute-number drift between the sanitised head
state and the now-stale key-rev snapshot);
- leaves already-conforming payloads untouched (no log noise on
good imports).
Returns the number of ops rewritten so the import can log a single
warning per legacy file rather than per-record. Pure-newline `+` ops
are exempted -- same whitelist as the wire-side guard.
Tests:
- tests/backend/specs/padInsertAuthorInvariant.ts: two new cases
drive setPadRaw end-to-end with a hand-crafted legacy payload
(asserts the head atext gains a `*N` attribute reference and the
pool registers `[author, a.etherpad-system]`) and with a
conforming payload (asserts the data round-trips unchanged).
Regression sweep across 17 backend spec files: 209 passing / 12
pending / 5 failing -- same 5 pre-existing failures present on
unmodified develop. +2 new passing tests; no new failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1864cd2fc1 | bump version | ||
|
|
8b9d9f31ec
|
build(deps): bump ueberdb2 from 5.0.48 to 6.0.2 (#7734) | ||
|
|
a4261c2895
|
fix(a11y): drop role=textbox / aria-multiline from innerdocbody (#7778) (#7782)
Murphy's 2026-05-16 re-test of #7255 reported "you still can't cycle through the text properly line by line to press links and such". The narrower toolbar/measurement fixes in #7777 don't address this — it's caused by the editor body advertising textbox semantics. role="textbox" + aria-multiline="true" pin NVDA/JAWS into focus mode for the whole pad. In focus mode arrow keys move the caret one character at a time, the P/H/K rotor shortcuts are suppressed, and links don't surface in the links list. That matches Murphy's symptoms exactly. contenteditable="true" by itself is enough to tell AT this is editable. Without the textbox role, NVDA/JAWS browse the content as document-mode HTML — line-by-line arrow nav, headings rotor, links list all return. aria-label / aria-describedby stay so the pad is still announced as "Pad content" with the keyboard hint on focus. This is the lighter alternative to the AT-only read mirror originally sketched in #7778 — ARIA-only, no DOM restructuring, no plugin impact. Refs #7255 #7777 |
||
|
|
278acb10cb
|
Drop swagger-ui, document telemetry, add opt-outs (#7524) (#7757)
* docs: design spec for #7524 drop swagger-ui + privacy opt-outs Three-deliverable plan: vendor RapiDoc to replace swagger-ui-express (Scarf-injecting), add privacy.updateCheck and privacy.pluginCatalog opt-outs for our two outbound calls, and ship PRIVACY.md as a public stance doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for #7524 swagger-ui + privacy opt-outs Twelve TDD-flavoured tasks: privacy settings shape, UpdateCheck + installer opt-outs (each with a failing-test-first cycle), admin backend/UI plumbing, dependency drop, vendored RapiDoc, PRIVACY.md, final verification matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): add privacy block to settings shape Adds privacy.updateCheck and privacy.pluginCatalog, both defaulting to true so behavior is unchanged until operators opt out. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.updateCheck=false in UpdateCheck check() and getLatestVersion() now early-return when the setting is off. Logs once on first skip. The admin "update available" panel already tolerates an undefined latestVersion. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.pluginCatalog=false in installer Extracts the gate into pluginCatalogGuard.ts so it can be unit-tested under vitest without dragging in the CJS require() chain from installer.ts. getAvailablePlugins() now throws the tagged disabled error before any fetch. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): emit results:catalogDisabled when pluginCatalog off Short-circuits the four catalog-driven socket events. The install/ uninstall events are untouched so operators can still install by plugin name even when the catalog is disabled. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bin): stalePlugins reads updateServer and honours privacy flag Was hardcoding static.etherpad.org and ignoring opt-out. Now exits 0 cleanly when privacy.pluginCatalog=false. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): document privacy block in settings template Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): replace swagger-ui-express with RapiDoc shell Drops the swagger-ui-express dep (third-party Scarf telemetry pixel, see swagger-api/swagger-ui#10573) and serves /api-docs with a static HTML shell that mounts <rapi-doc>. /api-docs.json is unchanged. The vendored RapiDoc asset is added in the next commit so the tree is broken for one diff hunk — pair this with the rapidoc-min.js commit during review. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): vendor RapiDoc 9.3.4 (MIT) as static asset Pinned bundle with checksum in VERSION. Replaces swagger-ui-dist which shipped a Scarf telemetry pixel. Disables RapiDoc's bundled Google Fonts request via load-fonts="false" plus explicit regular-font/mono-font system stacks — RapiDoc's CSS @font-face rules would otherwise fetch Open Sans from fonts.gstatic.com at render time. Also fixes the /api-docs route's res.sendFile to use an absolute path resolved via settings.root (the previous {root: 'src/static'} was resolved from CWD which is already src/, producing src/src/static). Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): banner when plugin catalog is disabled Subscribes to results:catalogDisabled and renders a localized info banner on the plugins page. install/uninstall still function via CLI. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PRIVACY.md and README/CHANGELOG pointers Publishes Etherpad's stance on telemetry: two documented, opt-out outbound calls; no third-party analytics; no install-time phone-homes in our deps. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): await checkPluginForUpdates and emit array on error Qodo flagged that checkUpdates emitted the unresolved Promise (missing await) and emitted {} for updatable on the error path, both breaking the admin UI's expected string[] shape. Pre-existing bug surfaced when the surrounding block was edited for the privacy.pluginCatalog gate. Refs #7524 * feat(api-docs): swap RapiDoc for Scalar (actively maintained) Per @SamTV12345's review on #7757: RapiDoc has been effectively unmaintained for a while. Scalar (https://github.com/scalar/scalar) is MIT-licensed, actively developed, and ships a self-contained standalone bundle that works the same way for our purposes. Privacy posture is preserved by configuring the embed: - withDefaultFonts: false (no fonts.scalar.com woff2 fetch) - telemetry: false (defensive) - agent.disabled: true (no api.scalar.com/vector/* calls) - mcp.disabled: true (no MCP integration) - showDeveloperTools: 'never' - hideClientButton: true Verified with headless Chromium: page loads /api-docs, mounts Scalar, renders the Etherpad OpenAPI document, and makes zero requests to any host other than localhost. Vendor: - src/static/vendor/scalar/standalone.js (@scalar/api-reference 1.57.2) - src/static/vendor/scalar/VERSION (sha256 pinned) - src/static/vendor/scalar/LICENSE (MIT) Removed: - src/static/vendor/rapidoc/* Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8c4f974122
|
fix(pad): keep menu_right visible on readonly pads by default (#7783)
PR #X (issue #5182) added a client-side `$('#editbar .menu_right').hide()` for readonly pads, opt-out via `?showMenuRight=true`. The intent — clean chrome for iframe-embedded readonly announcement pads — was good but the implementation hid the userlist toggle along with import/export. That has two unwanted effects on non-embed deployments: * Plugins like ep_guest inject their "Log In" button into `#myuser` inside the userlist popup, which lives under `.menu_right`. When the guest user (readOnly: true) lands on a readonly pad, the button they need to escape readonly is hidden. Chicken-and-egg. * Settings, embed, home, timeslider, showusers are all legitimately useful for readonly viewers and got removed alongside the actual write-only controls. The server already does the right thing without help from the client: * src/node/utils/toolbar.ts:282-290 strips `savedrevision` from the right toolbar when isReadOnly is true. * src/static/css/pad/popup_import_export.css:1 has `.readonly .acl-write { display: none }`, which hides the Import column of the import/export popup. Export stays visible (and is legitimately useful in readonly). Drop the client-side blanket hide. The iframe-embed use case from #5182 is still served by `?showMenuRight=false` (the existing handler at src/static/js/pad.ts:91-107), and `?showControls=false` continues to hide the entire editbar for callers who want even the left menu gone. Tests: rewrite `hide_menu_right.spec.ts`. The "readonly pad hides .menu_right by default" assertion is inverted; a new test confirms `?showMenuRight=false` still hides on readonly pads. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
294158e773
|
harden: reject USER_CHANGES inserts without an author attribute (#7773)
* harden: reject USER_CHANGES inserts without an author attribute Insert ops MUST carry the author attribute reference so that pad.atext.text and pad.atext.attribs stay in lock-step. An accepted insert with empty attribs would grow text without contributing matching attribute markers, leaving the stored AText in a state where the two iterables disagree on length when reconstructed. Downstream clients then fail reconciliation in ace2_inner.ts:setDocAText with 'mismatch error setting raw text in setDocAText' on every subsequent pad load — making the affected pad effectively unloadable until manually repaired. This commit adds a single defensive check inside the existing per-op validation loop in handleUserChanges: when an op is a '+' (insert) and its attribs string doesn't yield an 'author' entry via AttributeMap.fromString, reject with badChangeset. The check piggybacks on the wireApool that was already constructed for the prior author-match validation, so no extra parsing. Test fixtures in messages.ts were updated to send proper author-attributed inserts plus the matching apool (mirroring what the JS web client always does). A new regression test 'insert without author attribute is rejected' locks in the new behaviour. * harden: also close the HTTP API / plugin path via stable system author The first commit closed the socket.io USER_CHANGES hole. This commit closes the parallel path through Pad.spliceText (used by API.setText, API.appendText, the import flow, and plugins like ep_post_data) where an unattributed insert would otherwise produce a malformed AText. Approach: instead of REJECTING (which would break ep_post_data and many existing tests that call setText/appendText without an authorId), substitute a stable system author when none is provided. The resulting changeset is properly attributed, the AText stays well-formed, and existing callers continue to work unchanged. Plugins that want named author attribution should still pass an explicit authorId (e.g., one allocated via authorManager.createAuthor). Pad.SYSTEM_AUTHOR_ID = 'a.etherpad-system' — a stable identifier that appears in the pad's attribute pool when internal callers (HTTP API, plugins, server-side imports) write text without naming an author. The existing 'attribute changes by another author' protections still apply to socket.io USER_CHANGES paths — a remote client can't impersonate the system author for inserts (their session author check fires first). Test: - Pad.ts spec adds 'spliceText with empty authorId attributes to the system author' — verifies pad text lands AND the pool contains the system-author binding. Existing tests that pass an authorId are unaffected. * harden: reject USER_CHANGES that would strand the trailing newline Etherpad's pad text always ends with '\n'. _handleUserChanges previously appended a separate `nlChangeset` correction revision whenever the applied USER_CHANGES left the pad without a trailing '\n'. The stored pad ended up well-formed, but the FIRST NEW_CHANGES broadcast (the malformed user revision itself) reached browsers BEFORE the correction did, and applyToAttribution's MergingOpAssembler aborts with "line assembler not finished" on a non-'\n'-terminated doc — the watching browser session then dropped the changeset and any subsequent edits silently no-op'd until the user reloaded. Replace the silent auto-correction with an explicit reject. Compute `applyToText(rebasedChangeset, prevText)` before appendRevision; if the result doesn't end with '\n', throw -> badChangeset disconnect. Clients must emit USER_CHANGES whose application preserves the invariant — this matches what the JS web client already does and forces non-JS clients (etherpad-pad, third-party integrations) to surface their bugs in their own logs instead of stranding the trailing newline in pad revision history. Also fixes a latent retransmission-detection bug surfaced by this PR's author-attrib changes: moveOpsToNewPool renumbers `*N` references to whatever slot the pad pool assigns, which can differ from the wire form's slot. Comparing the raw client wire against the stored revision form (`changeset === c`) then misses legitimate retransmissions and the same edit gets duplicated. Snapshot the post-pool-mapping form (`canonicalCs`) and compare that against `c` instead. Backend test additions: - 'changeset that would strand the trailing \\n is rejected' covers the new rejection path with wire `Z:6>1|1=6*0+1$X` against `hello\n`. - handleMessageSecurity test now captures roSocket's own authorId and uses it in the apool sent through roSocket, because the prior PR commit made `*0` referencing the wrong author a hard reject. All 1130 backend tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump etherpad-cli-client to ^4.0.3 4.0.3 sends author-attributed inserts and preserves the trailing newline, complying with this PR's tightened USER_CHANGES validation. The rate-limit CI workflow drives the test pad via this client, so without the bump the new server-side rejects fire on the very first \`pad.append()\` and the rate-limit disconnect never gets a chance to arrive — testlimits.sh exits 0 instead of 1 and the rate-limit job fails with "ratelimit was not triggered when sending every 99 ms". Refs ether/etherpad-cli-client#131 * harden: reject USER_CHANGES that name the reserved system author The session-author equality check already rejects wire `*N` that names a different real user, but `a.etherpad-system` is server- internal — it's only used when spliceText / setText is called with an empty authorId from HTTP API or plugin paths. A wire op that names it is either a confused client or an attempt to launder edits through a reserved attribution slot. Refuse. Backend test 'insert claiming the reserved system author is rejected' locks in the new behavior with wire `Z:1>5*0+5$hello` plus an apool that maps slot 0 to `a.etherpad-system`. All 1131 backend tests pass. Inline literal `'a.etherpad-system'` rather than importing the constant from `Pad.SYSTEM_AUTHOR_ID` — `require('../db/Pad')` at PadMessageHandler module scope returned a partially-initialized class via the padManager circular path, leaving the static-field access undefined at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2a865a3a4a
|
feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763) (#7771)
* feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763) Old admins on out-of-date Etherpad installations get no feedback when they click Install on a plugin that needs a newer core. live-plugin-manager doesn't honor engines.node, and the admin UI dropped the error payload that adminplugins.ts already emits. This wires up an end-to-end signal: - pluginEngineCheck.ts: pure helper comparing a plugin's engines.node range against process.version, with a stable error code (PLUGIN_REQUIRES_NEWER_ETHERPAD) and a message that avoids leaking the Node-version implementation detail. Unparseable ranges fall through as compatible so the preflight is opportunistic, not a gate. 8 unit tests cover the happy + edge paths. - installer.ts: install() now best-effort fetches the published plugin's engines.node from npmjs.org, runs the preflight, and short-circuits with the typed error before invoking live-plugin-manager. Also wraps the body in try/catch so the error reaches the socket callback (it was silently dropped on every install failure today, including network errors). - HomePage.tsx: surfaces finished:install.error as a toast, using a dedicated i18n key when the code is PLUGIN_REQUIRES_NEWER_ETHERPAD and a generic fallback otherwise. - en.json: two new strings, parameterized by {{plugin}} and {{error}}. The message admins see is intentionally about Etherpad, not Node — upgrading Etherpad pulls the Node requirement along with it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(plugins): don't restart server when every install in the batch failed Qodo PR review on #7771 caught a side effect introduced earlier in this PR. Before this PR, install() never invoked its callback on error, so a failed install left the task counter inflated and onAllTasksFinished() never ran — masking, but not fixing, the bug. Once install() correctly propagates errors to its cb, the counter hits zero on failure too, and onAllTasksFinished() fires hooks.aCallAll('restartServer'). A no-op preflight rejection (EngineIncompatibleError) would then disconnect every connected pad. Fix: extract wrapTaskCb + task state into InstallerTaskQueue and track whether at least one task in the current batch succeeded. Only fire the "all finished" side effect when something actually changed. Failed-only batches do nothing. Seven unit tests cover the matrix: single success, single failure (the regression), mixed batch (still restarts), all-failed batch, batch reset, null cb, two-task drain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(plugins): time-bound the engines preflight registry fetch Qodo PR review on #7771 flagged that fetchPluginEnginesNode awaits fetch() with no timeout. A stalled DNS lookup or hung connection to registry.npmjs.org would block install() forever — the finished:install socket event would never fire and the admin UI would stay spinning with no error to surface. Wrap the fetch in AbortSignal.timeout(5000). On any failure (network, HTTP error, abort) fetchPluginEnginesNode returns undefined, which the preflight then treats as "no engines info → compatible," so a slow registry never blocks an install that would otherwise succeed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
924059257d
|
fix(a11y): name role=toolbar regions, hide linemetricsdiv from AT (#7255) (#7777)
* fix(a11y): name role=toolbar regions, hide linemetricsdiv from AT (#7255) Two regressions called out in the 2026-05-16 follow-up on #7255, after the firefox accessibility inspector flagged them: (1) The "Ether X" announcement between the editor and chat button was the outer ace iframe (titled "Ether") plus a single 'x' text leaf the renderer appends to outerdocbody for line-height measurement (linemetricsdiv in ace.ts). Add aria-hidden=true on creation so AT skips the measurement node entirely. Same approach we used for sidediv in PR #7758. (2) The two formatting/actions <ul role="toolbar"> regions and the history-mode role=toolbar div had no accessible name. Lighthouse + the firefox a11y panel both flagged this. Putting data-l10n-id directly on the <ul> would either destroy its <li> children (textContent branch) or not populate aria-label (the html10n auto-aria-label code path skips non-form-control elements), and a hidden <span> child inside the <ul> would be invalid HTML. Solution: three visually-hidden <span> labels sitting just before #editbar, each carrying data-l10n-id for translation, referenced from the toolbars via aria-labelledby. Apply the same treatment to .show-more-icon-btn, whose aria-label was previously hardcoded English (no data-l10n-id, so untranslated). Adds Playwright assertions for linemetricsdiv aria-hidden and the resolved accessible-name text of each toolbar. Updates the existing show-more test to expect aria-labelledby (it previously asserted hardcoded English aria-label). Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): reuse translated history-controls label key (Qodo PR review) Qodo flagged: adding `aria-labelledby="editbar-history-label"` on #history-controls overrode the `aria-label` that pad_mode.ts sets from `pad.historyMode.controlsLabel`. That key is already translated in multiple locales (en/de/nl/...); the new `pad.editor.toolbar.history` key was English-only, so non-English users would regress from a localized history-toolbar name to the English fallback. Point the hidden label span at the existing translated key instead of minting a new one, drop the new key from en.json, and update the Playwright expectation to match the translated string ("Pad history controls"). The aria-label that pad_mode.ts still writes to #history-controls is now redundant (aria-labelledby wins) but harmless, and leaving it preserves the runtime relocalization path. Refs ether/etherpad#7777 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): mark toolbar li/a wrappers presentational (Lighthouse, #7255) Lighthouse's axe-core `listitem` rule fires on every toolbar button because role="toolbar" on the <ul> overrides its implicit role="list", leaving the <li> children "orphaned" by axe's heuristic. Murphy's 2026-05-16 follow-up on #7255 attached the Chrome DevTools Lighthouse panel screenshot of this exact failure. Marking the <li>+<a> wrappers role="presentation" tells axe-core they are layout scaffolding for the toolbar role, while the inner <button> keeps its semantics for AT. Same treatment for SelectButton's <li> wrapper. The Separator's <li> also gets aria-hidden=true so AT does not announce an empty list item between toolbar buttons. CSS and JS selectors that still target `.toolbar ul li` continue to work — role="presentation" only affects the accessibility tree, not the DOM tree. No visual or behavioral change for sighted users. Adds a Playwright spec that walks every rendered toolbar <li>/<a> and asserts role="presentation" so future toolbar.ts tweaks can't silently re-introduce the Lighthouse failure. Refs ether/etherpad#7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): label #online_count for AT (#7255 - "number next to the user icon") Murphy's 2026-05-16 follow-up cut off mid-bullet ("It's not clear what the number next to the …"). Best guess: the user-count badge in the showusers toolbar button. Currently it exposes a bare digit to AT — "5" with no context — because the visible badge text is also the entire accessible content. Append a localized aria-label generated from a new pad.userlist.onlineCount key (plural macro for one / other) whenever the count updates, so AT announces "5 connected users" instead of the bare digit. Add role=status and aria-live=polite so the count change is announced inline without forcing the user to refocus the button. Visible badge digit unchanged. html10n.get is null-safe (falls back to an English template so AT never gets back "undefined" before the locale bundle has loaded). Adds a Playwright spec verifying role/aria-live and that the aria-label contains "connected user". Refs ether/etherpad#7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): tighten #online_count + plugin-emitted toolbar <li>s (#7255) Two small follow-ups on top of the toolbar/online-count work: (1) #online_count had no accessible label on solo-author pads. updateNumberOfOnlineUsers — which writes the localized aria-label — only fires on userJoin/userLeave/status change, never on initial single-author load. Call it at the end of init() so the badge ships with its label on first paint. Also bind html10n's 'localized' event so non-English users get the translated label after the locale bundle arrives (matches the keyboard-hint / history-toolbar pattern). Harden the Playwright spec to use polling toHaveAttribute instead of one-shot getAttribute. (2) Sweep role="presentation" onto plugin-emitted toolbar <li>s in pad_editbar.ts init(). Core's toolbar.ts emits its <li>s with the role already, but plugins (ep_headings2, ep_align, ep_font_*, ep_print, ...) ship their own editbarButtons.ejs templates that emit <li> directly, so Lighthouse's listitem rule kept firing on the "with plugins" test runs. Runtime sweep covers anything in the editbar at init time, no plugin coordination needed. Refs ether/etherpad#7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04045fe532
|
Roll Node.js floor back to >= 24 (Active LTS) — closes #7779 (#7781)
* Roll Node.js floor back to >= 24 (Active LTS) Closes #7779. #7779 originally proposed bumping past the Node 25 stop-gap to Node 26. After re-checking the release schedule, the cleaner LTS target is actually Node 24: - Node 24 (Krypton) is currently in Active LTS, supported until ~May 2028. - Node 25 hit end-of-life on April 10 2026 — the floor merged in #7752 / #7749 / #7754 a day ago ships an already-EOL major. - Node 26 was released May 5 2026 and does not enter Active LTS until October 2026. So this PR reverts the Node 25 ratchet from those three PRs and lands on Node 24 — Etherpad's runtime floor stays on a supported LTS for the next ~2 years. Runtime / infra - `package.json` + `src/package.json`: `engines.node` `>=25.0.0` -> `>=24.0.0` - `bin/functions.sh`, `bin/installer.sh`, `bin/installer.ps1`: `REQUIRED_NODE_MAJOR` 25 -> 24 - `Dockerfile`: `node:25-alpine` -> `node:24-alpine` (both stages). Corepack-via-npm workaround is intentionally kept: it works on Node 24 (which still ships corepack) and on Node 25+ (which doesn't), so the same recipe survives the next LTS bump without churn. Comments reworded accordingly. - `snap/snapcraft.yaml`: pinned `NODE_VERSION` 25.9.0 -> 24.15.0; design notes + corepack comment adjusted - `packaging/nfpm.yaml`: `nodejs (>= 25)` -> `nodejs (>= 24)` in top-level depends + deb/rpm overrides - `packaging/bin/etherpad`: comment matches the new pin - `packaging/README.md`: build prereqs + apt install snippet point at `node_24.x`; the long-stale "engines.node floor is 20" line is fixed while we're here - `.github/workflows/*.yml`: setup-node `node-version` 25 -> 24 across every workflow; backend / frontend-admin / upgrade matrices `[25]` -> `[24]` - `.github/workflows/deb-package.yml`: `NODE_MAJOR=25` + `node_25.x` smoke-test installer -> 24 - `bin/plugins/lib/npmpublish.yml`: 25 -> 24 (template propagates to the ~80 ether/* plugins via update-plugins workflow) Docs - `README.md`: install one-liner + Requirements -> Node.js >= 24 - `doc/npm-trusted-publishing.md`: runner requirement -> Node 24 - `doc/plugins.md` / `doc/plugins.adoc`: plugin metadata example `engines.node` -> `">=24.0.0"` @types/node is left at ^25.8.0 — newer type definitions cover Node 24 runtime fine and avoid an unnecessary lockfile churn. Companion homepage one-liner change to follow on ether/ether.github.com. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plugins): example engines.node = ">=22.0.0", not core's floor Plugin code is overwhelmingly ace-hook glue and rarely uses Node-version- specific APIs, so plugin engines.node should reflect the plugin's own requirements, not track core. Showing core's 24-floor in the example encouraged plugin authors to blindly copy a tighter pin than necessary and locked plugins out of being installable on older Etherpad/Node deployments. Use the most-recent Node LTS that has actually reached EOL (20 -> EOL April 2026) as the example floor, i.e. >=22. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
79f525b0a7
|
fix(a11y): action Qodo review on PR #7758 (#7764)
Three bugs and one test-flake risk that Qodo flagged on
|
||
|
|
986f139a61
|
feat(metrics): 3 Prometheus counters for scaling dive (#7756) (#7762)
* feat(metrics): expose 3 Prometheus counters for the scaling dive Per the spec section 6 of #7756: enables the load-test harness to attribute *where* time goes on the server, not just the gauge headline (CPU / event-loop / memory) the dive doc starts from. New /stats/prometheus rows: - etherpad_pad_users{padId} — gauge, derived from sessioninfos on each scrape. Lets the harness confirm the pad it points at actually has the expected concurrency. - etherpad_changeset_apply_duration_seconds — histogram observed inside handleUserChanges. Separates "apply path is slow" from "fan-out is slow" when latency rises. - etherpad_socket_emits_total{type} — counter at the broadcast emit sites (handleCustomObjectMessage, handleCustomMessage, sendChatMessageToPadClients) and inside the NEW_CHANGES per-socket loop in updatePadClients. Bucketed by message type so the harness can measure the amplification factor of each lever (especially the fan-out batching lever). Metric handles live in a new prom-instruments.ts module rather than in prometheus.ts itself, so PadMessageHandler can import the recording helpers without creating a circular dependency (prometheus.ts already requires PadMessageHandler). Tests: smoke test verifies recordSocketEmit + recordChangesetApply move the underlying counters/histogram. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(metrics): address Qodo review — flag-gate, scope histogram, bound label cardinality Three issues raised on the initial PR: 1. **Feature flag.** Per project compliance rule, new features must be behind a flag and disabled by default. Adds `settings.scalingDiveMetrics` (default `false`). When off, recordSocketEmit() / recordChangesetApply() short-circuit to no-ops and the metrics are never even registered with the Prometheus register. Enable only when running the ether/etherpad-load-test scaling-dive harness. 2. **Histogram scope.** Previously the etherpad_changeset_apply_duration_seconds timer wrapped the whole handleUserChanges() body — including `await exports.updatePadClients(pad)` — so the histogram measured apply+fan-out, defeating its stated purpose. Now stopped immediately after the apply work (`assert.equal(...rev, r)`), before the ACCEPT_COMMIT socket emit and the updatePadClients call. Failed applies deliberately don't observe so the success-path distribution stays clean. 3. **Label cardinality.** handleCustomMessage was passing the user-supplied msgString (an HTTP-API param) directly as the `type` label value. A misbehaving API caller could grow prom-client's internal label map until OOM. Now bucketed against a known-types allowlist; anything outside it lands in `other`. Tests updated: 5/5 — covers happy path, "other" bucketing of unknown/unsafe labels, and that the flag-disabled state is a true no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
21e1ae2fa3
|
security: allow integrator sessionID cookie to be HttpOnly (#7045) (#7755)
* security: allow integrator sessionID cookie to be HttpOnly (#7045) The integrator-set sessionID cookie was forced to be non-HttpOnly because Etherpad's own client JS read it via document.cookie and forwarded it in the socket.io CLIENT_READY payload, exposing it to XSS. Mirror the GDPR PR3 author-token migration: read sessionID from the socket.io handshake's Cookie header in PadMessageHandler.handleClientReady, falling back to the legacy message-level field with a one-time deprecation warning per socket. Drop the client-side Cookies.get('sessionID') reads in pad.ts and timeslider.ts so the field is no longer sent by current clients. Existing integrators that set sessionID without HttpOnly keep working unchanged; the field on the message becomes optional and integrators should now mark the cookie HttpOnly; Secure; SameSite=Lax. Closes #7045 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): treat undecodable handshake cookies as absent (Qodo #7755) decodeURIComponent() throws URIError on malformed values like `%ZZ`. The unguarded call in PadMessageHandler.handleClientReady's readCookie() let a single bad cookie abort CLIENT_READY for that socket, allowing unauthenticated peers to spam server error logs and lock themselves out of pads. Catch URIError and treat the value as absent so the legacy message-level field still serves as a fallback. Other error classes still propagate. Add a backend test that asserts a `sessionID=%ZZ` cookie no longer aborts the handshake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
72917f4457
|
build(deps-dev): bump the dev-dependencies group with 3 updates (#7759)
Bumps the dev-dependencies group with 3 updates: [eslint](https://github.com/eslint/eslint), [vite-plugin-babel](https://github.com/owlsdepartment/vite-plugin-babel) and [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify). Updates `eslint` from 10.3.0 to 10.4.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.4.0) Updates `vite-plugin-babel` from 1.7.0 to 1.7.1 - [Commits](https://github.com/owlsdepartment/vite-plugin-babel/commits) Updates `oxc-minify` from 0.130.0 to 0.131.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.131.0/napi/minify) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: vite-plugin-babel dependency-version: 1.7.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: oxc-minify dependency-version: 0.131.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7a1d9519f6
|
fix(a11y): skip-to-content link + hide line numbers from screen readers (#7255) (#7758)
* fix(a11y): skip-to-content link + hide line numbers from AT (#7255) PR #7451 landed editor-region labelling but didn't address two remaining screen-reader complaints from the original report: 1. No way to bypass the toolbar. Murphy noted that screen-reader users had to swipe through ~18 toolbar buttons to reach the editor (WCAG 2.4.1 Bypass Blocks). Adds a skip link as the first child of <body>, hidden offscreen until keyboard focus reveals it. Click handler in pad.ts routes through ace_focus so the inner contenteditable actually receives focus — a plain href="#editorcontainer" anchor only scrolls. 2. Line numbers read individually as "1, 2, 3, ...". The sidediv is visual scaffolding for sighted users; it carries no useful information for AT. Adds aria-hidden="true" on creation in ace.ts so screen readers skip it entirely. Also fixes two issues found while implementing this: - The Escape/Alt+F9 hint that PR #7451 added in the inner iframe's body was being wiped by Ace2Inner.init() (line splices manage body children). Move it to the inner <head> — aria-describedby resolves by ID anywhere in the same document. Localize the text via html10n.get('pad.editor.keyboardHint'). - Skip link and keyboard hint text are both routed through new locale keys (pad.editor.skipToContent, pad.editor.keyboardHint) so they translate via the existing html10n pipeline. Adds two Playwright assertions for the new behavior. Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): remove editor auto-focus so skip link is Tab-reachable PR #7758 added a skip-to-content link as the first body child, but on local testing it was unreachable via Tab from the URL bar — the standard WCAG 2.4.1 entry path. Root cause: postAceInit calls padeditor.ace.focus() on load, which moves focus into the editor iframe. Tab inside the editor inserts an indent character (handled by ace2_inner's key handler) rather than bubbling to the parent page, so the skip link sat in the DOM but no user could ever reach it. Drop the load-time auto-focus. Initial focus is now on <body>, and the first Tab focuses #skip-to-content as expected. Users now click or Tab into the editor; existing Escape → toolbar exit (PR #7451) and the new Enter-on-skip-link path both still work. Cost: the long-standing "start typing immediately on a fresh pad" UX goes away — one extra click to start editing. This matches the modern accessible default for rich text editors (Slack, Discord, Google Docs all require an explicit focus before accepting input) and removes a keyboard trap that violated WCAG 2.1.2 even after PR #7451's Escape escape hatch. Adds a Playwright assertion that fresh-page Tab focuses the skip link. Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: focus the editor before typing in three specs These three Playwright tests started failing after the autofocus removal in |
||
|
|
6895eb80c8
|
build(deps): bump js-cookie from 3.0.5 to 3.0.6 (#7760)
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.5 to 3.0.6. - [Release notes](https://github.com/js-cookie/js-cookie/releases) - [Commits](https://github.com/js-cookie/js-cookie/commits) --- updated-dependencies: - dependency-name: js-cookie dependency-version: 3.0.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e6eed050f0
|
build(deps): bump undici from 8.2.0 to 8.3.0 (#7761)
Bumps [undici](https://github.com/nodejs/undici) from 8.2.0 to 8.3.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.2.0...v8.3.0) --- updated-dependencies: - dependency-name: undici dependency-version: 8.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
83a1bcccd8
|
build(deps): bump mssql from 12.5.2 to 12.5.3 (#7743)
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.2 to 12.5.3. - [Release notes](https://github.com/tediousjs/node-mssql/releases) - [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt) - [Commits](https://github.com/tediousjs/node-mssql/compare/v12.5.2...v12.5.3) --- updated-dependencies: - dependency-name: mssql dependency-version: 12.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7e754ec8d6
|
build(deps): bump tsx from 4.21.0 to 4.22.0 (#7745)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.21.0 to 4.22.0. - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.21.0...v4.22.0) --- updated-dependencies: - dependency-name: tsx dependency-version: 4.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
436f9825c3
|
build(deps-dev): bump the dev-dependencies group across 1 directory with 8 updates (#7751)
Bumps the dev-dependencies group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.7.0` | `25.8.0` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.1` | `6.0.2` | | [i18next](https://github.com/i18next/i18next) | `26.1.0` | `26.2.0` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.14.0` | `1.16.0` | | [react-i18next](https://github.com/i18next/react-i18next) | `17.0.7` | `17.0.8` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.15.0` | `7.15.1` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.12` | `8.0.13` | | [vite-plugin-babel](https://github.com/owlsdepartment/vite-plugin-babel) | `1.6.0` | `1.7.0` | Updates `@types/node` from 25.7.0 to 25.8.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@vitejs/plugin-react` from 6.0.1 to 6.0.2 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.2/packages/plugin-react) Updates `i18next` from 26.1.0 to 26.2.0 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.1.0...v26.2.0) Updates `lucide-react` from 1.14.0 to 1.16.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.16.0/packages/lucide-react) Updates `react-i18next` from 17.0.7 to 17.0.8 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v17.0.7...v17.0.8) Updates `react-router-dom` from 7.15.0 to 7.15.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.15.1/packages/react-router-dom) Updates `vite` from 8.0.12 to 8.0.13 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.13/packages/vite) Updates `vite-plugin-babel` from 1.6.0 to 1.7.0 - [Commits](https://github.com/owlsdepartment/vite-plugin-babel/commits) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.8.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: lucide-react dependency-version: 1.16.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-i18next dependency-version: 17.0.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-router-dom dependency-version: 7.15.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite dependency-version: 8.0.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite-plugin-babel dependency-version: 1.7.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
33b616b925
|
chore: align Node version pins with the Node 25+ floor (#7752)
The workspace root declares `engines.node: >=25.0.0` and
`engineStrict: true`, so anyone running Node 22 or 24 is hard-blocked
by pnpm at install time. Three places still referenced the old
floor and were testing / supporting a configuration no real user can
reach:
1. `.github/workflows/backend-tests.yml` Windows matrices (both
`withoutpluginsWindows` and `withpluginsWindows`) still had
`node: [22, 24, 25]`. The Linux jobs were already collapsed to
`[25]`; this collapses the Windows side to match. Drops 4
Windows CI cells per develop push (Node 22 × 2 + Node 24 × 2)
that were exercising an unsupported runtime.
2. `src/package.json` engines.node was `>=22.13.0`. Bumped to
`>=25.0.0` to match the workspace root. `pnpm` floor bumped
from `>=11.0.0` to `>=11.1.2` so the inner package agrees with
the root packageManager pin.
3. `.github/workflows/deb-package.yml` setup-node was pinned to
`node-version: '24'`. Every other setup-node call in the
workflows folder is already on 25; this brings the deb job in
line.
Side benefit: the Windows + Node 24 flake addressed in #7748 is now
moot for develop CI — Node 24 isn't tested at all. The `--exit`
mitigation and node-diagnostic-report capture remain in place on
Windows Node 25 as defence in depth in case the same native-crash
class shows up on a different Node line.
Verified locally:
- cd src && pnpm exec vitest run tests/backend-new/specs/backend-tests-flake-mitigation.test.ts
→ 3 passed (3). The mitigation test counts step blocks (4) and
Windows --exit invocations (2), not matrix dimensions, so the
contract is unchanged.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ab60ed33c1
|
admin: parsed JSONC settings editor (takes over #7666, closes #7603) (#7709)
* admin: parsed JSONC settings editor with form view (#7603, #7666) Takes over #7666 / closes #7603. Squashed rebase of 32 commits onto current develop (which has since absorbed admin design rework #7716 and admin i18n fixes #7736 — granular history preserved on takeover/7666-admin-settings-editor before this squash, see PR description for the original commit log). Highlights: - New parsed JSONC settings editor under admin/src/components/settings/ — FormView, ModeToggle, ParseErrorBanner, JsoncNode dispatcher, leaf widgets (string, number, bool, null, env pill), and pure helpers (comments, envPill, jsoncEdit, labels, templateComments). - ${VAR:default} env placeholders render as editable inline inputs that round-trip through the raw textarea (env-pill spec asserts this; docker-template spec protects against form-view degradation on env-heavy configs). - Schema-driven help text sourced from settings.json.template, inlined at build time via vite (drops the runtime fs.allow widening that earlier iterations needed). - ModeToggle switches between FormView and raw textarea on /admin/settings; parse errors surface in a non-blocking banner. - jsonc-parser dep added; pure helpers wrap modify() for stable edits that preserve key order and trailing comments (stops at end-of-line so trailing-comment trains don't bleed into the next property). - i18n keys added for form mode, parse error, env pill, default_label, and input aria. - Playwright specs cover form view, env pill, parse error banner, raw round-trip, and form-mode regressions called out in #7666 review (stable React keys from AST offsets, save-toast on server ack only, NumberInput draft sync, parse-error flash during initial load, .settings CSS conflict resolution, focus retention via rAF, IconButton type defaulting to 'button'). Co-authored-by: Ayushi Gupta <ayushigupta36881@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): stabilise React keys to prevent focus loss in settings editor Switch React keys in JsoncNode and FormView from byte offsets to stable JSON paths (`getNodePath(...).join('.')`). Byte offsets shift on every keystroke because the edit changes the surrounding character count, which forces React to remount inputs and lose focus mid-typing. - Object children key on the property path. - Array elements key on their JSON path index. - Add a Playwright regression test pinning focus stability for array element edits. Co-authored-by: John McLear <john@mclear.co.uk> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): stop trailing /* */ comments from bleeding into next key's label (#7740) In the parsed settings form view, each key's row was rendering its label as the previous keys' source lines concatenated together. Root cause: findLeading() in admin/src/components/settings/comments.ts treated any line ending in `*/` as a comment continuation, so a JSON line like "altF9": true, /* focus on the File Menu and/or editbar */ was absorbed into the next sibling's leading comment block, and then each subsequent key picked up an even longer accumulation. - Tighten findLeading's isComment check to only match structural comment lines (`//`, `/*`, or a `*`-prefixed continuation/close), so JSON code with a trailing block comment no longer matches. - Surface leading and trailing comments separately from the template map. Leaf rows with only a trailing same-line comment now render the humanized key as the row label and the comment as the help text below the control, matching settings.json.template's convention (and #7740's recommendation that "helper text should be below"). - Add unit tests pinning the regression and the JSDoc/`//` leading styles, plus a Playwright spec that asserts altC's row carries a clean label and the "focus on the Chat window" help text. Closes #7740. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ayushi Gupta <ayushigupta36881@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8dbd917718
|
test(ci): kill Windows + Node 24 backend-test flake; capture native crashes (#7748)
* test(ci): kill Windows + Node 24 backend-test flake; capture native crashes The Backend tests suite has a ~22% silent-failure rate on Windows + Node 24 specifically (Linux 22/24/25 ✓, Windows 22/25 ✓). Two prior PRs instrumented the failure — common.ts handlers (#7663), then an unconditional diagnostics.ts (#7665) — and confirmed it's a hard kill: diagnostics.ts:23-27 documents the matrix, and every recurrence (run 25279692065, 25754938013, 25906496503) shows only `[diag +0ms] diagnostics loaded`, no beforeExit / exit / unhandledRejection / uncaughtException / signal handlers. Process dies 700–900 ms after the last passing test, in a varying spec each time. That's a native crash in V8 / libuv / the tsx loader, not anything reachable from JS. This PR ships two independent attacks at the failure: 1. Mitigation — add --exit to the mocha command in src/package.json. Mocha's default (--exit=false) waits for the event loop to drain after tests complete. The hard kill happens during that drain or the inter-spec transition. With --exit, mocha calls process.exit(failures) directly once the run finishes, closing the cleanup-race window. Linux/Windows-22/25 are green today, so the natural-drain path is not surfacing real leaks worth preserving. Verified locally: `cd src && pnpm test` -> 1121 passing, 0 failing, 23s. 2. Capture — set NODE_OPTIONS in each Backend tests step to --report-on-fatalerror, --report-uncaught-exception, --report-on-signal, --report-compact, plus --report-directory=${{ github.workspace }}/node-report. If Node crashes at the C++ level (segfault, V8 abort, libuv panic) the runtime writes a JSON diagnostic report with the V8 stack, libuv handle table, JS heap state, and OS info. A new "Upload Node diagnostic reports on failure" step (actions/upload-artifact@v7, `if: failure()`, `if-no-files-found: ignore`) uploads that directory as an artifact per matrix cell — the data we have been unable to capture from JS instrumentation alone. If (1) eliminates the flake on the next push to develop, great. If not, (2) finally gives us the crash dump and we can fix the root cause. Touches all four Backend tests jobs (Linux × 2, Windows × 2). The Windows steps now also set `shell: bash` so the same `mkdir -p ...` line works under git-bash; the existing `working-directory: src` is preserved. NODE_OPTIONS is scoped to the test step only, so the existing vitest step is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ci): address Qodo review on PR #7748 Three findings from Qodo's review of the Windows + Node 24 flake fix: 1. (bug) "mocha --exit masks handle leaks". --exit removes the post-suite event-loop drain that surfaces leaked timers / sockets — exactly the class of regression that lowerCasePadIds.ts notes is otherwise visible. Linux/local runs are currently green on natural drain, so dropping that signal everywhere would silence real leaks to fix a Windows-only flake. Fix: scope --exit to Windows only. - Remove --exit from src/package.json's "test" script (shared with local dev + Linux CI; both keep natural-drain behaviour). - Append `pnpm test -- --exit` in just the two Windows backend-test steps so the mitigation only runs where the flake actually lives. 2. (observability) "diagnostics exit-matrix misleading". With --exit on Windows, "only exit fires" becomes the EXPECTED pattern there, not a sign of unexpected process.exit(). Update the matrix comment in tests/backend/diagnostics.ts to spell out: clean drain on Linux/local → beforeExit + exit; Windows under --exit → only exit; "only exit" elsewhere still implies an unexpected process.exit somewhere. 3. (rule violation) "no regression test for the mitigation". Repo convention (see admin-i18n-source-lint.test.ts) is to pin policy-bearing config with a source-lint spec so a future refactor can't silently revert it. Add src/tests/backend-new/specs/backend-tests-flake-mitigation.test.ts: - Asserts every "Run the backend tests" step sets NODE_OPTIONS with the report-on-fatalerror diag flags AND is followed by an Upload Node diagnostic reports step (4 of each in the current matrix). - Asserts exactly 2 Windows jobs invoke `pnpm test -- --exit`. - Asserts the shared mocha "test" script in src/package.json does NOT bake in --exit globally. Verified locally: - cd src && pnpm exec vitest run tests/backend-new/specs/backend-tests-flake-mitigation.test.ts → 3 passed (3). - Stream.ts spec without --exit → 31 passing, diag prints "beforeExit code=0" + "exit code=0" (clean drain restored). - Existing admin-i18n-source-lint suite still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
02e00ad615
|
fix(admin): restore SearchField + sorting modules used by AuthorPage (#7746)
* fix(admin): restore SearchField + sorting modules used by AuthorPage PR #7736 ("replace hardcoded German strings with i18n keys") deleted admin/src/components/SearchField.tsx and admin/src/utils/sorting.ts as "orphan modules (no longer imported anywhere after #7716)". At that point, the GDPR admin AuthorPage (PR #7667) had not yet landed on develop. When #7667 merged afterwards, the new admin/src/pages/AuthorPage.tsx brought back imports of SearchField and determineSorting — but the files were already gone, leaving develop with broken admin imports. This breaks `pnpm --filter admin run build-copy` on every CI job that builds the admin UI: src/pages/AuthorPage.tsx(6,27): error TS2307: Cannot find module '../components/SearchField.tsx' or its corresponding type declarations. src/pages/AuthorPage.tsx(9,32): error TS2307: Cannot find module '../utils/sorting.ts' or its corresponding type declarations. src/pages/AuthorPage.tsx(207,29): error TS7006: Parameter 'v' implicitly has an 'any' type. Backend tests, Frontend admin tests, Docker (build-test, build-test-local-plugin, build-test-db-drivers), rate limit, and Upgrade from latest release all fail at the admin build step on develop. Restore both files verbatim from before the deletion (commit ff7c4d531^). With them back in place AuthorPage.tsx's existing imports resolve, the implicit-any on the SearchField onChange callback goes away (typed via SearchFieldProps), and `pnpm --filter admin run build-copy` succeeds. This is the minimal, surgical fix; whether SearchField / determineSorting should ultimately be inlined into AuthorPage is a separate cleanup. Test plan: - cd admin && pnpm exec tsc --noEmit → no errors - cd admin && pnpm exec tsc && pnpm exec vite build → built in 545ms Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(admin-i18n-lint): invert orphan-modules assertion now that AuthorPage imports them PR #7736 added a lint assertion that admin/src/components/SearchField.tsx and admin/src/utils/sorting.ts must NOT exist, on the assumption they were dead code after #7716. They aren't — the GDPR AuthorPage (#7667) imports both. The previous commit restored the files; this commit flips the assertion so the test: - verifies both files exist - verifies admin/src/pages/AuthorPage.tsx still imports them If a future cleanup wants to delete these modules, the test now forces the author to also delete or refactor the AuthorPage consumption first, preventing a repeat of the merge-order accident that produced this bug. Test plan: - cd src && pnpm exec vitest run tests/backend-new/specs/admin-i18n-source-lint.test.ts → 14 passed (14) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e358410b68
|
Localisation updates from https://translatewiki.net. | ||
|
|
6075708818
|
feat(gdpr): admin UI for author erasure (follow-up to #7550) (#7667)
* docs(gdpr): admin UI for author erasure — design spec Follow-up to PR5 (#7550): adds an in-product /admin/authors page so operators can search by name or external mapper, preview the impact of an Art. 17 erasure (token mappings, mapper bindings, chat messages, affected pads), and commit it without crafting a curl. Backend uses three new admin-socket events on settings_admin (not REST), so the existing public REST endpoint and its gdprAuthorErasure.enabled flag keep their current single meaning. The page stays discoverable when the flag is off — banner + disabled buttons explain how to enable it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(gdpr): admin UI for author erasure — implementation plan Step-by-step TDD plan for the /admin/authors follow-up to PR5 (#7550). Nine tasks covering: lastSeen field on globalAuthor writes, anonymizeAuthor({dryRun}), authorManager.searchAuthors helper, three new admin-socket events (authorLoad / anonymizeAuthorPreview / anonymizeAuthor) + settings-flag delivery, frontend types/swatch/ i18n, store/route/sidebar wiring, AuthorPage.tsx with two-step modal and disabled-flag banner, Playwright coverage, and the PR/ Qodo workflow. References the spec at docs/superpowers/specs/2026-05-03-gdpr-admin-author-erasure-ui-design.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(authors): stamp lastSeen on globalAuthor writes Adds a lastSeen timestamp to the globalAuthor record on createAuthor, setAuthorName, and setAuthorColorId. Read paths are not modified to keep the write cost zero per page load. Pre-existing records gain the field on their next identity write — no migration sweep, callers that read the field tolerate undefined. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(authors): anonymizeAuthor({dryRun}) for preview Adds an opt-in dryRun option that walks the same token/mapper/chat loops and returns identical counter shape without touching the database. The public REST endpoint is unchanged (it never passes the flag), so production behaviour is identical. Used by the upcoming admin-UI two-step erase modal to show 'will clear: N mappings, K chat messages' before the irreversible commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(authors): restore lost rationale on anonymizeAuthor + document dryRun Code review on the previous commit caught that the dryRun refactor silently dropped four WHY-comments (lazy-require cycle, drop-mappings- first ordering, zero-identity-without-sentinel split, sentinel-last discipline) and left the new opts parameter undocumented. Restored the comments verbatim and added a one-line JSDoc note for dryRun. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(authors): authorManager.searchAuthors helper In-memory enumeration of globalAuthor:* with a join on mapper2author:* for the mapper column. Filter (substring on name OR mapper OR authorID — the authorID match lets admins verify a specific erased record where name and mapper bindings are gone), sort (name | lastSeen), paginate, cap the pre-pagination set at 1000 to prevent runaway scans. Powers the upcoming /admin/authors page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(authors): tie-break searchAuthors sort on authorID Code review pointed out that ties in the primary sort key (common on lastSeen for authors created the same ms, possible on identical names too) fell back to findKeys enumeration order — not guaranteed stable across DB backends. Adding an authorID secondary sort makes pagination safe across requests. Also fix a misleading 'default' note in the JSDoc — includeErased is required, not optional. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(authors): admin-socket events for author erasure UI Adds three handlers on the /settings admin namespace: - authorLoad: paginated search via authorManager.searchAuthors - anonymizeAuthorPreview: dry-run counters, always available to authenticated admins (read-only) - anonymizeAuthor: live commit, gated on gdprAuthorErasure.enabled (returns {error: 'disabled'} when off) Extends the load reply with a flags.gdprAuthorErasure boolean so the client knows whether to render the disabled-flag banner without an extra round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(authors): clean up admin-socket test mutations Code review found two test-isolation issues: - settings.users 'restoration' was a no-op (savedUsers held a reference to the same object that the test mutated). The test-admin key now gets explicitly removed in after(). - The Promise that awaited connect/connect_error left the loser listener attached for the lifetime of the socket, risking an unhandled rejection on a later spurious connect_error event. Listeners are now paired with off() so only the winner survives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): types, ColorSwatch, and en.json for authors page Standalone primitives for the upcoming /admin/authors page: - AuthorSearch.ts: query/result/preview wire types matching the new admin-socket events - ColorSwatch.tsx: resolves a globalAuthor.colorId (palette index or raw hex) to a small inline-styled swatch - ep_admin_authors/en.json: every user-visible string the page needs, loaded by the existing namespace-as-static-asset i18n strategy Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): /admin/authors page Adds a searchable, sortable, paginated authors page mirroring the existing PadPage shape. Search matches name OR mapper substring; 'Show erased' toggle off by default; cap-at-1000 hint surfaces when the backend caps the pre-pagination set. Two-step erase modal: dry- run preview shows what will be cleared, then a Continue button commits the irreversible erasure. Disabled-flag banner explains how to enable when gdprAuthorErasure.enabled is false; per-row Erase button is disabled with a tooltip in the same condition. Sidebar gets a Users link between Pads and Communication. App.tsx listens for the new flags.gdprAuthorErasure on the connect-time settings push so the page knows the flag state without an extra round trip. ep_admin_authors namespace is added to i18next's ns list so all translation keys resolve. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(authors): i18n the pagination controls Spec review caught three hardcoded English strings in the /admin/authors pagination footer ('Previous Page', 'Next Page', 'X out of Y'). Carried over from PadPage.tsx via the plan template, which had the same gap. Added three new keys to ep_admin_authors and routed the spans through Trans/t(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(authors): banner CSS, IconButton attribute drop, erase phase string Code review on the AuthorPage commit caught three issues: - Disabled-flag banner used dialog-confirm-content classname which is position: fixed + centered + z-index: 101, making it render as a modal-style overlay over the table. Drop the className and define the banner with inline styles only; add role='alert' for SR users. - The Erase IconButton spread {data-disabled-reason: …} alongside {disabled: true}, but IconButton only forwards a small allowlist of props — the data attribute was silently dropped. Replaced with a conditional title that flips to the disabled-reason string when the button is disabled (which IconButton does forward). - 'Erasing…' string was rendered during loading-preview, but the string literally describes the commit phase. Added a new loading-preview key for the preview-loading state, and surface the existing 'erasing' string under the buttons during the committing phase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(admin): Playwright /admin/authors + fix i18n key shape The earlier en.json shipped namespace-prefixed JSON keys ('ep_admin_authors:title': 'Authors') which is the wrong shape: i18next splits the lookup on ':' to extract the namespace, then looks up the bare key in the loaded namespace data. The existing convention (admin/public/ep_admin_pads/en.json) uses flat keys without the namespace prefix; matching it makes every <Trans i18nKey='ep_admin_authors:foo'/> resolve to the intended translated string. Strings render as English fallback without this fix; only the page-title test passes (and only by substring accident). Also adds the Playwright coverage required by Task 8: localized title, empty-state message on a fresh search tag, disabled banner toggling with gdprAuthorErasure.enabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): JSONC-tolerant settings parse + sidebar count = 7 CI on PR #7667 surfaced two test failures caused by my changes: 1. setErasureFlag() in admin_authors_page.spec.ts used JSON.parse on the raw settings.json textarea content. The CI environment loads settings.json.template which has unquoted property names, trailing commas, and block + line comments — JSON.parse rejects all three. Switched to `new Function('return (' + raw + ')')` which evaluates the textarea as a JS object literal, accepting every shape Etherpad's own settings loader handles. 2. admintroubleshooting.spec.ts hardcoded `menu.locator('li').toHaveCount(6)`. The new /authors sidebar entry made it 7. Updated the assertion and the sidebar comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(authors): IconButton title, Dialog.Title, preview errors, settings restart Final whole-branch review found three Important UX/a11y defects, plus CI flagged one runtime defect: - IconButton renders the title prop as a visible <span>, not as the HTML title attribute. Disabled rows were displaying the 80-character 'Author erasure is disabled...' string next to every trash icon. Reverted to the short 'Erase' label; the page-level banner already explains the disabled state. - Radix Dialog.Content was missing Dialog.Title. Wrapped the existing <h3> in <Dialog.Title asChild> so screen readers can announce the dialog purpose. - onPreview proceeded to render the preview UI even when the backend reply carried {error}, leaving 'Will clear undefined token mappings...' on screen. Now mirrors onErase. - The disabled-banner-hidden Playwright test failed because settings.json save does not hot-reload. setErasureFlag now restartEtherpad's after saveSettings and re-logins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(authors): action Qodo review — lastSeen, flag-gating, defensive payloads Qodo on PR #7667 surfaced three issues: 1. (Bug, Correctness) lastSeen lost or stale. - mapAuthorWithDBKey only updated `timestamp` for returning authors so the admin /authors 'Last seen' column drifted on every reconnect without an identity write. Now stamps both timestamp and lastSeen. - anonymizeAuthor's two db.set calls overwrote globalAuthor without preserving lastSeen, blanking the column for erased rows. Both writes now carry forward `existing.lastSeen ?? existing.timestamp`. - searchAuthors falls back to rec.timestamp when rec.lastSeen is missing so legacy records aren't blank. 2. (Rule violation, Security) /authors route not flag-gated. The new admin-socket read paths (authorLoad, anonymizeAuthorPreview) were always-on; only the destructive anonymizeAuthor was gated. Project rule (Compliance ID 6) requires new features behind a flag, disabled by default. All three handlers now check gdprAuthorErasure.enabled and return {error:'disabled'} when off. The sidebar 'Authors' link is hidden when the flag is off (deep-link to /admin/authors still works and renders the existing disabled banner so docs can point to it). 3. (Bug, Reliability) Socket destructure throws on missing payload. Handlers signed `async ({authorID}: {authorID: string}) => …` threw before try/catch when a client emitted with no payload, producing an unhandled rejection. Switched to `async (payload: any) => { const authorID = payload?.authorID; … }`. Test impact: anonymizeAuthorSocket gains two regressions (authorLoad disabled-shape, payload-less emits don't crash) and updates the preview-when-flag-off test to assert {error:'disabled'} per the new gating posture (was 'preview still works'). admintroubleshooting sidebar-count reverts 7 → 6 since the Authors link is now conditional on the flag (off by default in the test environment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d1ed89dfe2
|
build(deps-dev): bump the dev-dependencies group across 1 directory with 9 updates (#7730)
Bumps the dev-dependencies group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@playwright/test](https://github.com/microsoft/playwright) | `1.59.1` | `1.60.0` | | [@types/jsdom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsdom) | `28.0.1` | `28.0.3` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.6.2` | `25.7.0` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.5` | `4.1.6` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.59.2` | `8.59.3` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.59.2` | `8.59.3` | | [i18next](https://github.com/i18next/i18next) | `26.0.10` | `26.1.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.11` | `8.0.12` | | [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.129.0` | `0.130.0` | Updates `@playwright/test` from 1.59.1 to 1.60.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.59.1...v1.60.0) Updates `@types/jsdom` from 28.0.1 to 28.0.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsdom) Updates `@types/node` from 25.6.2 to 25.7.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `vitest` from 4.1.5 to 4.1.6 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.6/packages/vitest) Updates `@typescript-eslint/eslint-plugin` from 8.59.2 to 8.59.3 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.3/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.59.2 to 8.59.3 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.3/packages/parser) Updates `i18next` from 26.0.10 to 26.1.0 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.0.10...v26.1.0) Updates `vite` from 8.0.11 to 8.0.12 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.12/packages/vite) Updates `oxc-minify` from 0.129.0 to 0.130.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.130.0/napi/minify) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.60.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@types/jsdom" dependency-version: 28.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@types/node" dependency-version: 25.7.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.59.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.59.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: oxc-minify dependency-version: 0.130.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: vite dependency-version: 8.0.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vitest dependency-version: 4.1.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
cbe551b432
|
feat(updater): tier 3 — auto update with grace window (#7607) (#7720)
* feat(updater): scheduled execution state + graceStartTag dedupe field (#7607) Preparation for Tier 3 of the auto-update subsystem: - ExecutionStatus gains `scheduled` (targetTag, scheduledFor, startedAt). - EmailSendLog gains `graceStartTag` for one-shot grace-start email dedupe. - state validator accepts the new shape, requires per-status fields, and backfills graceStartTag=null on a Tier 1/2 state file. Plus the implementation plan at docs/superpowers/plans/2026-05-11-auto-update-pr3-tier3-auto.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): decideSchedule pure decision function (#7607) Adds src/node/updater/Scheduler.ts with the Tier 3 pure decision logic: - schedules when canAuto + idle/verified/terminal-cleared - reschedules when a newer tag appears mid-grace - emits a grace-start email (once per tag) when adminEmail is set - cancels a stale schedule when policy flips canAuto off - no-ops during in-flight / terminal states - clamps preApplyGraceMinutes to [0, 7 days] Also extends Notifier's EmailKind union with 'grace-start' so the decision result types correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): scheduler timer runner with arm/cancel (#7607) Adds createSchedulerRunner to Scheduler.ts: - arm(): clears any prior timer, sets a fresh one for scheduledFor - cancel(): clears the pending timer, idempotent - past scheduledFor → fires with delay=0 (rehydrate after restart-in-grace) - single-fire-per-arm semantics; armedFor cleared on fire Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(updater): extract apply pipeline shared by HTTP + scheduler (#7607) Lifts the preflight → drain → execute orchestration out of the /admin/update/apply HTTP handler into src/node/updater/applyPipeline.ts. The HTTP handler keeps its 4xx status mapping; the pipeline owns the state transitions, lock release, drain coordination, and rollback hand- off. The new ApplyPipelineDeps interface accepts an onAccepted callback so the HTTP path can still 202 mid-flow while the Tier 3 scheduler path (next commit) can no-op. Adds `scheduled` to the apply allowed-entry list so an admin can "Apply now" during the Tier 3 grace window. 13 vitest cases cover happy / preflight-failed / cancelled / busy / lock-held / scheduled-entry / rollback / lock-release. Existing 12 mocha integration tests still pass without change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): wire Tier 3 scheduler into boot + performCheck (#7607) - expressCreateServer instantiates the scheduler runner and rehydrates the timer when a prior boot left state.execution = scheduled - performCheck evaluates decideSchedule after the notifier pass: schedule transitions state + sends grace-start email + arms timer; cancel-schedule resets to idle + cancels timer - shutdown cancels the timer - exposes cancelScheduler() so the cancel endpoint (next commit) can drop the pending schedule - buildSchedulerApplyDeps() supplies the full production-wired pipeline deps (preflight, executor, rollback) for the scheduler-triggered apply Adds tests/backend/specs/updater-scheduler-integration.ts covering boot-rehydrate fire-on-past and the decision-to-state round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): cancel handler supports Tier 3 scheduled state (#7607) POST /admin/update/cancel now accepts execution.status === 'scheduled' in addition to preflight/draining. The handler calls cancelScheduler() to drop the pending in-process timer, then transitions state to idle with lastResult.outcome = 'cancelled' (mirroring the existing pattern). Adds a Tier 3 integration test that seeds a scheduled state, calls /admin/update/cancel, and asserts the state machine landed correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): countdown + cancel UI for Tier 3 scheduled updates (#7607) - store.ts: extend Execution union with the scheduled variant - UpdatePage.tsx: render countdown panel during scheduled; Apply button is relabelled "Apply now" so the admin can skip the remaining grace; Cancel button accepts scheduled state - UpdateBanner.tsx: dedicated scheduled banner with live remaining time - en.json: new i18n keys (execution.scheduled, banner.scheduled, page.scheduled.{title,countdown,apply_now}) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(updater): playwright spec for Tier 3 scheduled UI (#7607) Three cases against a mocked /admin/update/status: - countdown panel + Apply now + Cancel render when execution is scheduled - Cancel button posts /admin/update/cancel and triggers re-fetch - /admin (banner) shows "Auto-update to <tag> scheduled" copy Mirrors the existing update-page-actions.spec.ts mock pattern (page.route). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): document Tier 3 auto with grace window (#7607) - doc/admin/updates.md: flip Tier 3 from "designed, not yet implemented" to current; expand preApplyGraceMinutes table row; add a Tier 3 section explaining schedule / cancel / Apply now / restart-in-grace and the grace-start email - settings.json.template: clarify the preApplyGraceMinutes comment - CHANGELOG.md: Unreleased entry for Tier 3 - runbook §11: full Tier 3 smoke (happy, cancel, apply-now, restart-in- grace, email) plus the additional sign-off checkboxes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): UpdatePage handles missing execution field; scope spec locator (#7607) Two CI fixes for PR #7720: 1. UpdatePage.tsx — optional-chain us.execution.status. Integration test stubs (update-banner.spec.ts) ship payloads without the Tier 2/3 execution / lastResult / lockHeld fields; without optional chaining on the new scheduled-derivation line the whole page crashed before the h1 rendered, breaking the unrelated "renders current version" test. 2. update-scheduled.spec.ts — scope the v2.7.2 assertion to the .update-scheduled section. The regex was matching three elements (banner, countdown panel, changelog link) and tripped Playwright's strict-mode locator check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo review (Tier 3 race conditions + tier-off bypass) (#7607) Four fixes for bugs flagged by Qodo's review of PR #7720: 1. **Tier=off bypasses scheduler** (correctness). expressCreateServer used to instantiate the scheduler and rehydrate any persisted `scheduled` state regardless of `updates.tier`. A user who set `tier: "off"` after a schedule had been persisted would still see the timer fire after restart. The boot path now skips scheduler creation when tier is off and explicitly clears a stale scheduled state to idle (logged so the admin sees what happened). 2. **Timer fire skips state recheck** (reliability). The scheduler's timer callback called applyUpdate() directly. Race: admin clicks Cancel at the same instant the timer fires, or the tier flips during the grace window. Now schedulerTriggerApply re-loads state and re-evaluates policy via a new pure decideTriggerApply() helper in Scheduler.ts. If state is no longer scheduled (or scheduled for a different tag), aborts. If policy now denies auto, persists state back to idle and aborts. 3. **Apply-now leaves scheduler timer armed** (correctness). The apply endpoint accepts `scheduled` as an entry status but didn't cancel the in-process scheduler timer. After the admin clicks Apply now, the still-armed timer could later fire and attempt another apply (especially if the manual one finishes in preflight-failed, which is also an allowed-entry status). Apply handler now calls cancelScheduler() when entering from `scheduled`. 4. **scheduledFor not validated as timestamp** (reliability). State validator only required scheduledFor / startedAt etc. to be non-empty strings; a hand-edited "scheduledFor": "garbage" would pass validation and yield NaN delay → immediate fire. The validator now requires known timestamp fields to be parseable via Date.parse(). Tests: 6 new decideTriggerApply cases + 3 new state.ts validation cases. 189 vitest pass / 29 mocha integration pass / ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b4b39e84e9
|
build(deps): bump semver from 7.7.4 to 7.8.0 (#7731)
Bumps [semver](https://github.com/npm/node-semver) from 7.7.4 to 7.8.0. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v7.7.4...v7.8.0) --- updated-dependencies: - dependency-name: semver dependency-version: 7.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
ff7c4d5310
|
fix(admin): replace hardcoded German strings with i18n keys (#7735) (#7736)
* fix(admin): replace ~50 hardcoded German strings with i18n keys (#7735) PR #7716 ("chore: fixed admin design rework") rebuilt admin/src/pages with literal German copy inline — "Update verfügbar", "Aktualisieren", "Keine Pads gefunden", "Hook-Bindings", "de-DE" date formatters, etc. Non-DE users see a French/English/German salad: <Trans i18nKey="…"/> calls resolve correctly via translatewiki, but every literal stays German regardless of browser locale. This change: - Adds 90+ keys to src/locales/en.json under admin.*, admin_login.*, admin_pads.*, admin_plugins.*, admin_plugins_info.*, admin_settings.*, admin_shout.*, and the previously-orphaned update.page.{disabled, unauthorized,error}. - Replaces every hardcoded literal in admin/src/{App,LoginScreen, HomePage,HelpPage,PadPage,SettingsPage,ShoutPage,UpdatePage}.tsx with t() or <Trans>. - Threads i18n.language into PadPage so relativeTime() and toLocale*() honour the user's locale instead of forcing de-DE. Test coverage: - src/tests/backend-new/specs/admin-i18n-source-lint.test.ts (vitest): scans admin/src/pages/*.tsx + App.tsx for a denylist of German literals introduced by #7716, asserts PadPage no longer hardcodes 'de-DE', and pins the set of new en.json keys. - src/tests/frontend-new/admin-spec/admini18n.spec.ts (Playwright): extended to assert rendered English text on every page (Home, Pads, Help, Login) and verify no German leakage on the English path. Non-EN locales pick up translations from translatewiki on its normal cadence; until then i18next falls back to en.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): reuse existing ep_admin_pads:* keys instead of duplicating Pre-rework admin already had: ep_admin_pads:ep_adminpads2_action ("Action") ep_admin_pads:ep_adminpads2_last-edited ("Last edited") ep_admin_pads:ep_adminpads2_no-results ("No results") Initial pass added admin_pads.{col.action, col.last_edited, sort.last_edited, empty_state} duplicating those — drop the duplicates from en.json and point PadPage.tsx at the existing translatewiki-fed keys. Stats/column heads that genuinely didn't exist before (admin_pads.col.{pad,users,revisions}, the filter chips, relative-time, pagination, etc.) stay as new keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): address Qodo finding + restore #7716 regressions Qodo flagged a reliability bug in PadPage on PR #7736: i18n.language flows from user-controlled ?lng= straight into Intl.* formatters, which throw RangeError on malformed tags (e.g. 'en_US', '💥'). Crashing the pads page on a crafted URL. Wrap the locale in a sanitizeLocale() helper that normalises '_' → '-' and validates via Intl.DateTimeFormat.supportedLocalesOf(), falling back to 'en' so dates render in a sane locale rather than the user's browser default fighting page copy. Same audit surfaced four additional regressions from #7716 still on develop, fixed here on-theme: - HomePage dropped <a href="https://npmjs.com/..."> wrappers on both installed and available plugin rows. Restored with .pm-plugin-link. - "Downloads" column / "Most popular" default sort / "Popular" tag were dead UI — src/static/js/pluginfw/installer.ts::search() never populates `downloads`. Removed the column, default sort, and tag; dropped `downloads` from PluginDef + SearchParams.sortBy. - PadPage sort dropdown hardcoded `ascending: e.target.value === 'padName'`, leaving no way to invert direction. Replaced with a paired ↑/↓ button (.pm-sort-dir) for both HomePage and PadPage. - "1 Core" stat hint hardcoded count=1. Derived from installedPlugins.filter(p => p.name === 'ep_etherpad-lite').length. - Deleted orphan modules SearchField.tsx and sorting.ts (no longer imported anywhere after #7716). Tests: - admin-i18n-source-lint.test.ts: +3 assertions (sanitizeLocale pattern, dead-downloads check, orphan-module deletion, sort-dir toggle) → 14 passing. - admini18n.spec.ts: +2 assertions (npmjs link on ep_etherpad-lite row, sort-direction toggle visible). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): add mandatory i18n + a11y guardrails PR #7716 ("admin design rework") shipped ~50 hardcoded German literals, dropped npmjs.com link affordances, removed the sort-direction control on PadPage, and forced `de-DE` into Intl formatters — none of which the AGENTS.MD guide explicitly forbade. Document the rules so the next UI refresh cannot regress these in the same way: - i18n section spells out which slots must be localised (JSX text, placeholders, titles, aria-labels, alts, toasts, options, alerts), which API to use per surface (<Trans>/t() in React, data-l10n-id in the legacy pad UI, never window._ rebound), where keys live (src/locales/en.json — never hand-edit non-EN locales), to reuse existing keys before duplicating, pluralisation via _one/_other, defaultValue is safety not a substitute, and points at the source-lint test that enforces the denylist. - a11y section spells out the lessons surfaced by the audit: icon-only buttons need aria-label AND title (both localised), sort controls must be focusable + reversible, semantic HTML over div soup, external navigation is <a>, "don't drop affordances when restyling" is a hard rule, Playwright specs must assert rendered strings + at least one structural affordance for UI changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b3faeffc31
|
fix(tests): retry rmdir to clear Windows EBUSY flake in updater-integration (#7728)
* fix(tests): retry rmdir to clear Windows EBUSY flake in updater-integration
The Windows backend-test job has been intermittently red on `crash-loop
guard: bootCount=3 forces immediate rollback` (and other cases in
`updater-integration.ts`) with:
Error: EBUSY: resource busy or locked, rmdir
'C:\Users\RUNNER~1\AppData\Local\Temp\updater-it-...'
Each `it()` builds a temp git repo via `execSync('git ...')` and cleans
up in a `try…finally` with `fs.rm(dir, {recursive: true, force: true})`.
On Windows, git child processes can briefly hold file handles after
exit (NTFS lazy-release / antivirus scan / pack-file handles), so the
first rmdir attempt hits EBUSY. `fs.rm`'s default `maxRetries` is 0, so
there is no recovery and the test errors out.
Hoist the cleanup to a single `cleanupTmp()` helper that passes
`maxRetries: 10, retryDelay: 100` (a built-in `fs.rm` capability since
Node 14.14). On Linux/macOS this is a no-op — there's nothing to retry.
On Windows it absorbs the transient lock.
No production code touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tests): poll for rollback terminal state instead of 250ms sleep
Windows CI failure on this branch surfaced a *second* flake in the same
file (`crash-loop guard: bootCount=3 forces immediate rollback`):
TypeError: Cannot read properties of undefined (reading 'execution')
at updater-integration.ts:230
`checkPendingVerification` kicks off `performRollback` as fire-and-forget
(`void performRollback(s, deps).catch(...)`), and the test waits a flat
250 ms before asserting `states.at(-1)!.execution.status === 'rolled-back'`.
On Linux 250 ms is plenty. On Windows, git checkout + spawned-process
bookkeeping regularly push past that — so `saveState` hasn't fired yet
and `states` is empty.
This race was previously masked: the test's `finally` ran `fs.rm`,
which threw EBUSY against handles still held by the in-flight rollback,
and JS's "finally-throws-override-try-throws" semantics meant mocha
reported the EBUSY rather than the underlying TypeError. The retry-rm
patch on this branch unmasked it.
Replace the flat sleep with condition-based polling (25 ms tick, 10 s
ceiling) for a terminal state (`rolled-back` | `rollback-failed`). The
existing `assert.equal(... 'rolled-back')` still runs, giving a clean
diff if rollback landed on the failure side instead. Linux runtime
drops 329 ms → 104 ms because the poll exits as soon as the state
lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
113324c6be
|
Localisation updates from https://translatewiki.net. | ||
|
|
e56cb659d3
|
fix(tests): close socket.io clients in lowerCasePadIds spec (#7722)
This spec opened three socket.io-client connections (for ALREADYexistingPad, alreadyexistingpad, maliciousattempt) but never closed them. Each leaked client kept a 5-second reconnect timer armed in node_modules/.pnpm/socket.io-client/.../manager.js past mocha's "passing" output. That triggered the unclean-exit force-quit (server.ts setTimeout(..., 5000)) which then exits the process with code 1 — visible on Windows + Node 24 in CI and 100% reproducible locally on Node 24. Verified with `wtfnode.dump()` patched into the force-exit path: before this change, three timers at socket.io-client manager.js:375 were live; after, the same suite exits cleanly with code 0 and the force-exit timer never fires. The fix tracks every socket returned by common.connect() in an array and disconnects each one in afterEach. Refs: src/tests/backend/diagnostics.ts comment block, PR #7663. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d6a55c2283
|
feat(7642): bin/compactStalePads — staleness-gated bulk compaction (#7708)
* feat(7642): bin/compactStalePads — staleness-gated bulk compaction Adds bin/compactStalePads with --older-than / --keep / --dry-run. Composes listAllPads → getLastEdited → compactPad so hot pads in active timeslider use are left alone and only the cold tail is compacted. Targeting stays a CLI concern; compactPad's API surface is unchanged. Per-pad failures (including a getLastEdited fault) don't stop the run — same error-tolerance shape as compactAllPads. End-to-end test plumbs through the real /api/1.3.1/getLastEdited + compactPad endpoints to lock the adapter contract. Daily-cron variant (cleanup.compactOlderThanDays setting) deferred to a follow-up so this PR stays focused on the on-demand operator tool from the issue's primary acceptance bullet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7642): TOCTOU recheck before compaction + admin CLI docs Qodo flagged two real issues: 1. Race window between staleness selection and compaction. On a long bulk run a pad could become active between first-pass filtering and compactPad, which would then kick those sessions. Added a getLastEdited recheck right before each compact call; if the pad is now fresh it's reclassified as skippedFresh rather than failed (the user did the right thing — edited it — and we bow out). 2. doc/cli.md had nothing on pad compaction at all (gap predates this PR; #6194 landed without doc updates). Added a Pad compaction section covering all three CLIs — compactPad, compactAllPads, compactStalePads — so the toolset is discoverable as a unit. Tests cover both the recheck-skip path and a recheck-failure path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
49466a2870
|
build(deps): bump undici from 7.25.0 to 8.2.0 (#7701)
Bumps [undici](https://github.com/nodejs/undici) from 7.25.0 to 8.2.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.25.0...v8.2.0) --- updated-dependencies: - dependency-name: undici dependency-version: 8.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
41d10ecae5
|
chore: fixed admin design rework (#7716)
* chore: fixed admin design rework * chore: fixed qodoos remarks |
||
|
|
451bd9c3eb
|
feat(pad): scrub history in-place on the pad URL (#7659) (#7710)
* feat(pad): scrub history in-place on the pad URL (#7659) Clicking the timeslider toolbar button now keeps the user on /p/:pad and toggles a hash-based history mode (#rev/N) instead of navigating to a separate /timeslider page. The pad shell — chat, users panel, settings, plugin chrome — stays mounted across the transition. A sticky banner plus a sepia tint on the toolbar make it unmistakable that what is visible is historical, not live. Implementation: - New PadModeController (src/static/js/pad_mode.ts) owns enter/exit, the URL hash, browser back/forward, and a mutation-observer bridge from the inner timeslider's revision label/date into the outer banner. Esc and a Return-to-live button both exit history. - pad.html grows a banner element and an iframe mount slot. The live ACE iframe stays mounted but hidden during history; on exit the socket is still alive, so the user snaps straight back to the current state without a reconnect. - The /p/:pad/timeslider route 302-redirects to the pad page for direct visits (legacy bookmarks), and serves the timeslider HTML for the in-pad iframe when called with ?embed=1. The embedded variant hides the redundant title and return-to-pad button via CSS; the slider, settings, and export controls stay reachable. - Legacy #NN shortlinks are preserved through the redirect by the browser and translated to #rev/NN client-side. Tests: - New backend spec asserts the 302 redirect, pad-name preservation, and the ?embed=1 path still serves the timeslider HTML. - New padmode.spec.ts exercises toolbar entry, return-to-live, browser back, and direct /timeslider URL handling. Asserts the rendered localized banner string, not just element presence. - Existing timeslider specs that hit /p/:pad/timeslider directly now pass ?embed=1 to bypass the redirect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): make history iframe fill the editor area (#7659) Without an explicit positioning model the history-frame-mount inherited half-width from a phantom flex parent and the embedded timeslider rendered at 640×625 instead of the full editor area. Switch to the same absolute-fill model the live ACE iframe uses by making #editorcontainerbox the positioning anchor when in history mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): address Qodo review and CI failures (#7659) Concrete review fixes for PR #7710: - Tighten the embed query check from `if (!req.query.embed)` to `req.query.embed !== '1'` so values like `?embed=0` no longer bypass the redirect. - Fix the `#rev/latest` mapping: the parser yields -1 for "latest", which the iframe sync handler was clamping to 0 and so jumping the embedded timeslider to revision 0. Resolve "latest" to the inner BroadcastSlider's upper bound instead. - Update existing backend tests (`socialMeta`, `specialpages`) that hit `/p/:pad/timeslider` directly — they now pass `?embed=1` like the rest of the suite. Without this fix three pre-existing tests failed CI (302 instead of 200). - Document the route change in `doc/skins.md` and `doc/skins.adoc`: direct visits redirect; iframe consumers use `?embed=1`. - Back out a stray `data-theme="editorial"` attribute and the hardcoded Google Fonts `<link>` tags from `pad.html` that leaked into the branch from an unrelated working-tree change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): consolidate chrome and replay chat/users in history mode (#7659) Picks up the rough edges left by the initial in-place history mode: the embedded timeslider iframe was rendering its own duplicate Settings and Export buttons, and the chat panel + users list still showed live state while the editor scrubbed back in time. Chrome consolidation - Hide the entire inner editbar's right-side toolbar and modal popups in embedded mode (slider stays). Outer pad shell now owns Settings, Export, Share, Users, Chat across both modes. - Outer Settings popup grows a "History playback" section (visible only when scrubbing) with playback speed + follow-contents. Both bridge to the iframe's BroadcastSlider state. - Outer Export anchors are rewritten to /p/<pad>/<rev>/export/<type> on each scrub and restored on exit, so Save As exports the visible historical revision. Chat replay - Each chat message is annotated with data-timestamp at render time. In history mode, messages newer than the scrubbed revision's timestamp are display:none'd; a "Chat as of HH:MM" header sits above the chat log. - Restores cleanly on exit (inline display cleared, header removed). Users replay - Live users table is replaced with the embedded timeslider's authors-at-this-revision label while scrubbing; restored on exit. Plumbing - Expose padContents on window in broadcast.ts so the outer pad can read currentTime after each scrub without postMessage. - Expose BroadcastSlider on window in timeslider.ts so the outer pad can register an onSlider callback to drive replay UI. Tests - New padmode specs cover: history-only Settings section, hidden embedded chrome, chat filter + replay header, Export href rewriting + restore, authors-row swap + restore. - timeslider_line_numbers cookie-persistence test updated to bypass the now-hidden inner Settings popup (programmatic checkbox). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): theme propagation, hide inert buttons, plugin loading (#7659) Picks up rough edges from the in-place history mode that turned up in real usage: Theme / dark mode - skin_variants.updateSkinVariantsClasses now also walks the history iframe (and its ace_outer/ace_inner) so toggling dark mode while scrubbing re-themes the embedded view in lockstep. - timeslider.ts inherits the parent's skinVariant tokens (super-dark-* / dark-* / full-width-editor) on first paint when it detects it is embedded — same-origin guarantee, falls through silently if not. Toolbar UX - Hide #editbar .menu_left (Bold/Italic/Lists/Indent/Undo/...) and the show-more chevron while in history mode. Those buttons target the hidden live editor and would do nothing useful; rendering them disabled-looking implied state the user doesn't have. Right-side menu (Settings / Share / Users / Chat / Home) stays at full opacity and fully interactive. Slider position - Pin the embedded #editbar to the bottom of the iframe so the outer banner and the slider can't visually compete for the same band of pixels. Reserve padding-bottom on the iframe's editorcontainerbox so the editor never scrolls under the slider. Plugin loading in timeslider - timeSliderBootstrap.js now pre-loads plugin modules into a Map and passes them to plugins.update(), mirroring padBootstrap.js. Without this the loadFn fallback called require(path) at runtime, which the esbuild-bundled timeslider couldn't resolve, so client_hooks like ep_headings2's aceRegisterBlockElements silently failed to register and historical revisions rendered without plugin chrome. Tests - New padmode specs cover: outer toolbar's left/right asymmetry, slider pinned to bottom, dark-mode class propagation into the history iframe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): move history slider into the outer toolbar (#7659) The slider previously rendered inside the embedded iframe — first at the top (where it visually competed with the banner), then briefly at the bottom (where the chat icon overlapped it). Both were wrong. Move the controls into the outer toolbar's left zone, where #editbar .menu_left is hidden in history mode and the slider can occupy the full width without colliding with anything. - pad.html grows a #history-controls div (slider + play/pause/step buttons + timer) inside #editbar, between menu_left and menu_right. Hidden by default; revealed via body.history-mode CSS. - pad.css swaps #editbar .menu_left out for #history-controls in history mode (display:none / display:flex). - timeslider.css fully hides the embedded iframe's #editbar — the outer toolbar now owns the slider, and the iframe is purely the editor surface. - pad_mode.ts wires the outer controls as a remote control: the range input calls inner BroadcastSlider.setSliderPosition, the play button calls BroadcastSlider.playpause, step buttons forward clicks to the inner #leftstep/#rightstep so they share the existing logic. An onSlider subscription mirrors inner state back into the outer slider value, timer label, and play-button .pause class. Tests - Existing timeslider.spec asserts the outer controls are visible. - New padmode specs cover: inner editbar fully hidden, outer toolbar swap (menu_left → history-controls), and outer slider drives the iframe's revision via BroadcastSlider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): exempt embedded history iframe from userdup kick (#7659) When the in-place history iframe opens its socket, the server's duplicate-author kick treats it as a stale tab and disconnects the parent pad's live socket — toolbar-overlay drops over the editor and Settings/Share/Users/Chat all stop responding. Mark the iframe's connection with `embed=1` in the socket.io handshake query, record it on sessionInfo, and skip the kick whenever either side is embedded. - timeslider.ts: detect `?embed=1` (and parent !== window) on the iframe URL, pass through as a query parameter to socketio.connect. - PadMessageHandler: read socket.handshake.query.embed on CLIENT_READY, set sessionInfo.embed; the duplicate-author kick now skips when either the connecting session OR the existing session is embedded. Behavior preserved - Two real tabs (both non-embedded): older tab still gets kicked. - Authenticated sessions still bypass the kick entirely. - Live pad socket survives entry into history mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): a11y of history toolbar controls (#7659) The new history controls (slider + play/step/timer) had hardcoded English aria-labels, which html10n won't replace because they were present without the data-l10n-aria-label marker. Screen readers in non-English locales would have heard English. Drop the static aria labels and let html10n.translateElement populate aria-label from the data-l10n-id translation, matching how the rest of the toolbar works. - pad.html: remove hardcoded aria-label on play/step buttons and the range input; keep titles (hover tooltip) and data-l10n-id. Add role="toolbar" + data-l10n-id on the controls container so the toolbar landmark is announced. Mark play button as a toggle with aria-pressed reflecting playback state. - en.json: add pad.historyMode.controlsLabel and pad.historyMode.sliderLabel for the toolbar landmark and the slider. - pad_mode.ts: keep aria-pressed in sync with the inner playback state on every revision update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): a11y + responsive for history controls (#7659) Two issues with the previous a11y attempt: the data-l10n-id on icon buttons was setting their textContent (drawing "Playback / Pause Pad Contents" on screen next to the glyph), and there was no responsive treatment so the timer + slider could overflow narrow viewports. - pad.html: drop data-l10n-id from the icon buttons. They're now empty <button>s. Localized title (hover tooltip) and aria-label (screen reader name) are populated by pad_mode.localizeControls() using the existing timeslider.* keys, with an html10n.bind subscription so language switches re-localize. - Mark #history-timer as hide-for-mobile. - pad.css: dedicated @media (max-width: 800px) and 480px rules shrink padding, gap, and button widths so play + slider + step buttons stay on a single toolbar line at narrow viewports. Mirrors the legacy timeslider's responsive behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): inline Follow + Playback speed, match toolbar height (#7659) Two follow-ups from real testing: - Move "Follow pad content updates" (now "Follow") and "Playback speed" out of the Settings popup and inline them in the history-mode toolbar, alongside the slider + play/step buttons. They were always needed while scrubbing; one extra click into Settings was friction. Removed the now-empty #history-settings-section. - The history controls toolbar was visibly shorter than the live toolbar because the icon buttons sat as bare <button> elements without the live editbar's <li><a> wrapping. Add explicit min-height (40px) and per-button padding so the toolbar is the same vertical size in both modes — switching between live and history no longer reflows. - Differentiate "iframe-mounted history view" from "direct ?embed=1 visit". Only the former hides the inner timeslider editbar — direct visits keep their full chrome so existing test/legacy entry points stay independently usable. Marker: timeslider.ts adds an `iframe-mode` class on body when window.parent !== window; CSS scopes the hide to that combo. Tests - padmode spec asserts Follow + Speed live in the toolbar (not the Settings popup) and are visible in history mode, hidden in live. - timeslider*.spec direct-?embed=1 flows continue to pass because the inner editbar is no longer hidden when not iframe-mounted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): use absolute path for legacy /timeslider redirect (#7659) CI Firefox failed the legacy-URL redirect test (1 of 32 jobs); Chromium passed. The redirect Location header was a relative `../padname`, which both browsers resolve to /p/padname for `/p/padname/timeslider`. Firefox flaked on it once consistently. Switch to an absolute path including the proxy prefix so the resolution is unambiguous across browsers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): accept 304 on legacy timeslider redirect (#7659) CI Firefox failed `expect(res.status()).toBe(200)` because Firefox issues a conditional GET when the redirect target is the same URL the test just loaded via goToNewPad — the server returns 304 Not Modified and the test treats that as a regression. Chromium happens to send fresh requests so it stayed green. Accept either 200 or 304 — both are valid completed navigations to the pad page; what we actually care about is the pathname assertion above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): eye toggle for Follow, fix line-number alignment (#7659) Two refinements from real testing in 9002: Follow as an eye toggle - Replace the labeled checkbox with an inline-SVG eye icon. The eye is always rendered; a diagonal slash is overlaid via SVG <line> only when the underlying (visually hidden) checkbox is unchecked. Default state is on (auto-following) so the eye renders unobstructed. - Localized hover tooltip + aria-label flips with state — html10n populates "Following pad changes — click to stop following" vs "Not following pad changes — click to follow", and pad_mode.ts re-applies on every change event so screen readers narrate the action the click would take. - Hidden checkbox keeps the existing pad_mode.ts bridge code working (still reads .checked) and lets <label for="…"> handle the click. Line-number alignment fix (broadcast.ts) - The first-line height formula was `nextDocLine.offsetTop - innerdocbody.padding-top` which only computes the right value when innerdocbody is the offsetParent. In the in-pad history iframe, outerdocbody contributes its own padding-top to the offsetTop chain, so the first gutter row was 20px too tall and every subsequent line drifted out of alignment. Use the consistent `next.offsetTop - current.offsetTop` formula for every iteration — same result in the standalone timeslider, correct result in the embedded one. - New padmode spec asserts every gutter row's top matches the editor line's top within 2px, in iframe-mounted history mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cbf71285a2
|
feat(api): clean up published OpenAPI spec for downstream codegens (#7714)
* feat(api): clean up published OpenAPI spec for downstream codegens
The /api/openapi.json doc had four issues that broke generated tooling
(printingpress.dev, openapi-generator, Postman): empty top-level tags
array, every operation duplicated as GET+POST, 14 operations missing
from the resources map (drift since API 1.2.8), and empty summaries on
several tracked ops.
This PR splits the runtime spec from the published spec via a {public}
flag on generateDefinitionForVersion: the runtime definition fed to
openapi-backend keeps both verbs (existing third-party clients that
call GET /api/x.x.x/foo?apikey=... continue to work), while the spec
served at /api/openapi.json, /rest/openapi.json, and per-version paths
advertises only POST.
Other changes:
- top-level tags array declares pad/author/session/group/chat/server
- per-op tags override added to SwaggerUIResource type so chat ops
(still nested under pad for routing) and checkToken can be tagged
without changing existing REST URLs
- 14 missing ops (getAttributePool, getRevisionChangeset, copyPad,
movePad, getPadID, getSavedRevisionsCount, listSavedRevisions,
saveRevision, restoreRevision, appendText, copyPadWithoutHistory,
compactPad, anonymizeAuthor, getStats) backfilled with summaries
- empty summaries on listSessionsOfGroup, listAllGroups,
createDiffHTML, createPad filled in
- new backend tests assert the public spec shape (tags, summaries,
POST-only) and that runtime routing still resolves both verbs
Driven by integrating Etherpad with printingpress.dev: pointing the
generator at the previous spec produced a 96-command CLI with no
resource grouping and many empty descriptions. Design notes in
docs/superpowers/specs/2026-05-10-openapi-cleanup-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): keep checkToken at /rest/<ver>/pad/checkToken (Qodo #2)
Moving checkToken to a new `server` resource broke REST-style
backward compat: existing callers of /rest/<ver>/pad/checkToken
would have hit `code: 3` (no such function). The whole point of
per-op tag overrides is to preserve REST URLs while still grouping
correctly in OpenAPI tags — checkToken should follow the same
pattern as the chat ops.
Keep checkToken in `resources.pad`, give it `tags: ['server']`,
and add a regression test asserting /rest/<ver>/pad/checkToken
still resolves. The `server` resource still exists for `getStats`
(genuinely new server-level op with no prior REST URL).
Updates the design doc accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
df0ecec834
|
a11y: localize aria-label on form-control elements (<select>, <input>, <textarea>) (#7713)
#7584 introduced auto-population of aria-label from a translation when an element has data-l10n-id and no author-supplied aria-label. That branch only fires for elements with no children (or with data-l10n-id ending in a recognized attribute suffix like .title). Form-control elements break the assumption: a <select> always has <option> children, an <input>/<textarea> may have implicit value content. The textContent branch handles them, but the aria-label fallback wasn't called from there. Plugins like ep_font_size, ep_headings2, and ep_hljs end up with a localized translation applied to a <select> but no accessible name on the element itself. Calls populateAriaLabel() from the textContent branch when the node is a <select>, <input>, or <textarea>. Keeps the same "author-supplied aria-label wins on first pass; the data-l10n-aria-label marker lets us refresh values we wrote" semantics from #7584. Adds Playwright coverage in src/tests/frontend-new/specs/html10n_form_controls_aria.spec.ts: - aria-label is populated on <select> with data-l10n-id - aria-label is populated on <textarea> with data-l10n-id - author-supplied aria-label is preserved on first pass |
||
|
|
2adc228e61
|
fix(tests): unblock CI — Windows updater paths + admin-plugins row count (#7712)
* fix(updater): build expected paths via path.join in updater tests The Windows backend job has been red on develop since #7607 (tier-2 auto-update) merged: RollbackHandler.test.ts:122 and UpdateExecutor.test.ts:66 asserted POSIX-style paths, but the implementations build the same paths via path.join — which emits backslashes on Windows. The tests pass on Linux/macOS only by coincidence. Switch the expected values to path.join(deps.repoDir, ...) / path.join(deps.backupDir, ...) so they track whatever the implementation produces on the host platform. No production code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin-spec): assert installed-plugins by name, not row count ep_set_title_on_pad@0.7.2 took on ep_plugin_helpers as a transitive plugin dependency, so installing it now adds two rows to the installed-plugins table (the new plugin + its helper plugin) rather than one. The Playwright spec hard-coded `toHaveCount(2)` after install and `toHaveCount(1)` after uninstall, so it has been red on develop since #7705 merged the new admin bundle (every Frontend admin tests job, every Node version). Switch the assertions to scope by row text — `tr` containing `ep_set_title_on_pad` — and check that exactly one such row exists after install and zero after uninstall. This survives whatever transitive plugin deps the chosen test plugin pulls along, which is the only thing this spec actually cares about. Verified locally on a fresh Etherpad install: 3/3 admin-update-plugins tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
efb8328084
|
feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704)
* docs(updater): PR 2 (Tier 2 manual-click) implementation plan 20-task TDD plan for shipping the manual-click update flow on top of the Tier 1 (notify) work merged in #7601. Covers UpdateExecutor, RollbackHandler, SessionDrainer, lock + trustedKeys, four admin endpoints (apply / cancel / acknowledge / log), admin UI updates, integration tests against a tmp git repo, and a manual smoke runbook for the spec's "before each tier ships" gate. Plan deliberately scopes signature verification to an opt-in stub (updates.requireSignature: false default) to avoid blocking on a separate release-signing project. Plan: docs/superpowers/plans/2026-05-08-auto-update-pr2-manual-click.md Spec: docs/superpowers/specs/2026-04-25-auto-update-design.md Issue: ether/etherpad#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): extend state + settings for Tier 2 manual-click Adds ExecutionStatus discriminated union, bootCount, and lastResult to UpdateState, plus the preApplyGraceMinutes/drainSeconds/diskSpaceMinMB/ requireSignature/trustedKeysPath knobs that Tier 2's executor needs. loadState backfills the new fields on Tier 1 state files so existing installs keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): PID-based update.lock with stale-pid reaping Single-flight guard for Tier 2's UpdateExecutor. Atomic O_CREAT|O_EXCL acquire; on EEXIST, sends signal 0 to the recorded PID and reaps if dead. Unparseable / partially-written lock files are treated as stale rather than fatal so a half-written lock from a SIGKILL'd parent doesn't lock the install out forever. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): verifyReleaseTag — gpg-via-git stub for Tier 2 preflight Default updates.requireSignature=false: log a warning and return ok with reason=signature-not-required. Set true to make preflight refuse a tag whose signature does not verify under the system keyring (or trustedKeysPath via GNUPGHOME). Etherpad's release process does not yet sign tags consistently; turning the check on by default would break Tier 2 for every admin and forcing a release-signing change is out of scope for this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): preflight check pipeline for Tier 2 Pure orchestrator over injected probes for install-method, working tree, disk space, pnpm presence, lock state, remote tag existence and signature verification. Cheap-and-definitive checks run first; first failure short-circuits with a typed reason that the route layer will surface in the preflight-failed admin banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): rolling update.log helpers (appendLine + tailLines) Direct file-append + size-based rotation rather than a log4js appender — avoids re-configuring log4js on top of the user's existing logconfig. appendLine creates parents, rotates at 10MB (configurable), keeps 5 backups by default. tailLines reads the last N lines for /admin/update/log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): SessionDrainer + handshake guard Drainer schedules T-60 / -30 / -10 broadcasts and resolves at T=0; isAcceptingConnections() flips off for the duration. PadMessageHandler consults the flag at the start of CLIENT_READY and disconnects new joiners with reason "updateInProgress" — existing sockets are unaffected. Drains shorter than 30s collapse the early timers to fire ASAP rather than queue past the drain end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): UpdateExecutor — snapshot, fetch/checkout/install/build, exit 75 Pure-DI orchestrator: spawnFn, copyFile, readSha, saveState, exit are all injected so unit tests run the full pipeline without spawning real children or mutating the real install. Streams stdout/stderr to update.log via the now-best-effort appendLine helper (swallows fs errors so the executor itself never breaks on read-only / unwritable log dirs). Failure paths transition to rolling-back and return — the route layer hands off to RollbackHandler which owns the rollback exit, so we don't double-exit and lose tail lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): RollbackHandler — health-check timer + crash-loop guard checkPendingVerification arms a 60s timer at boot when state is pending-verification and increments bootCount; bootCount>2 forces an immediate rollback (crash-loop guard). markVerified persists the verified state and stops the timer. performRollback restores the backup lockfile, runs git checkout <fromSha> and pnpm install, lands on rolled-back or rollback-failed (terminal) on sub-step failure, exits 75 either way so the supervisor restart brings the new state up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): wire RollbackHandler into boot + UpdatePolicy honours rollback-failed - expressCreateServer now invokes checkPendingVerification before polling starts so a previous boot's pending-verification either re-arms the health-check timer or, when bootCount has climbed past the crash-loop threshold, forces an immediate rollback. - server.ts calls markBootHealthy after state hits RUNNING so /health-being-up is the implicit happy-path signal that cancels the rollback timer. - /admin/update/status surfaces execution + lastResult + lockHeld so the admin UI can render the right Apply / Cancel / Acknowledge state. - UpdatePolicy gains an `executionStatus` input. While it equals 'rollback-failed', canAuto / canAutonomous are denied (reason: rollback-failed-terminal); manual stays on because clicking Apply IS the intervention the terminal state needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): apply / cancel / acknowledge / log endpoints Strict admin-only POSTs that drive Tier 2's manual-click flow: - POST /admin/update/apply: acquire lock, persist preflight, run preflight, drain $drainSeconds, executeUpdate (which exits 75 on success), or run performRollback on a failure path (also exits 75). - POST /admin/update/cancel: cancel a pre-execute drain/preflight, write cancelled lastResult, release lock. - POST /admin/update/acknowledge: clear terminal states (preflight-failed, rolled-back, rollback-failed) back to idle. lastResult is preserved so the admin still sees what happened. - GET /admin/update/log: tail var/log/update.log (200 lines) for the in- progress UI. Strict admin auth. Also: - socketio hook exports getIo() so the apply endpoint can broadcast the drain shoutMessage outside the regular hook surface. - ep.json registers updateActions after admin/updateStatus. - 11 mocha integration tests cover auth, policy denial, execution-busy, acknowledge-clears-terminal, log content-type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): admin UI Apply/Cancel/Acknowledge + live log stream UpdatePage renders the right action set based on execution.status: Apply when idle/verified and policy allows, Cancel during preflight/draining, Acknowledge on terminal preflight-failed / rolled-back / rollback-failed. While the executor is in flight (preflight/draining/executing/rolling-back) the page polls /admin/update/log + /admin/update/status once a second and shows the rolling tail; polling stops automatically when the run terminates. lastResult and policy denial reasons surface localised copy. Buttons disable themselves while a network round-trip is in flight to dodge double-clicks. New i18n keys live under update.page.{apply,cancel, acknowledge,log,execution,policy.*,last_result.*}, update.execution.*, update.banner.terminal.rollback-failed, and update.drain.{t60,t30,t10}. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): pad shoutMessage renders update.drain.* via html10n broadcastShout now sends {messageKey, values, sticky} so the existing pad-side shout pipeline can route through html10n.get(). The renderer gains a values pass-through so update.drain.t60 etc. interpolate {{seconds}}, and gives updater shouts a different gritter title (the banner.title localised string) so users know it's a system event rather than a generic admin message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): rollback uses git checkout -f + integration suite over tmp git repo RollbackHandler now does git checkout -f <fromSha> BEFORE overlaying the backup lockfile. Without -f, git refuses checkout when there are unstaged modifications to files it would overwrite — exactly the case after a partial executor run that mutated the working tree. With -f the partial mutation is discarded and the working tree returns to fromSha cleanly. The backup-lockfile copy is still done (belt-and-braces) but tolerates ENOENT since checkout already restored the right lockfile. The new integration suite at src/tests/backend/specs/updater-integration.ts exercises the full pipeline against a disposable git repo: happy path, install-fail rollback, build-fail rollback, crash-loop guard, and a target-sha-doesn't-exist rollback-failed terminal case. 5 mocha tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(updater): Playwright admin Apply / Cancel / Acknowledge flow Stubs /admin/update/status (and /admin/update/apply for the apply path) at the route level so we can assert UI transitions without actually running an update. Four scenarios: - Apply button POSTs and re-fetches status (>=2 status fetches total). - install-method-not-writable hides the button and shows localised denial copy. - rollback-failed terminal state shows the Acknowledge button and the "Manual intervention required" lastResult copy. - lockHeld=true hides Apply even when policy.canManual is on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): admin banner shows rollback-failed terminal alert When execution.status === 'rollback-failed' the banner switches to a role=alert with the strong update.banner.terminal.rollback-failed copy and overrides the regular "update available" framing — an admin who left the system in this state needs to fix it before any other admin work matters. Other terminal states (preflight-failed, rolled-back) are informational and surface on the page itself, not the banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): Tier 2 admin docs + manual smoke runbook + CHANGELOG doc/admin/updates.md gains a full Tier 2 section: prerequisites (git install + process supervisor with sample systemd unit), Apply flow with timings, every failure mode and the resulting state, the four endpoints, and the signature-verification opt-in. Settings table picks up the new updates.* knobs. docs/superpowers/specs/2026-04-25-auto-update-runbook.md is the manual smoke runbook the design spec calls for: disposable VM, systemd unit, every observable transition (happy path, install/ build-fail rollback, crash-loop guard, rollback-failed terminal, cancel during drain) plus a sign-off checklist for the release cut. CHANGELOG Unreleased section explains the supervisor requirement and points readers at the runbook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): note docker-friendly update flows as follow-up work Tier 2 refuses Apply on installMethod=docker because in-container mutation doesn't survive a container restart. Adds a future-work note covering the two reasonable paths for an in-product docker Apply button (instructions-only vs deploy-webhook) and explicitly rules out mounting /var/run/docker.sock as a footgun. Watchtower gets a pointer for admins who want fully autonomous docker updates today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo review (1-6) + Playwright strict-mode CI fix 1. Tier 2 endpoints now gate on tier in {manual, auto, autonomous} — notify and off return 404 to match the prior PR-1 behaviour. Gate is evaluated per-request via app.use middleware so a settings.json reload takes effect without a full restart, and so integration tests can flip the tier dynamically. Adds a regression test that exercises 404 at tier=notify across all four endpoints. 2. cancel/apply race fixed: /admin/update/cancel no longer releases the lock — apply's finally block owns it for the request's lifetime. Apply now reloads state after preflight and aborts with 409 cancelled-during- preflight if execution.status is no longer 'preflight' for the same targetTag. Prevents a second apply from sneaking in while the first is still running its slow checks, and prevents the post-cancel apply from continuing into drain/execute. 3. SessionDrainer now restores acceptingConnections=true at drain completion (not just on cancel). The lock + persisted execution.status prevent a fresh apply from racing in — the in-memory flag was redundant safety that turned into a wedge if the executor threw post-drain. Adds a unit test asserting the flag is restored after natural drain end. 4. PadMessageHandler drain guard switched from socket.json.send (a socket.io v2/v3 API that may not exist on v4) to socket.emit('message', ...) for consistency with the other disconnect paths in the file. 5. Spawn 'error' handlers added to runStep helpers in UpdateExecutor and RollbackHandler, plus the gpg verify-tag spawn in trustedKeys. Without them, a missing/unexecutable binary leaves the promise hanging forever and the update flow stuck in-flight. SpawnFn type extended to allow on('error', ...) listeners cleanly. Spawn errors now resolve with code 1 + the error message in stderr, so the existing failure-detection branches fire normally. 6. executeUpdate body wrapped in try/catch. An exception from readSha, saveState, copyFile, or any step now lands in a rolling-back persist + returns failed-checkout, so the route's post-executor rollback path picks it up. State can no longer wedge at 'executing'. The catch's inner saveState is itself try/wrapped so a write-after-write failure doesn't crash the route either. CI: Playwright update-page-actions strict-mode violation fixed. Both the banner and the lastResult <p> contain "Manual intervention required"; selector now scopes to p.last-result-rollback-failed for the lastResult assertion specifically. 129 vitest unit tests + 23 mocha integration tests passing; ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo #7 (status leak) + #8 (short-drain values) #7. /admin/update/status now redacts diagnostic strings for unauth callers even when requireAdminForStatus is left at its default (false). Status enum + outcome enum are kept (the admin banner / pad-side badge need them to render the right UI) but execution.reason / execution.fromSha / execution.targetTag and the same fields on lastResult are stripped. Authed admin sessions still get the full payload — they're looking at their own server's diagnostics. Two new mocha tests cover both paths: "redacts execution.reason / lastResult.reason for unauth callers" and "returns full diagnostic payload to authed admin sessions". #8. SessionDrainer no longer schedules T-30 / T-10 broadcasts when the configured drainSeconds can't honour them. Previously, with drainSeconds < 30 the T-30 timer fired at zero remaining but the broadcast still claimed "30 seconds" — misleading. Now T-30 only schedules when drainSeconds > 30 and T-10 only when > 10. Admins picking a short drain get fewer announcements but each carries an accurate countdown. The opening announcement now reports the configured drain length rather than a hardcoded 60. Two updated unit tests: drainSeconds=15 (skips T-30, still fires T-10) and drainSeconds=5 (skips both). 131 vitest unit + 26 mocha integration tests passing; ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo follow-up — tag injection, rollback rejections, state validation Qodo posted three new concerns after the first fix push. 1. Git tag option injection (security). The release tag from GitHub's tag_name flowed into `git checkout` / `git verify-tag` as a positional arg. A tag starting with '-' would be parsed as an option and could bypass signature verification or change checkout semantics. Mitigated in three layers: - New refSafety helper (isValidTag / assertValidTag / refsTagsForm) enforces a strict subset of git's check-ref-format spec: rejects leading '-' or '.', whitespace, control chars, and ~ ^ : ? * [ \\ and the '..' sequence. - VersionChecker validates tag_name before persisting to state, so a malformed value from a misconfigured githubRepo never lands on disk. - UpdateExecutor calls assertValidTag and uses the refs/tags/<tag> form for git checkout. trustedKeys also validates and adds '--' to git verify-tag for an end-of-options marker. updateActions does an up-front isValidTag check on state.latest.tag so a corrupt state file gets a clean 409 instead of a 500. 2. Unhandled rollback rejections. checkPendingVerification was firing `void deps.saveState(...)` and `void performRollback(...)` without .catch(), so an fs error during boot's rollback path would bubble out as an unhandled rejection. Both callsites now go through fireSaveState / fireRollback helpers that catch and log; rollback rejections fall through to a best-effort terminal-state write + exit 75 so the supervisor can re-try the next boot with bootCount++. 3. Execution state under-validated. isValidExecution previously checked only that `status` was a known enum value, so a hand-edited state file with `{execution: {status: 'pending-verification'}}` (missing fromSha / targetTag / deadlineAt) would pass validation and reach RollbackHandler with undefined refs. The validator now consults a per-status required-fields map mirroring the ExecutionStatus union in types.ts and rejects empty strings as well as missing fields. Same tightening applied to lastResult.outcome (must be in the allowed enum, not just any string). Six new unit tests cover hand-edited corruption. 145 vitest + 26 mocha tests green; ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4f1b524864
|
feat(admin): document admin endpoints in OpenAPI (#7693) (#7705)
* chore(admin): typesafe API client + TanStack Query rails (#7638) (#2)
* docs(admin): design for typesafe API client + TanStack Query rails (#7638)
Rails-only scope: codegen toolchain, runtime client, and provider — no
call-site migrations. Admin endpoints are not yet covered by the OpenAPI
spec, so a separate issue will follow before any migration is useful.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): implementation plan for typesafe API client rails (#7638)
Step-by-step task breakdown for the rails-only PR: codegen toolchain,
runtime client, TanStack Query provider, CI freshness check, docs. No
call-site migrations until admin endpoints are added to the OpenAPI
spec (separate follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(api): export generateDefinitionForVersion from openapi hook
Required by the admin codegen script (#7638) to dump the OpenAPI spec
without booting Express. No behavior change for the request hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): add OpenAPI codegen + TanStack Query deps (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): add OpenAPI spec dump entry (#7638)
Loaded via tsx by gen-api.mjs in the next commit. Writes JSON to a file
path argument so log4js stdout output (from Settings init) does not
pollute the spec output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): wire OpenAPI codegen into build (#7638)
Adds gen:api script and amends build/build-copy to regenerate
admin/src/api/schema.d.ts before compiling. The generated file is
checked in so it shows up in PR review and so a fresh checkout doesn't
need codegen to typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): typed openapi-fetch + react-query client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): TanStack Query provider, dev-only devtools (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): mount TanStack Query provider at root (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(admin): smoke test for typed openapi-fetch client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(admin): verify generated OpenAPI schema is up to date (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): document OpenAPI codegen workflow (#7638)
Replaces the default Vite scaffold README with admin-specific scripts
table and codegen workflow notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(admin): exclude __tests__ from tsc include (#7638)
The smoke test imports node:test/node:assert which need @types/node.
Admin source is browser-only, so excluding __tests__ from the production
typecheck is cleaner than adding Node types to the bundle config. The
test still runs under tsx, which doesn't share this constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate lockfile with pnpm 10 to restore overrides block (#7638)
Adding admin deps with pnpm 11 stripped the top-level \`overrides:\`
section from pnpm-lock.yaml, which CI uses pnpm 10 to verify. Result:
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every job. Re-running pnpm 10
\`install --lockfile-only\` restores the overrides block; the new admin
package entries land in the same commit. Two stale lockfile entries
not present in package.json (\`serialize-javascript\` version pin and
\`uuid@<14.0.0\`) were normalized by the regen — package.json is the
source of truth for those.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(docker): preserve strictDepBuilds=false in trimmed workspace yaml (#7638)
Adding tsx as an admin devDep brings esbuild@0.27.x into the resolved
subgraph, and the Dockerfile's runtime stage was overwriting
pnpm-workspace.yaml with a stripped-down version that lost the
strictDepBuilds=false setting from the source repo. With pnpm 10's
default of strictDepBuilds=true, the install then errors on
ERR_PNPM_IGNORED_BUILDS for esbuild + scarf rather than warning.
Restore the strictDepBuilds=false and the @scarf/scarf ignore in the
trimmed yaml so the production install matches develop's behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): point client baseUrl at /api/<version> via codegen (#7638)
Qodo flagged: with baseUrl='/' and schema paths like '/createGroup',
calls landed at /createGroup, but the backend mounts the FLAT-style
spec under /api/<version>/. So once a call site lands, every request
404s.
gen:api now also emits admin/src/api/version.ts containing
LATEST_API_VERSION (read from info.version in the spec) and a derived
API_BASE_URL = `/api/<version>`. client.ts imports API_BASE_URL.
Workflow freshness check covers both generated files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(admin): cross-platform spawn in gen-api.mjs (#7638)
Windows CI failed because spawnSync('pnpm', ...) cannot resolve
pnpm.cmd without a shell. Set shell:true on win32 only so Linux/macOS
runs avoid Node's DEP0190 warning. All spawn args are literal strings,
so the shell variant is not an injection risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): gitignore generated schema/version, regen on every script (#7638)
Qodo flagged the committed admin/src/api/schema.d.ts as a build artifact
that violates the rule against committing generated files (rule 467291,
"Exclude build artifacts and runtime-generated files from version control").
This commit:
- Adds admin/src/api/{schema.d.ts,version.ts} to .gitignore.
- Removes both from VCS (the prior squash had committed them).
- Chains \`gen:api\` into the dev and test scripts so a fresh checkout
lands a working dev server / test run without an extra step. build
and build-copy already chained gen:api.
- Drops the now-redundant CI freshness diff step from
frontend-admin-tests.yml — with the files no longer committed, the
build step's gen:api invocation is the only check needed.
- Updates admin/README.md to describe the new generated-file workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): design for admin OpenAPI coverage (#7693)
Adds the design doc for documenting `/admin-auth/*` and `/admin/update/status`
in the OpenAPI spec so the typed client generated by #7695 (`admin/src/api/
schema.d.ts`) gains admin call-sites the day it lands.
This PR is stacked on #7695 — codegen rails must merge first. Schema-only,
no call-site migrations (those are an explicit follow-up named in #7693).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): correct UpdateStatus schema to base-branch shape (#7693)
The first draft mirrored the Tier 2 (#7607) response shape, but this PR
stacks on #7695 whose updateStatus.ts only emits the Tier 1 fields. Fix
the schema, install-method enum, and tier enum to match types.ts and
the actual handler. Tier 2 amends UpdateStatus when it lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): implementation plan for admin OpenAPI coverage (#7693)
Step-by-step task breakdown for the schema-only PR: stub the document,
add /admin-auth/ + /admin/update/status paths and sub-schemas,
collision regression, /admin/openapi.json route, codegen merge, and CI
verification. No call-site migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): stub OpenAPI document for admin endpoints (#7693)
Adds generateAdminDefinition() returning a minimal valid OpenAPI 3.0
document with no paths yet, plus security schemes for the two auth
modes (Basic + session cookie). Subsequent tasks fill in the actual
admin paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): document POST /admin-auth/ in OpenAPI (#7693)
Adds verifyAdminAccess as the operation that the admin UI's LoginScreen
and App session check both call. Documents Basic auth, session cookie,
and anonymous request modes plus their 200/401/403 responses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): document GET /admin/update/status in OpenAPI (#7693)
Adds getUpdateStatus operation plus UpdateStatus, ReleaseInfo,
PolicyResult, and VulnerableBelowDirective sub-schemas. Property names
and enums mirror src/node/updater/types.ts and the response object
emitted by updateStatus.ts. Tier 2 (#7607) will amend UpdateStatus when
it ships execution/lastResult/lockHeld.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(admin): regression net for admin/public OpenAPI collisions (#7693)
Cross-checks admin paths, operationIds, and schema names against the
latest public spec. Today there are no overlaps; the test exists to
catch future renames before they break the merged client codegen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): expose admin OpenAPI doc at /admin/openapi.json (#7693)
Mounts the admin OpenAPI document at /admin/openapi.json (CORS: *) via
an expressPreSession hook, matching the /api/openapi.json convention.
The admin SPA wildcard at /admin/{*filename} registers later in
expressCreateServer, so the JSON route wins. Live-route test confirms
JSON content-type and CORS header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): mergeOpenAPI helper for codegen pipeline (#7693)
Pure-JS deep-merge of two OpenAPI 3.0 documents. Unions paths and
components by key; throws on collisions. Public document's info,
servers, and root security win over the admin document's. Used by
dump-spec.ts to produce a single merged JSON for openapi-typescript.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): fix stale "Section 3" reference in spec (#7693)
Drafting numbered the design walkthrough; final spec uses titled
sections. Update the back-reference accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): include admin OpenAPI in generated client (#7693)
Modifies dump-spec.ts to import generateAdminDefinition alongside the
public generator and feed both through mergeOpenAPI before writing the
JSON consumed by openapi-typescript. The resulting admin/src/api/
schema.d.ts paths interface now exposes /admin-auth/ and
/admin/update/status, ready for typed call-site adoption in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): gate /admin/openapi.json behind a feature flag (#7693)
Address Qodo finding 1: new features must ship behind a flag, disabled
by default (CONTRIBUTING.md, AGENTS.MD, best_practices.md). Adds
settings.adminOpenAPI.enabled (default false). expressPreSession
returns early when the flag is off, so the route is dormant on a fresh
install.
The codegen pipeline imports generateAdminDefinition() in-process and
does not depend on the runtime route, so default-off has no effect on
the typed client. Operators who want third-party tooling (Postman,
swagger-ui, downstream clients) to consume the spec at runtime opt in
via settings.
Adds:
- SettingsType + defaults entry in src/node/utils/Settings.ts
- settings.json.template documentation
- A backend spec asserting expressPreSession is a no-op when the flag
is off (live route test now sets the flag explicitly)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): split fetchClient by surface; admin client baseUrl='/' (#7693)
Address Qodo finding 2 (correctness bug). The merged schema introduced
in this PR exposes both public-API paths (under /api/<version>/) and
admin paths (at root, e.g. /admin-auth/). The single fetchClient from
#7695 has baseUrl=API_BASE_URL ("/api/<version>"), so calling an admin
path through it would resolve to /api/<version>/admin-auth/ — wrong.
Fix:
- Narrow the generated `paths` interface by URL prefix
(`/admin*` vs everything else).
- Export two clients: `fetchClient` (public, baseUrl=/api/<version>)
and `adminFetchClient` (admin, baseUrl='/').
- Mirror with `$api` / `$adminApi` query hook factories.
TypeScript now rejects mixing surfaces at compile time:
fetchClient.GET('/admin-auth/') // type error: not in PublicPaths
adminFetchClient.GET('/createGroup') // type error: not in AdminPaths
Smoke test updated to assert all four exports exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): spec — feature flag + dual-client design (#7693)
Reflect Qodo-driven changes:
- /admin/openapi.json route is now feature-flagged (default off).
- admin/src/api/client.ts splits paths by URL prefix and exports two
clients (public + admin) so TypeScript rejects mixing surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): check adminOpenAPI flag per-request, not at hook setup (#7693)
The first feature-flag commit (
|
||
|
|
ac9751cb0b
|
build(deps-dev): bump the dev-dependencies group across 1 directory with 6 updates (#7706)
Bumps the dev-dependencies group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.6.0` | `25.6.2` | | [i18next](https://github.com/i18next/i18next) | `26.0.9` | `26.0.10` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.5` | `19.2.6` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.5` | `19.2.6` | | [react-i18next](https://github.com/i18next/react-i18next) | `17.0.6` | `17.0.7` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.10` | `8.0.11` | Updates `@types/node` from 25.6.0 to 25.6.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `i18next` from 26.0.9 to 26.0.10 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.0.9...v26.0.10) Updates `react` from 19.2.5 to 19.2.6 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react) Updates `react-dom` from 19.2.5 to 19.2.6 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react-dom) Updates `react-i18next` from 17.0.6 to 17.0.7 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v17.0.6...v17.0.7) Updates `vite` from 8.0.10 to 8.0.11 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.11/packages/vite) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.6.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.0.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react dependency-version: 19.2.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-dom dependency-version: 19.2.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-i18next dependency-version: 17.0.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite dependency-version: 8.0.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
c47ffd5705
|
feat(export): native DOCX export via html-to-docx (opt-in) (#7568)
* feat(export): native DOCX export via html-to-docx (opt-in) Addresses #7538. The current DOCX export path shells out to LibreOffice, which means every deployment that wants a Word download either installs soffice (~500 MB) or loses that export. This PR adds a pure-JS alternative: render the HTML via the existing exporthtml pipeline, then feed it to the `html-to-docx` library in-process to produce a valid .docx buffer — no soffice required, no subprocess spawn, no temp file dance for the DOCX case. Behavior: - `settings.nativeDocxExport` (default `false`) gates the new path so existing deployments see zero behavior change. - When enabled, `type === 'docx'` requests skip the LibreOffice branch, run `html-to-docx(html)`, and return the buffer with the `application/vnd.openxmlformats-officedocument.wordprocessingml.document` content-type. - If the native converter throws, the handler falls through to the existing LibreOffice path — so flipping the flag on is safe even on a mixed-installation where soffice is still present as a backstop. - Other export formats (pdf, odt, rtf, txt, html, etherpad) are unchanged. Files: - `src/package.json`: `html-to-docx` dep (pure JS, no binary reqs) - `src/node/handler/ExportHandler.ts`: new DOCX branch gated on the setting, with fall-through on error - `src/node/utils/Settings.ts`, `settings.json.template`, `settings.json.docker`, `doc/docker.md`: wire up the new setting + env var (`NATIVE_DOCX_EXPORT`) - `src/tests/backend/specs/export.ts`: two new tests — asserts the exported buffer is a valid ZIP (PK\x03\x04 signature) and the response carries the correct content-type — both with `settings.soffice = 'false'` to prove the path doesn't need soffice at all. Out of scope for this PR: - Native PDF export (would need a PDF rendering step — separate undertaking, and the issue acknowledges the `pdfkit`/puppeteer size trade-off). Closes #7538 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7538): skip native DOCX test when html-to-docx isn't installed The upgrade-from-latest-release CI job installs deps from the previous release's package.json (before this PR adds html-to-docx) and then git-checkouts this branch's code without re-running pnpm install. Under that one workflow the new test can't find the module and fails on the LibreOffice fallback, masking that the native path actually works in every normal install. Guard the describe block with require.resolve('html-to-docx'); Mocha's this.skip() on before cascades to the sibling its. Regular backend tests (pnpm install against this branch's lockfile) still exercise it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(7538): spec for soffice-free DOCX/PDF export and DOCX import Captures the agreed scope expansion of PR #7568: replace the flag-gated native DOCX path with a soffice-first selection cascade, add native PDF export via pdfkit + a small htmlparser2-driven walker, and add native DOCX import via mammoth. Also defines a shared HTML sanitizer (stripRemoteImages) used by both export converters to close the SSRF surface that Qodo flagged on the html-to-docx path. The spec drops the nativeDocxExport setting and its env var; with soffice configured, behavior is unchanged, and with soffice null, docx/pdf export and docx import all work in-process. odt/doc/rtf (and pdf import) keep needing soffice and are documented as such. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(7538): implementation plan for native DOCX/PDF and DOCX import Bite-sized TDD task breakdown of the soffice-free export/import work: rebase, deps, sanitizer, PDF walker, mammoth wrapper, ExportHandler cascade, route guard, ImportHandler branch, UI fix, flag rollback, verification + Qodo reply. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(7538): add pdfkit, htmlparser2, mammoth deps Pure-JS, no native binaries: - pdfkit ^0.18.0 (PDF rendering) - htmlparser2 ^12 (SAX parser used by walker + sanitizer) - mammoth ^1.12 (DOCX -> HTML for native import) - @types/pdfkit ^0.17 (dev) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): add stripRemoteImages HTML sanitizer Drops <img src=> elements pointing at non-data, non-relative URLs to prevent the DOCX/PDF converters from making outbound requests via plugin-modified HTML. Closes Qodo finding #4 against the html-to-docx path; will be wired into both export branches in the cascade refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): native PDF export via pdfkit + htmlparser2 walker Renders pad HTML to a PDF Buffer in-process: headings, paragraphs, lists, links, inline emphasis, data:-URI images. Remote images are explicitly skipped at the walker (defense-in-depth on top of the shared stripRemoteImages sanitizer). PDFs are emitted with compress:false so accessibility/SEO indexers that don't FlateDecode can still extract text. Pads are small enough that the size cost is negligible. Walker is 167 LOC, well under the spec's 500-LOC bail-out threshold for switching to pdfmake+html-to-pdfmake+jsdom. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): native DOCX import via mammoth Wraps mammoth.convertToHtml so a soffice-less Etherpad can ingest .docx files. Images are coerced to data: URIs at the converter boundary so the import pipeline never sees a remote src=. Includes a tiny generated DOCX fixture (heading, paragraph, list) under tests/backend/specs/fixtures/ for both this wrapper test and the upcoming end-to-end import test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): soffice-first cascade in ExportHandler Replaces the flag-gated DOCX branch with a deterministic dispatch: soffice if configured, native DOCX/PDF otherwise, 5xx on native error. Both native paths run plugin-modified HTML through stripRemoteImages first. Test changes: - existing native DOCX block now sets soffice=null (was 'false', a truthy non-null string that sidestepped the route guard); fixes Qodo finding #3. - new native PDF integration tests assert %PDF- header and application/pdf content-type with soffice=null. - new negative test: with soffice=null, /export/odt still returns the 'not enabled' message. - the legacy 500-on-export-error test now uses /bin/false so it exercises the soffice error path explicitly (the cascade dropped the ad-hoc 'false' string; .doc has no native path so this still works as a soffice error probe). Integration tests for native DOCX/PDF currently fail because the /export route guard still treats both formats as soffice-only; the next commit fixes that. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): allow docx/pdf through export guard without soffice Tightens the no-soffice block to ['odt','doc'] only — formats with no native path. docx and pdf are handed to ExportHandler, which dispatches to the in-process converters. Closes Qodo finding #2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): native DOCX import path in ImportHandler When soffice is null and the upload is .docx, run mammoth and feed the resulting HTML through setPadHTML. Other office formats (pdf/odt/doc/rtf) are explicitly rejected with uploadFailed instead of silently falling through to the ASCII-only path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): always show DOCX/PDF export links Native paths (#7538) make DOCX and PDF available regardless of soffice presence, so unconditionally render those links. ODT still gates on exportAvailable. Closes Qodo finding #2 on the UI side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(7538): drop nativeDocxExport flag Selection is now purely soffice-presence-driven (cascade in ExportHandler). The opt-in setting and its NATIVE_DOCX_EXPORT env var are no longer needed -- soffice configured means soffice path; soffice null means native path (DOCX, PDF, and DOCX import). Reverts the additive surface introduced earlier in this PR. Also updates the SOFFICE doc row to reflect that null no longer means 'plain text and HTML only' -- docx/pdf export and docx import now work natively without soffice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7538): tighten link annotation assertion CodeQL flagged the loose 'raw.includes("etherpad.org")' as 'incomplete URL substring sanitization' (a false positive in test context, but worth fixing). Match the full /URI (host) form instead -- it's both more accurate (we're verifying the PDF link annotation structure) and CodeQL-clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): cleaner DOCX/PDF output + round-trip test coverage DOCX: - New extractBody helper drops <head>/<style> and the leading newline inside <body> so html-to-docx doesn't render CSS or prefix paragraphs with empty space. - New wrapLooseLines pre-processor wraps loose pad lines in <p> before the converter sees them. html-to-docx renders <br> outside <p> as a new <w:p> (full empty line in Word); inside <p> it correctly emits <w:br/> (soft break). Etherpad's HTML uses bare <br> for every line, so this was making single Enters look like double Enters in the Word output. PDF: - Walker SKIP_TAGS rejects head/style/script/title/meta/link content -- prior version dumped CSS into the rendered PDF. - New breakLine() helper combines flushLine() with moveDown(1). pdfkit's text('', false) closes the continued run but does NOT advance the cursor, so consecutive runs were stacking at the same y-coordinate. <br>, end-of-block, and list items now use breakLine(). - ontext collapses runs of whitespace and drops pure-whitespace text nodes so pretty-printed source HTML doesn't render its formatting newlines. Round-trip: - New backend test: pad text -> DOCX export -> DOCX import -> new pad. Asserts content survives the trip. - New PDF sanity test: extracts visible text from the PDF stream and asserts the source pad text appears verbatim. - 6 new unit tests for extractBody and wrapLooseLines plus 1 for PDF walker SKIP_TAGS coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): CodeQL ReDoS + import.ts type error - BR_PARA_RE was /(?:\s*<br>\s*){2,}/ -- two adjacent \s* runs can match the same chars, so on '<br>\t<br>\t<br>...' the regex backtracks exponentially. Re-anchored to match a fixed first <br> followed by one or more additional <br>s, so each whitespace run has exactly one home. - import.ts: fetchBuffer was typed Promise<Buffer> but call sites chained .expect(200) on it, which only works on supertest's Test object. Return the Test (typed any) so the chain is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): plugin-aware HTML cleanup, PDF text-align, monospace ep_headings2/ep_align emit one heading-styled blank-line block after every styled line in the pad ('<h1 style=text-align:right></h1>'), which both html-to-docx and our pdfkit walker render as a full empty paragraph. Plus the pdfkit walker had no support for text-align or monospace, so right/center alignment and 'code' lines rendered the same as plain body text. - New dropEmptyBlocks helper strips empty h1-h6/p/code/pre/div/ blockquote wrappers in preprocessing. Iterates so nested empties collapse too. Applied before both DOCX and PDF conversion. - PDF walker now reads style='text-align:left|center|right|justify' on block elements (h1-6, p, div) and passes it as pdfkit's align option. align is applied once per continued run, then reset on flushLine so the next block can pick up its own value. - PDF walker handles <code>, <tt>, <kbd>, <samp> as inline monospace (Courier) and <pre> as block monospace (Courier + breakLine on open/close). 11 new unit tests: - 4 for dropEmptyBlocks (heading wrappers, code, nesting, pass-through) - 1 for PDF text-align (compares the BT matrix x for left vs right) - 2 for Courier in <code> and <pre> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): separate adjacent heading-style blocks on import Round-trip bug: ep_headings2 emits <h1>/<h2>/<code> in pad HTML. Mammoth round-trips them as adjacent <h1>A</h1><h2>B</h2><p>C</p> on import. Etherpad's server-side content collector has a default _blockElems set of just {div, p, pre, li}, and ep_headings2 only registers the CLIENT-side aceRegisterBlockElements -- not the server-side ccRegisterBlockElements. So h1/h2/code end up being treated as inline by the importer, and adjacent blocks merge into a single pad line. Fix: insert <br> after </h1>...</h6>/</code> when followed by another block. Server-side workaround keeps this PR self-contained regardless of plugin version. The right long-term fix is to extend ep_plugin_helpers' lineAttribute factory to register both hooks (filed as a follow-up). Tests: - 5 unit tests for separateAdjacentHeadingBlocks - New end-to-end round-trip test asserts H1+H2+P land on three separate pad lines after the import path. Plus the prior PDF text-align/Courier/code commit also included here: - code/tt/kbd/samp inherit text-align from style attribute - pre inherits text-align too Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): blank-line round-trip + DOCX code monospace + a==c tests DOCX round-trip dropping blank pad lines: - wrapLooseLines now emits an explicit <p></p> marker for each blank line in a <br> run, instead of collapsing all gaps into a single paragraph break. (N consecutive <br>s -> 1 paragraph boundary + N-2 empty <p></p> markers, mapping to N-1 blank pad lines.) - mammoth's docxBufferToHtml now passes ignoreEmptyParagraphs:false so the empty <w:p> entries survive the import side. mammoth's default of true was silently dropping them. - dropEmptyBlocks no longer strips <p></p> -- that's the meaningful marker for the round-trip. Empty <h1>/<code>/<pre>/<div>/ <blockquote> are still stripped (plugin noise). DOCX <code> rendering as monospace: - New applyMonospaceToCode wraps code/tt/kbd/samp/pre content in a <span style="font-family:'Courier New', monospace">. html-to-docx honors that and emits <w:rFonts w:ascii="Courier New".../>, which Word renders as Courier. The bare <code> tag is otherwise just a no-op for html-to-docx. - Applied only on the DOCX export path (PDF walker already handles monospace via Courier font selection). Round-trip tests: - New a==c suite: txt, etherpad, html, docx -- export from src, import to dst, re-export and compare against the meaningful invariant (line text for binary formats; trimmed body for HTML). - HTML test tolerates one trailing <br> per round-trip because setPadHTML appends a final <p> on import; this is pre-existing core behavior, not our bug. - DOCX test normalizes trailing newline run (same reason). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): preserve <w:jc> alignment through mammoth round-trip mammoth doesn't expose Word's paragraph alignment (`<w:jc>`) when it converts a docx to HTML -- there's no equivalent in its style-mapping machinery. To keep alignment through DOCX round-trips we walk the docx's document.xml directly, pull the `w:val` from each `<w:p>`'s `<w:jc>`, and inject `style="text-align:..."` onto the matching block element in mammoth's output by document order. Word's w:jc accepts more values than CSS text-align; we map left/ start, center, right/end, both/justify/distribute and skip the rest (start/end take left/right because we don't track ltr/rtl from the docx for now). Combines with the upstream ep_align PR (ether/ep_align#183) for the full round-trip: this PR makes the mammoth output carry the alignment style; ep_align#183 makes the importer pick it up. Closes the alignment side of #7538. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): preserve <a href> inside <code>/<pre> in DOCX export html-to-docx silently drops <a href> children of <code>/<pre> tags (and of styled <span>s, but the <code> wrapper is the active offender here). The pad-export HTML produced by ep_headings2 + ep_align uses <code style='text-align:right'>...<a href>...</a>...</code> for each 'Code'-style line, which lost its links on every DOCX export. Workaround: applyMonospaceToCode now drops the code/pre/tt/kbd/samp wrapper entirely. The non-anchor content gets wrapped in monospace spans; anchors are emitted unstyled so they keep their hyperlink. For block-level usage (<pre>, or <code> with an inline style attr) we emit a wrapping <p> and forward the text-align style. Run BEFORE wrapLooseLines so the <p> doesn't get double-wrapped. Tests added: - inline <code> -> just a styled span (no <code> wrapper) - <code style='text-align:right'> -> <p style> wrap - <pre> -> always block-wrapped - <tt>/<kbd>/<samp> -> inline span only - regression: <a href> inside <code> survives html-to-docx round-trip with both the URL in word/_rels/document.xml.rels AND a <w:hyperlink> in the document body Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): HTML import line-break drift between blocks Etherpad's HTML export wraps each pad line in <p>...</p> (or <h1>, <code>, etc.) and then appends a <br> between lines. The closing block tag already ends the line for contentcollector, so the trailing <br> is redundant -- and on import the server collector counts BOTH as line breaks, doubling every blank line between paragraphs and inserting an extra blank between adjacent headings. Two fixes, both gated on the runtime block-element registry so they don't double-trigger when the underlying plugin already handles adjacency: 1. HTML import path now runs the new collapseRedundantBrAfterBlocks helper before setPadHTML. Drops a single <br> immediately following </p>/</h1-6>/</code>/</pre>/</div>/</blockquote>/</ul>/ </ol>/</li>/</table>/</tr>/</td>/</th>. Multiple consecutive <br>s after a block keep all but the first (the rest still represent intentional blank lines). 2. The DOCX-import separateAdjacentHeadingBlocks workaround now checks whether 'h1' is in the runtime ccRegisterBlockElements set before inserting <br>s. When ep_headings2 has the new server hook (per ep_plugin_helpers#14 + the upcoming ep_headings2 PR), the workaround correctly stays out of the way -- otherwise it adds an extra blank line per heading transition. Also fixed a subtle ts-check failure on the import.ts test changes and a leftover implicit-any in ImportDocxNative's alignment preserver. Tests added: - collapseRedundantBrAfterBlocks: 5 unit tests (each block tag, whitespace tolerance, multiple <br> keeping intentional blanks) - HTML import: 'does not introduce a blank line between H1 and H2', 'preserves blank-line count between H1 and H2 (realistic shape)' reproduces the 5-blanks-where-2-expected bug from the user's round-trip pad. 1054 backend tests pass locally (the 6 failures are the pre-existing favicon/webaccess send@1.x dotfile-path issue from running under .claude/, doesn't reach CI). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7538): skip plugin-dependent HTML import tests on no-plugin CI The two new HTML-import-adjacency tests assume ep_headings2 (or another plugin) has registered h1/h2 as server-side block elements via ccRegisterBlockElements. Without that, contentcollector treats <h1>/<h2> as inline and adjacent ones merge into a single pad line -- making the assertions inapplicable. CI's backend-tests job runs without plugins installed, so guard the describe block with a runtime hooks.callAll() check and skip when h1 isn't a registered block. Local dev with ep_headings2 (and the local plugin patch wiring ccRegisterBlockElements) still exercises both tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
afee7969dd
|
fix(7696): scrollable settings popup on short viewports (#7703)
* fix(7696): make settings popup scroll on short viewports Move max-height + overflow:auto out of the mobile-only media query and onto the base .popup-content rule so the Settings popup (and other popups) gain a scrollbar instead of cropping items off-screen when the window is short. Pad-wide Settings is the worst offender because it adds a second column of controls plus a Delete pad button. Adds a Playwright regression test that verifies the popup is scrollable and the Delete pad button is reachable at a 900x500 viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7696): float nice-select dropdowns above scrollable popups Qodo flagged that making .popup-content a scroll container clips absolutely-positioned descendants — so the Settings popup's font and language dropdowns can be truncated when their list extends past the popup's scroll bounds on short viewports. Mirror the existing toolbar workaround: when a nice-select sits inside .popup-content, switch the list to position:fixed (CSS) and place it with viewport-relative coordinates from getBoundingClientRect (JS), respecting the existing reverse class for upward-opening lists. Also relax the regression test per Qodo: drop the brittle scrollHeight > clientHeight assertion in favour of asserting the popup declares overflow-y:auto and proving Delete pad is initially off-screen, then reachable via scroll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7696): nice-select reverse list disappeared in scrolled popup When a nice-select inside a popup-content scroll container sits in the lower half of the viewport, the JS adds the .reverse class so the list opens upward. The default .reverse rule sets bottom: calc(100% + 5px), which is fine when the list is position:absolute relative to its parent — but with the position:fixed treatment the popup branch uses, that percentage resolves against the viewport and pushes the list ~100vh above the screen, so it appears not to open at all until you scroll to the bottom of the popup (where .reverse no longer triggers). Override the rule for both .toolbar and .popup so .reverse drops back to bottom: auto and JS-set `top` controls placement, with a JS belt-and- braces also setting `bottom: auto` inline. Adds a Playwright regression test that scrolls the settings popup to the bottom, opens the Pad-wide font dropdown, and asserts the list is both visible and inside the viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
85c941fe95
|
feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings (#7698)
* feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings
Native pad-wide settings ride a single padOptions object: the server seeds
clientVars.initialOptions, the client mutates via pad.changePadOption(), and
the existing padoptions COLLABROOM message broadcasts changes. Plugins can't
use the same rail today because applyPadSettings (client) and
normalizePadSettings (server) silently drop any key not in their hardcoded
whitelist.
Add a passthrough loop that preserves keys matching /^ep_[a-z0-9_]+$/ on both
sides. Plugins can now stash their pad-wide values under their own namespace
(e.g. pad.padOptions.ep_table_of_contents = {enabled: true}) and inherit the
existing broadcast, persistence, creator-only-write enforcement, and
enforceSettings semantics for free.
A new src/node/utils/PluginCapabilities module exposes
padOptionsPluginPassthrough = true so plugins can feature-detect via
require() and fall back to per-user behavior on older cores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Qodo review on PR #7698
Four concerns raised by Qodo (qodo-free-for-open-source-projects):
1. Feature flag — AGENTS.MD §52 requires new features behind a flag,
disabled by default. Add `enablePluginPadOptions` (default false) gating
the passthrough on both server (normalizePadSettings) and client
(applyPadSettings, via clientVars). Plugins detect the runtime state
through clientVars.enablePluginPadOptions; the static
PluginCapabilities flag stays as the "core can do this" signal.
2. Documentation — add a "Plugin-namespaced pad-wide options" section to
doc/plugins.md covering capability detection, the runtime flag, the
key namespace pattern, and the validation rules. Mirror the flag
description in settings.json.template.
3. Unbounded payload — values for ep_* keys are persisted with the pad and
broadcast to every connected client, so an unvalidated path was a
reliability hazard. Validate every ep_* value:
- Must round-trip through JSON.stringify (rejects functions, symbols,
BigInt, circular refs).
- Per-key serialized size capped at 64 KB.
- Combined ep_* size capped at 256 KB per pad.
Rejects drop the value with a console.warn line; the rest of the pad
settings round-trip cleanly.
4. PadOption type — add `[k: \`ep_${string}\`]: unknown` index signature
so the SocketIO message type matches runtime behavior; TS callers no
longer need unsafe casts to read plugin-namespaced keys.
Also extends the backend test suite with cases covering the runtime flag
(off/on), JSON-serializability rejection, per-key cap, and total cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(snap-tests): assert_grep — use here-string to dodge pipefail SIGPIPE
`assert_grep` ran `printf '%s' "$out" | grep -q -F -- "$needle"` under
`set -o pipefail`. When grep matched early it closed its stdin, printf
got SIGPIPE on its next write (exit 141), and pipefail propagated the
broken-pipe failure to the pipeline — making `if` see non-zero and
falling into the FAIL branch even though grep itself succeeded.
Failure was timing-dependent: it only fired when `$out` was large enough
that printf hadn't flushed before grep exited. CI ubuntu-latest tipped
into the racy path on PR #7698 once `settings.json.template` grew by 11
lines (the new `enablePluginPadOptions` flag); the symptom was the
`Wrapper unit tests` step reporting `dbType rewritten to sqlite ✗` with
"got: /*…" output even though the seeded file did contain the needle.
Replace the pipe with a here-string so grep gets its input in one shot
with no pipe between processes — no SIGPIPE possible. The fail-message
`head -3` is converted to a here-string for the same reason.
Repro on a runner whose pipe-buffer flush is slower than grep's first
match would have hit the same flake on any PR; the bug isn't about
this particular template change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c7ac1cce6f
|
fix(a11y): localized aria-label + title on export-as links (#7697)
* fix(a11y): localized aria-label + title on export-as links The six export anchors in the import/export dialog had no aria-label or title; their accessible name relied on the inner icon span's translated text (e.g. "Etherpad"). That announces just the format name with no "export" verb context, and the icon span doubles as the visible glyph which screen readers also pick up. Add `data-l10n-id="pad.importExport.export<format>a.title"` to each anchor — html10n populates both the `title` attribute and `aria-label` from the same key, so screen readers announce e.g. "Export as Etherpad" and sighted users get a tooltip. Mark the inner icon span `aria-hidden="true"` so SR doesn't double-read the format name. Strengthens the existing `a11y_dialogs.spec.ts` "export links" test to assert aria-label/title are populated and inner spans are aria-hidden, and only checks the three formats that are present without soffice. * Address Qodo review - Add rel="noopener" to each export anchor: closes the reverse-tabnabbing window-opener gap. Other target="_blank" links in pad.html (e.g. the "Powered by Etherpad" link) already follow this hardening pattern. - Pin Playwright locale to en-US for the a11y_dialogs spec: this file already asserts specific English strings (e.g. "Close chat", "Active users on this pad"); without the pin, translatewiki updates would break those tests. Tighten the export-anchor assertions to exact-match the English aria-label/title now that the locale is stable, and assert rel="noopener" too. |
||
|
|
bcf5c91c15
|
Localisation updates from https://translatewiki.net. | ||
|
|
a356414254
|
fix(socialMeta): coerced numeric/boolean override silently dropped (#7692)
* fix(socialMeta): coerced numeric/boolean override silently dropped Qodo flagged on PR #7691: Settings.coerceValue() turns numeric-looking env vars into numbers and "true"/"false" into booleans, so e.g. SOCIAL_META_DESCRIPTION="2026" arrives at the resolver as the number 2026. The previous resolver gated on `typeof override === 'string'`, so it silently fell back to the i18n catalog with no warning — the operator's docker config would appear broken. Accept string|number|boolean and stringify before the empty-check; null / undefined / unsupported types still fall through to the catalog. Two new unit specs cover the numeric and boolean coercion paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(socialMeta): widen description type to match coerced runtime Action Qodo bug-find: SettingsType.socialMeta.description and SocialMetaSettings.description still claimed `string | null`, but Settings.coerceValue() can produce number|boolean from env-var-driven config — the previous resolver fix was correct at runtime but the type mismatch forced `as unknown as string` casts in the new tests. Widen both declared types to `string | number | boolean | null` to match runtime reality, drop the typeof string|number|boolean guards in the resolver (the union now narrows automatically) and remove the test casts. Behaviour is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |