feat: Backend esm vitest#5
Open
deepshekhardas wants to merge 435 commits into
Open
Conversation
Bumps [axios](https://github.com/axios/axios) from 1.15.1 to 1.15.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](axios/axios@v1.15.1...v1.15.2) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.2 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>
…ith 9 updates (ether#7579) Bumps the dev-dependencies group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [eslint](https://github.com/eslint/eslint) | `10.2.0` | `10.2.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.4` | `4.1.5` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.58.2` | `8.59.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.58.2` | `8.59.0` | | [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `7.1.0` | `7.1.1` | | [i18next](https://github.com/i18next/i18next) | `26.0.5` | `26.0.6` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.72.1` | `7.73.1` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.14.1` | `7.14.2` | | [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) | `4.0.1` | `4.1.0` | Updates `eslint` from 10.2.0 to 10.2.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.2.0...v10.2.1) Updates `vitest` from 4.1.4 to 4.1.5 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest) Updates `@typescript-eslint/eslint-plugin` from 8.58.2 to 8.59.0 - [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.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.58.2 to 8.59.0 - [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.0/packages/parser) Updates `eslint-plugin-react-hooks` from 7.1.0 to 7.1.1 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks) Updates `i18next` from 26.0.5 to 26.0.6 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](i18next/i18next@v26.0.5...v26.0.6) Updates `react-hook-form` from 7.72.1 to 7.73.1 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](react-hook-form/react-hook-form@v7.72.1...v7.73.1) Updates `react-router-dom` from 7.14.1 to 7.14.2 - [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.14.2/packages/react-router-dom) Updates `vite-plugin-static-copy` from 4.0.1 to 4.1.0 - [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases) - [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md) - [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@4.0.1...vite-plugin-static-copy@4.1.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vitest dependency-version: 4.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.59.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.59.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: eslint-plugin-react-hooks dependency-version: 7.1.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.0.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-hook-form dependency-version: 7.73.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-router-dom dependency-version: 7.14.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite-plugin-static-copy dependency-version: 4.1.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>
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.1 to 3.22.2. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](sidorares/node-mysql2@v3.22.1...v3.22.2) --- updated-dependencies: - dependency-name: mysql2 dependency-version: 3.22.2 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>
…ther#7559) * feat(packaging): add Debian (.deb) build via nfpm with systemd unit First-class Debian packaging for Etherpad, producing signed-ready etherpad-lite_<version>_<arch>.deb artefacts for amd64 and arm64 from a single nfpm manifest. Installing the package gives users: - /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no pnpm required at runtime, just `nodejs (>= 20)`. - etherpad system user/group, created via `adduser` in preinst. - /etc/etherpad-lite/settings.json seeded from the template on first install, preserved across upgrades, removed on `purge`. - /var/lib/etherpad-lite owned by etherpad:etherpad, with the default dirty-DB retargeted there so ProtectSystem=strict works. - /lib/systemd/system/etherpad-lite.service — hardened unit (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies) with Restart=on-failure. - /usr/bin/etherpad-lite CLI wrapper running `node --import tsx/esm`. CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both arches via native runners (ubuntu-latest + ubuntu-24.04-arm), smoke-tests the amd64 package end-to-end (install → systemctl start → curl /health → purge → confirm user removed), and attaches the artefacts to the GitHub Release. Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted reprepro) is intentionally out of scope — needs a governance decision on who holds the signing key. Recipes are documented in packaging/README.md. Refs ether#7529 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(deb): fail smoke test on /health timeout, tighten default-file perms, 2-space indent Addresses Qodo review feedback on ether#7559: 1. Smoke test false-positive: the `for` loop polling /health never failed the job if the endpoint stayed down — `curl && break || sleep 2` keeps returning 0 from the trailing `sleep`, so `set -e` never trips. CI could attach a broken .deb to a release. Fix: track success explicitly and exit 1 (plus dump journald logs for diagnostics) when the service never becomes healthy. 2. /etc/default/etherpad-lite was world-readable (0644). systemd loads it via `EnvironmentFile=…`, and Etherpad supports ${ENV_VAR}-substitution for secrets (DB_PASSWORD etc.), so any local user could read anything admins drop there. Fix: install the conffile as root:etherpad 0640 — only root and the service user can read it. 3. Indentation: reflow maintainer scripts from 4-space to 2-space to match the repo style rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d unit (…" (ether#7582) This reverts commit 6bb879e.
) * fix(editor): preserve U+00A0 non-breaking space (ether#3037) Non-breaking spaces were silently normalized to regular spaces at every ingestion point, so typed/pasted/imported nbsps never reached the changeset and users could not glue words against line-wrap in French or other languages that require nbsp typography. Removed the four strip sites that replaced U+00A0 with U+0020: - src/node/db/Pad.ts cleanText - src/static/js/contentcollector.ts textify - src/static/js/ace2_inner.ts textify - src/static/js/ace2_inner.ts importText raw-text guard Updated both processSpaces functions (domline and ExportHtml) to tokenize U+00A0 as a separate unit, emit it verbatim as , and treat it as content (not whitespace) for the run-collapse bookkeeping so adjacent regular-space runs aren't miscounted. Added backend round-trip tests for spliceText and setText, and extended the cleanText case table. Updated the existing contentcollector and importexport specs whose expectations encoded the previous buggy behavior; they now assert genuine nbsp preservation. Verified manually in Firefox: clipboard U+00A0 → paste → pad → getText returns c2 a0; getHTML emits `100 km`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(contentcollector): collapse display-artifact nbsp runs on DOM read-back processSpaces is a lossy one-way display transform: leading/trailing spaces and all-but-the-last of a run get rendered as so HTML doesn't collapse them. When incorporateUserChanges reads text back from the DOM, those display-artifact nbsps were being stored in the changeset model instead of being normalized back to plain spaces. This broke handleReturnIndentation, whose /^ *(?:)/ regex only matches ASCII spaces: auto-indent after `foo:\n` produced 4 spaces instead of the expected prev-indent (2) + THE_TAB (4) = 6, because the previous line's model had nbsps where it used to have spaces. Fix: in contentcollector.textify, collapse any [ ]+ run back to plain spaces UNLESS the run is pure U+00A0 AND strictly interior to word chars. That preserves user-intended typographic nbsps like "100 km" while undoing the one-way display transform. Updated 7 contentcollector tests and 7 importexport tests whose assertions needed to reflect the new rule (boundary/mixed runs collapse; pure-interior nbsp runs preserve). Fixes the Playwright regression in indentation.spec.ts:117 that the previous commit introduced. * fix(contentcollector): canonicalize nbsp runs at line assembly, not per text node Addresses Qodo code review feedback on PR ether#7585. ## Bug fix — nbsp lost at DOM text-node boundary The previous approach ran the "collapse display-artifact nbsp" rule inside textify(), which is called per individual DOM TEXT_NODE. A user-intended nbsp sitting at a text-node boundary (e.g., <span>100</span><span> km </span>) was incorrectly seen as non-interior (before === '' for the second text node) and normalized back to a regular space. Fix: move the canonicalization out of textify() and run it on each fully assembled line string inside cc.finish(). The rule remains: [ ]+ run -> plain spaces UNLESS pure U+00A0 AND strictly interior to non-ws chars It is length-preserving, so attribute offsets and line lengths are unaffected. Added a regression test (contentcollector.spec.ts) for the cross-span case. ## Docs concern Reverted the type-only addition of spliceText to PadType. spliceText is an existing Pad runtime method; the backend test now uses a cast (`(pad as any).spliceText`) so the PR does not expand the declared public type surface, avoiding a separate documentation requirement. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tization (ether#7587) PadMessageHandler built the `pluginsSanitized` payload for clientVars by aliasing `plugins.plugins` and then mutating each entry's `package` field in place: let pluginsSanitized: any = plugins.plugins; Object.keys(plugins.plugins).forEach(function(element) { const p: any = plugins.plugins[element].package; pluginsSanitized[element].package = {name: p.name, version: p.version}; }); Because `pluginsSanitized` is a reference to `plugins.plugins`, the assignment clobbered the server-side plugin registry. After the first pad connection, every plugin's `package` object held only `{name, version}` — `realPath`, `path`, and `location` were gone. Minify.ts resolves `/static/plugins/ep_*/...` URLs via `plugin.package.realPath`. Once the field disappeared, every subsequent static asset request for a bundled plugin 500'd with: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined at Object.join (node:path:1354:7) at _minify (src/node/utils/Minify.ts:181:23) Symptoms on Chromium: plugin CSS/JS assets fail to load (e.g. /static/plugins/ep_font_size/static/css/size.css returns 500), so plugins partially render or don't work at all. Firefox swallows the resulting console errors quietly. Fix: extract the sanitization into a pure helper `sanitizePluginsForWire` that returns a fresh object graph and never touches the input. The helper is covered by a new backend spec that: * verifies the sanitized output has only {name, version} in `package` * asserts the input registry's realPath/path/location survive the call * runs the call repeatedly and confirms non-destructiveness * mutates the returned copy and asserts the input is independent Verified live with the dev server: before the fix, `/static/plugins/ ep_font_size/static/css/size.css` 500'd after visiting any pad; after the fix it returns 200 both before and after pad connections.
ether#7586) (ether#7588) `Pad.normalizePadSettings()` was defaulting `lang` to the literal string 'en' when `rawPadSettings.lang` was not a string. That value flowed into `clientVars.padOptions.lang` and then into `getParams()` in pad.ts, which calls `html10n.localize([serverValue, 'en'])` as a callback for the `lang` setting. The result: every pad forced English on load, overriding the browser's Accept-Language and the existing auto-detect chain in l10n.ts (cookie -> navigator.language -> 'en'). The regression was introduced in ether#7545 ("Add creator-owned pad settings defaults", commit e0ccdb4). 2.6.1 did not have this default, so auto-detect worked there. 2.7.0 broke it. Fix: default `lang` to null. The client's existing flow already handles null correctly — getParams() at pad.ts:172 has `if (serverValue == null) continue;`, so the forced-localize callback simply does not fire, and l10n.ts's browser-language auto-detect runs. Pad-settings dropdown consumer at pad.ts:489 already uses `padOptions.lang || 'en'` so null renders fine there too. `PadSettings.lang` is now typed `string | null` to match. Added three backend regression tests under `normalizePadSettings lang`: * defaults to null when lang is absent (so client auto-detects) * preserves an explicit string lang (creator override still works) * drops non-string lang values to null rather than coercing to 'en' Manual verification: with Firefox set to German, loading a fresh pad now renders the UI in German. Index and timeslider continued to work as before. Setting `?lang=de` or a language cookie continues to override browser detection, as intended. Fixes ether#7586
…ether#7584) * fix(a11y): negotiate lang/dir per request and set on <html> Server-renders the html element with `lang` and `dir` matching the client's Accept-Language header (negotiated against availableLangs from i18n hooks). Falls back to `en`/`ltr` if no match. This gives screen readers a correct document language during the brief window before client-side html10n refines it (l10n.ts already sets both attributes after locale data loads). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): dialog semantics on popups; fix aria-role typo on userlist Adds role=dialog, aria-modal=true, and either aria-labelledby (when an h1 is present) or aria-label (for popups without an h1) to: - #settings, #import_export, #embed, #skin-variants (labelledby) - #connectivity, #users, #mycolorpicker (aria-label) Fixes the invalid aria-role="document" attribute on #otherusers; it's now role=region with aria-live=polite so screen readers announce collaborator joins/leaves. Container aria-label values are English-only for now — Etherpad's html10n implementation only supports localizing specific attributes (title, alt, placeholder, etc), not aria-label on container nodes. Localization can follow once html10n grows that affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): focus management and Escape-to-close for popups Three additions to toggleDropDown / _bodyKeyEvent: - Remember the trigger element (document.activeElement) when opening a popup, so we can restore focus when it closes. - On open, focus the first focusable element inside the popup so keyboard users land inside the dialog instead of staying on the trigger button. - Escape pressed while focus is inside a popup closes it, then the restore-focus path runs and the trigger button is refocused. Replaces the previous behavior where Escape from inside a popup did nothing; users had to click outside to dismiss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): make chaticon and chat header controls real buttons - #chaticon: <div onclick> → <button type=button> with aria-label - #titlecross / #titlesticky: <a onClick> → <button type=button> with aria-label (Close chat / Pin chat to screen) - Decorative chat-bubble glyph gets aria-hidden=true so it isn't read alongside the button label - #chatcounter labelled "Unread messages" - Inline onclick attributes moved to chat.init() handlers - CSS reset on the new buttons (transparent bg, no border, inherit font/color) so they match the prior visual design - :focus-visible outlines for keyboard users Existing test selectors (#chaticon, #titlecross, #titlesticky) are unchanged and continue to work — they never relied on element type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): accessible names for icon-only toolbar/export controls - Export links (#exportetherpada, #exporthtmla, #exportplaina, #exportworda, #exportpdfa, #exportopena): added aria-label so the link is announced as e.g. "Export as PDF". The inner icon span gets aria-hidden=true so screen readers don't read both the icon text and the link label. - Show-more toolbar toggle (.show-more-icon-btn): converted from <span> to <button type=button> with aria-label and aria-expanded. The click handler now toggles aria-expanded alongside the full-icons class so assistive tech reflects the open/closed state. - Theme switcher knob: aria-label changed from "theme-switcher-knob" (a class-style identifier, not human text) to "Toggle theme". Aria-label values are English-only for now. Etherpad's html10n implementation only localizes a fixed attribute list (title, alt, placeholder, value, innerHTML, textContent); aria-label is not included, so a clean l10n path requires a follow-up to either extend html10n or set aria-label client-side after locale loads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(a11y): cover dialog semantics, html lang, icon button labels New Playwright spec verifies the a11y guarantees added by this branch: - <html> has a non-empty lang attribute - settings/import_export/embed/users popups expose role=dialog, aria-modal=true, and either aria-labelledby (when an h1 exists) or aria-label (when none does) - Escape from inside the settings popup closes it AND restores focus to the trigger button - Export links each carry a descriptive aria-label - #chaticon is a real <button> with aria-label - #titlecross / #titlesticky are real <button>s with aria-label - #otherusers uses role=region + aria-live=polite + aria-label (and the previous aria-role typo is gone) - .show-more-icon-btn is a <button> with aria-label and aria-expanded Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): address Qodo review feedback from PR ether#7584 1. Users Escape close broken - toggleDropDown('none') intentionally skips the users module so switching between other popups doesn't hide the user list. That meant Escape couldn't dismiss the Users popup either. The Escape branch now checks for #users as the focused popup and closes it explicitly (respecting stickyUsers) before falling through to the normal close-all path. 2. Embed focus overridden - the rAF auto-focus in toggleDropDown grabbed the first focusable descendant, which stole focus from command handlers that target a specific control (notably the Embed command's #linkinput). rAF now bails out if focus is already inside the newly-opened popup. 3. Button click blurs :focus before toggleDropDown captures trigger - discovered while investigating the Firefox Playwright failure for "settings popup Escape restores focus". Button.bind() calls $(':focus').trigger('blur') before invoking the callback, so by the time toggleDropDown() captured document.activeElement as the restore target it was already <body>. The click handler now stashes padeditbar._lastTrigger to the clicked <button> before blur runs; toggleDropDown only falls back to activeElement when the pre-stash didn't happen (keyboard shortcut path). 4. html10n overwrites aria-label - html10n unconditionally set aria-label to the translated string, clobbering explicit aria-label on elements that also carry data-l10n-id. setAttribute now only fires when the element has no aria-label; explicit author labels win, unlabelled translated elements still get a name. 5. Button visual reset - the show-more-icon-btn and #chaticon conversions inherited UA default button border/background/padding, shifting icon glyphs visibly off-centre. Added appearance / background / border / padding resets. 6. Export links test assumes soffice is installed - #exportworda, #exportpdfa, #exportopena are removed client-side by pad_impexp.ts when clientVars.exportAvailable === 'no'. The test now skips links absent at runtime. Verified locally: all 10 a11y_dialogs specs pass on both Chromium and Firefox; backend suite remains 799/799 passing; ts-check clean. * fix(a11y): close popups with no focusable content; unbreak chat-icon layout Round 2 of ether#7584 review follow-ups. 1. Users popup Escape still didn't close the dialog (user-confirmed). Root cause: _bodyKeyEvent is bound to the OUTER document's body. When #users opens, the command handler tries to focus #myusernameedit but that input is `disabled`, so focus stays in the ace editor iframe. Keydown from inside the iframe does not bubble to the outer document, so Esc never reaches _bodyKeyEvent. Fix: in the open-popup rAF, if no command handler placed focus inside the dialog, focus the popup div itself (with tabindex=-1). That keeps subsequent keydown events on the outer document so Esc can dismiss the popup. Also broadened the Esc branch to fire whenever any popup is `.popup-show`, regardless of where :focus lives — some popups legitimately have no focusable content at open. Added a regression test that opens #users and asserts Esc closes it. Passes on both Chromium and Firefox. 2. Chat icon (#chaticon) visual still wrong after the first CSS fix. - My previous `border: 0` reset was overriding the intended `border: 1px solid #ccc; border-bottom: none` from the earlier rule. Removed `border: 0`; the earlier explicit border suffices to suppress UA defaults. - The `<span class="buttonicon">` inside `#chaticon` was picking up the global `.buttonicon { display: flex; }` rule meant for toolbar button instances, which broke the inline layout of the label + glyph + counter row. Added a scoped `#chaticon .buttonicon { display: inline; }` override. All 11 a11y_dialogs specs pass on Chromium and Firefox. Backend suite and ts-check remain clean. * fix(a11y): only stash _lastTrigger for dropdown-opening buttons Round 3 follow-up. The previous Button.bind() change stashed every clicked toolbar button as padeditbar._lastTrigger before blurring :focus. That was necessary for popup-opening buttons (settings, import_export, etc.) so Escape could return focus to them — but it also fired for non-popup toolbar buttons (list toggles, bold/italic, indent/outdent, clearauthorship). For those, the stash held a stale reference that interfered with subsequent editor interactions and regressed Playwright tests: ordered_list, unordered_list, undo_clear_authorship. Fix: only stash when the clicked command is a registered dropdown (settings, import_export, embed, showusers, savedrevision, connectivity). Other commands return focus to the ace editor as before and leave _lastTrigger alone. Verified locally on Chromium: - ordered_list.spec.ts: 6/6 pass (was 4/6) - unordered_list.spec.ts: 6/6 pass (was 4/6) - undo_clear_authorship.spec.ts: 2/2 pass (was 0/2) - a11y_dialogs.spec.ts: 11/11 pass (unchanged) * fix(a11y): address Qodo review round 4 for PR ether#7584 #1 Stale aria-label after relocalize html10n.translateNode() refused to overwrite any existing aria-label, which also skipped updates on language change (pad.applyLanguage() re-runs localize). Use a `data-l10n-aria-label="true"` marker: set aria-label + marker when html10n populates it, overwrite only if the marker is present. Explicit template-supplied aria-labels stay as-is; html10n-generated ones refresh on relocalize. #2 Escape won't close colorpicker _bodyKeyEvent caught Escape on any `.popup.popup-show` but only closed dropdown popups via toggleDropDown('none'). Popups opened outside the editbar framework (#mycolorpicker, toggled directly by pad_userlist.ts) stayed open while preventDefault() swallowed the key. Now the Escape branch manually closes any popup that toggleDropDown('none') cannot reach (non-dropdown ids, plus #users unless pinned) and leaves registered dropdowns for toggleDropDown to close so its focus-restore sees the transition. #3 Stale focus restoration toggleDropDown('none') restored focus to _lastTrigger even when no popup was open on entry, which meant background callers (connectivity setup, periodic state handling) could yank focus out of the editor to a stale toolbar button. Gated the restore on `wasAnyOpen === true` so it only fires when there was a popup to close. ether#11 English aria-label overrides i18n (export links, chat icon) Removed the hard-coded English aria-label from export anchors and removed aria-hidden from their inner localized spans. Screen readers now get the localized child text as the accessible name (Etherpad, HTML, PDF, etc.), matching the visible UI language. Removed the English aria-label from #chaticon and #titlesticky as well — both have data-l10n-id, so html10n populates a localized aria-label via the marker mechanism in #1. #titlecross keeps its static aria-label because it has no data-l10n-id yet. #4 4-space indent in a11y spec Two tests had continuation lines at 4-space indent violating the repo's 2-space rule. Folded the signatures onto one line. Updated a11y_dialogs.spec.ts to assert accessible-name presence rather than hard-coded English for elements whose names now come from the localized text. Still asserts static English for #titlecross (not localized yet). Verified locally (dev server restarted for each round): - a11y_dialogs.spec.ts: 11/11 on Chromium, 11/11 on Firefox - ordered_list + unordered_list + undo_clear_authorship: 13/13 on Chromium - Full backend suite: 799 passing, 0 failing - tsc --noEmit clean in our code ether#9 Popup behavior documentation: deferred to a follow-up doc PR so this PR stays focused on the a11y code changes. The new keyboard behavior (Escape-to-close, focus-restore-to-trigger) is small enough to summarize in a short doc/ addition. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.3.2 to 8.4.0. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](express-rate-limit/express-rate-limit@v8.3.2...v8.4.0) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.4.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>
Bumps the dev-dependencies group with 1 update: [i18next](https://github.com/i18next/i18next). Updates `i18next` from 26.0.6 to 26.0.7 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](i18next/i18next@v26.0.6...v26.0.7) --- updated-dependencies: - dependency-name: i18next dependency-version: 26.0.7 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>
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.3.1 to 12.5.0. - [Release notes](https://github.com/tediousjs/node-mssql/releases) - [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt) - [Commits](tediousjs/node-mssql@v12.3.1...v12.5.0) --- updated-dependencies: - dependency-name: mssql dependency-version: 12.5.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>
…er#7600) Bumps the dev-dependencies group with 1 update: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react). Updates `lucide-react` from 1.8.0 to 1.11.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.11.0/packages/lucide-react) --- updated-dependencies: - dependency-name: lucide-react dependency-version: 1.11.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>
* fix(admin): restore i18n on /admin by copying locales to the right path The admin SPA fetches `/admin/locales/<lang>.json`. Building with vite-plugin-static-copy and `src: '../src/locales'` was placing the 115 core locale files at `src/templates/admin/src/locales/` (the plugin's `dirClean` strips a leading `../` but keeps the remaining parent path). The express admin handler 404'd those fetches, fell back to serving `index.html`, JSON.parse silently failed, and every `<Trans>` rendered its raw key — see ether#7586. Replace the plugin with a small inline build/dev plugin: at build time copy `src/locales/*.json` to `<outDir>/locales/`; in dev serve the same files via middleware so `vite dev` also works. Drop the now-unused `vite-plugin-static-copy` dependency. Add regression coverage that none of the existing admin specs had: - backend HTTP test for GET /admin/locales/{en,de}.json - Playwright admin i18n spec asserting translated <h1> renders for the default locale and for ?lng=de, plus a request-level check that the response is JSON, not the SPA fallback. Closes ether#7586 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(admin): bundle locales via import.meta.glob, drop copy plugin The first pass at ether#7586 replaced vite-plugin-static-copy with a custom build/dev plugin that copied src/locales/*.json into the admin output and served them in dev. That works, but the vite-plugin-static-copy README explicitly recommends the public directory or a JS import for this case, and the import path is strictly cleaner: no copy step, no /admin/locales/* express route, no SPA-fallback-shaped failure mode. Use import.meta.glob in admin/src/localization/i18n.ts so each language ships as its own hashed JSON chunk and is lazy-loaded on demand. The vite config goes back to just react + base + outDir. The plugin namespaces (e.g. ep_admin_pads) keep their existing admin/public/<ns>/<lang>.json layout. Tests: - Drop tests/backend/specs/adminLocales.ts — it asserted on a /admin/locales/<lang>.json route that this approach no longer uses; the regression mechanism it pinned doesn't exist anymore and the test required the admin frontend to be built before the backend test runs (which CI doesn't do). - Keep tests/frontend-new/admin-spec/admini18n.spec.ts (rendered <h1> in default and ?lng=de). Verified red→green: reverting just the loader to the pre-fix /admin/locales fetch makes both specs fail; restoring makes them pass. Also update pnpm-lock.yaml to drop the now-unused vite-plugin-static-copy entries — fixes ERR_PNPM_OUTDATED_LOCKFILE that was failing every CI install upfront. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#7589) (ether#7598) After picking a value from a toolbar <select> (ep_headings style picker is the canonical case), keyboard focus was left on the nice-select wrapper rather than returned to the pad editor. Users had to click back into the pad before typing resumed. The ToolbarItem.bind() class already calls padeditor.ace.focus() at the end of triggerCommand, but that only runs for selects wired via data-key on the wrapping <li>. Plugin-provided selects (e.g. ep_headings2's #heading-selection inside <li id="headings">, no data-key) don't go through that path — they bind their own change handler and never return focus. Fix: add a delegated change handler on `#editbar select` that calls padeditor.ace.focus() after any toolbar select change. Deferred via setTimeout(0) so plugin change handlers (bound on the same event) complete their ace.callWithAce work before focus moves. Redundant but harmless for data-key-wired selects that are already refocused by triggerCommand. Added a Playwright regression test that simulates the nice-select option-click (val + change, which is what the wrapper dispatches internally) and verifies typing after the change lands in the pad. Skips when ep_headings2 isn't installed. Closes ether#7589.
) * fix: page down/up now scrolls by viewport height, not line count The previous implementation counted logical lines in the viewport, which failed when long wrapped lines consumed the entire viewport. Now scrolls by actual pixel height for correct behavior. Fixes ether#4562 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use outerDoc instead of outerWin.document for viewport height in PageDown/Up outerWin is an HTMLIFrameElement (returned by getElementsByName), not a Window object, so it has no .document property. The existing getInnerHeight() helper already uses outerDoc.documentElement.clientHeight correctly; align the PageDown/PageUp handler with that pattern. Adds a Playwright regression test that verifies PageDown scrolls the viewport when the pad contains long wrapping lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rewrite page down/up to use pixel-based line counting The previous approach tried to scroll the outerWin iframe element directly which didn't work. Reverted to the original cursor-movement approach but calculates lines-to-skip using viewport pixel height divided by actual rendered line heights. This correctly handles long wrapped lines that consume multiple visual rows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore getInnerHeight + inclusive range fixes lost in rebase Recover the PageDown/Up fixes that got dropped when this branch was rebased onto develop: - Use getInnerHeight() instead of outerDoc.documentElement.clientHeight so hidden-iframe and Opera edge cases are handled the same as the rest of the editor. - scroll.getVisibleLineRange() returns an inclusive end index, so count (end - start + 1) logical lines to match the pixel-sum loop bounds. - Replace the flaky 'PageDown scrolls viewport' test with the robust ether#4562 regression that builds long wrapped lines via direct DOM and asserts the caret advances on successive PageDown presses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r#7562) * fix(editor): undo/redo scrolls the viewport to follow the caret Before: on a large pad, pressing Ctrl+Z (or Ctrl+Y, or the toolbar undo button) updated the caret in the rep model and the DOM, but the viewport did not follow when the caret landed below the visible area. The user was left looking at the same scroll position while their change had been undone somewhere they couldn't see. Root cause: scroll.ts's `caretIsBelowOfViewport` branch ran `outer.scrollTo(0, outer[0].innerHeight)` — a fixed offset equal to the inner iframe's height, NOT the caret position. That was a special-case added in PR ether#4639 to keep the caret visible when the user pressed Enter at the very end of the pad. It worked for that one scenario because the newly-appended `<div>` happened to be at the bottom of the pad too; for any other way of putting the caret below the viewport (undo, redo, programmatic selection change, deletion that collapsed a long block) it scrolled to an arbitrary spot. Fix: mirror the `caretIsAboveOfViewport` branch. After the deferred render settles, recompute the caret's position relative to the viewport and scroll by exactly the delta needed to bring the caret back in — plus the configured margin. The Enter-at-last-line case still works because the caret genuinely is near the bottom of the pad and the delta resolves to "scroll down by a screen". Closes ether#7007 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7007): use real typing so undo has changesets to replay The first iteration of the Playwright spec built the pad by writing directly to #innerdocbody.innerHTML. That bypasses Etherpad's text layer, so the undo module had no changeset to revert — Ctrl+Z became a no-op and the scroll assertion saw no movement (CI failure output: `Expected: < 2302, Received: 2302`). Replace with real keyboard typing of 45 lines via the existing writeToPad-style pattern, then make the edit + scroll + Ctrl+Z under that real content. Slower (~5s per test) but faithful to how undo interacts with the pad. Also drop the `test.beforeEach(clearCookies)` scaffolding — it wasn't doing anything useful here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(7007): scroll caret into view directly in doUndoRedo Revert the scroll.ts rewrite from the previous commits and move the fix to the right abstraction layer: the undo/redo entry point itself. `scrollNodeVerticallyIntoView`'s caret-below-viewport branch has a well-documented special case (PR ether#4639) that scrolls to the inner iframe's innerHeight so Enter-on-last-line stays smooth. Changing that function for the undo case risked regressing the Enter case or racing with the existing scrollY bookkeeping. The CI run showed the rewrite wasn't actually producing viewport movement. Do the simpler thing instead: in `doUndoRedo`, after the selection is updated, call `Element.scrollIntoView({block: "center"})` on the caret's line node. That's browser-native, works inside the ace_inner / ace_outer iframe chain, doesn't need setTimeout, and matches what gedit/libreoffice do. Closes ether#7007 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes ether#5071. `/p/:pad/:rev/export/etherpad` has always ignored the rev parameter and returned the full pad history, unlike the txt/html export endpoints which use the same route but do respect rev. Users wanting to back up or inspect a snapshot of a pad at a specific rev got every later revision in the payload instead — both wasteful and a surprise when the downloaded .etherpad blob contained content that had supposedly been reverted. Change: - `exportEtherpad.getPadRaw(padId, readOnlyId, revNum?)` now takes an optional revNum. When supplied, it clamps to `min(revNum, pad.head)`, iterates only revs 0..effectiveHead, and ships a shallow-cloned pad object whose `head` and `atext` reflect the requested snapshot. The original live Pad is still passed to the `exportEtherpad` hook so plugin callbacks see the real document. - `ExportHandler` passes `req.params.rev` through on the `etherpad` type, matching the existing behavior of `txt` and `html`. - Chat history is intentionally left full (it is not rev-anchored). Adds three backend regression tests under `ExportEtherpad.ts`: - default (no revNum) still exports the full history - explicit revNum limits exported revs and rewrites the serialized head so re-import reconstructs the pad at that rev - revNum above head is treated as full history, preventing accidental truncation of short pads Out of scope: `getHTML(padID, rev)` on the API side is already honoring rev in current code (exportHtml.getPadHTML threads the parameter through), so the earlier report on that API call appears to be resolved. This PR does not touch it. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ther#7592, ether#7593) (ether#7597) * fix(userlist): stop username input from overlapping the Log out button Fixes ether#7593. In the pad's Users popup, #myusernameform had no width set and the <input id="myusernameedit"> inside it took its natural content width, pushing past the Log out button and making the button overflow the popup at common widths. Constrain #myusernameform to 75px and make the input fill its container with box-sizing: border-box so the text field stays inside the form and the Log out button sits visibly next to it rather than getting covered or clipped off-screen. Low-risk, CSS-only change. No test plan beyond visual verification because the affected control is in the users popup UI. * fix(chat): bottom-align titlebar controls; restore chat icon click (ether#7590) Two regressions from the ether#7584 a11y refactor of the chat widget, both pure-CSS fixes scoped to the chat panel. 1. Title bar — `<a>` → `<button>` for #titlecross/#titlesticky kept the `float: right` layout, but a `<button>`'s box is only as tall as its glyph, so the small `−` and `█` controls floated at the *top* of the 44px title bar instead of sitting on the title's baseline as the anchors did. Switch #titlebar to a flex row with `align-items: flex-end`, give #titlelabel `flex: 1` to push the controls to the right edge, and use `order: 1/2` to keep the historical visual order `[█] [−]` (which `float: right` previously produced from reverse source order). 2. Chat-icon corner widget — `<div>` → `<button id="chaticon">` exposes the inner `<span class="buttonicon">` to the global `.buttonicon` rule's `display: flex; position: relative; align-items/justify-content: center;`. The existing override only reset `display`, leaving the span as a positioned flex item that, in some layouts, sat over the button's hit surface and swallowed clicks. Reset the remaining flex properties and add `pointer-events: none` so clicks always reach the `<button>`'s own click handler — preferred over weakening the global .buttonicon rule, which the toolbar relies on for icon centring. Visual-only / behaviour-fix, no markup or JS changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(settings): grey disabled chat option labels (ether#7592) When "Disable chat" is ticked in the Settings dialog, refreshMyViewControls() already sets `disabled` on `#options-stickychat` and `#options-chatandusers`, but the browser only greys the checkbox itself — the adjacent `<label>` keeps its normal colour, so the row still looks interactive even though clicks are no-ops. Add a popup-scoped rule that follows the existing convention used for disabled `.nice-select` controls (`color: ether#999; cursor: not-allowed`) so any disabled checkbox or radio in a settings popup matches its label to the disabled state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(userlist): drop username input width cap (ether#7593 review) The width:75px on #myusernameform and width:100%/box-sizing on #myusernameedit from a55436c were guarding against an overlap with a "Log out" button — but no Log out button exists in vanilla etherpad-lite (the original report came from a setup with a plugin that adds one). Without that button visible, the cap just makes the default username field unnecessarily narrow. Restore #myusernameform to just `margin-left: 10px` and drop the forced width on the input. If the overlap reappears in a real plugin setup it should be re-fixed there (or with a more targeted rule that only kicks in when a logout button is actually present). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): keep titlesticky at top of title bar (ether#7590 review) The previous pass bottom-aligned both corner controls via align-items: flex-end on #titlebar. That correctly placed the close button (#titlecross) on the title's baseline, but it also dragged the much smaller "stick to screen" button (#titlesticky) down to the same baseline — visibly far below where it sat in the original layout. Switch to per-control align-self so each lands where it should: - #titlesticky → align-self: flex-start (top, where it always was) - #titlecross → align-self: flex-end (bottom, on the title's baseline) - #titlelabel → align-self: center (don't stretch the heading) Drop align-items from #titlebar so the defaults don't override these. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(chat): restore original #titlebar layout (ether#7590 review) Both attempted CSS layouts for the title bar (full flex with align-items: flex-end, then per-control align-self) ended up looking worse than the original in review. Drop all the #titlebar / #titlelabel / #titlecross / #titlesticky changes from 905294d and f37da9a and restore the pre-existing float-based layout. The chat panel ships with its original visuals; we'll revisit ether#7590 separately if needed. Keeps the chat-icon click fix from 905294d (#chaticon .buttonicon flex/pointer-events reset) and the focus-visible additions for the title-bar buttons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): clear inline display:none in chat.show() When the user disables chat in settings, applyShowChat(false) calls \`$('#chatbox').hide()\` which sets the chatbox's inline display to \`none\`. Re-enabling chat doesn't undo that — it only re-shows the icon. Then clicking the icon runs chat.show(), which adds the \`.visible\` class but only flips visibility, not display, so the chatbox stays hidden by the lingering inline style and the chat appears not to open. Clear the inline display in chat.show() before adding the .visible class so the box becomes visible regardless of how it got hidden. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(colibris): align username gap; grey unchecked-disabled toggles users.css: change #myusernameform margin-left from 35px to 10px to match the base popup_users.css. The 35px value was chosen for the sticky chatAndUsers layout, but for the standalone Users popup it opens an unnecessarily wide gap between the colour swatch and the username field. (ether#7593 review) form.css: drop the \`:checked\` qualifier from the disabled toggle visual rule so unchecked-but-disabled toggles also dim. Without this, "Chat always on screen" / "Show Chat and Users" stayed fully bright when "Disable chat" was ticked even though the underlying inputs were disabled. Fixes ether#7592 in the colibris skin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): simple flex titlebar — CHAT _ [] Single flex row, vertically centred via align-items: center. Title takes the remaining width with flex: 1; the two corner controls fall in at the right edge in source order (titlecross then titlesticky), giving the intended visual: minus on the left, sticky on the right. Drops `float: right` from the controls, `display: inline` from the heading, and the prior `padding-top: 2px` hack on titlesticky (flex alignment handles the vertical position now). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): titlebar uses underscore for minimize; symmetric padding - Replace \`−\` with \`_\` in #titlecross. The minus glyph sits at the centre of its em-box and read as a hyphen mid-row when the row was vertically centred; \`_\` sits at the bottom of its em-box and reads as a proper minimize indicator. - Even out #titlebar horizontal padding to 9px and drop the asymmetric \`margin-left: 4px\` on #titlelabel so CHAT on the left and the sticky button on the right are the same distance from the bar's edges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): lift #titlecross underscore 5px The \`_\` glyph renders at the bottom of its em-box, so even with the title bar's flex \`align-items: center\` it sits noticeably below the CHAT baseline. Lift it with \`transform: translateY(-5px)\` (doesn't affect flex layout calculations) so the underscore reads at roughly the same vertical line as the title. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(chat): cover ether#7590 / ether#7592 / ether#7593 fixes Adds Playwright frontend specs for the changes in this PR: chat.spec.ts - chat icon click reveals chatbox after disable→enable cycle (regression: chat.show() must clear inline display:none) - title bar lays out as a centred flex row with underscore minimize (covers display, align-items, label flex:1, no float, translateY lift, and visual padding symmetry via rendered geometry) - chat icon click reliably opens the chat box (#chaticon .buttonicon pointer/flex reset) pad_settings.spec.ts - disabling chat disables and visually greys the dependent chat toggles (ether#7592 — checks input :disabled state and label opacity) change_user_name.spec.ts - #myusernameform has 10px left margin and is not width-capped (ether#7593 review — colibris margin alignment, no input width cap) Padding symmetry asserted via rendered rect deltas rather than the CSS literal, since colibris ships its own #titlebar padding override. 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: added release notes for 2.7.1 * chore: don't cache node_modules due to cas
…r#7563) * fix(settings): derive randomVersionString from release identity Fixes ether#7213. Etherpad appends a `?v=<token>` cache-buster to static assets and embeds the same token as `clientVars.randomVersionString` in the padbootstrap JS bundle produced by specialpages.ts. Because esbuild's content-hash feeds back into the generated bundle filename (`padbootstrap-<hash>.min.js`), the token's value determines the file that clients are told to load. Historically the token was `randomString(4)`, regenerated on every boot. In a horizontally-scaled deployment (ingress → etherpad service → multiple pods) that meant every pod produced a different filename for the same built artifact. A client that loaded the HTML from pod A would request `padbootstrap-ABCD.min.js` from pod B and hit a 404 when the upstream balancer placed the follow-up request elsewhere. Derive the token deterministically so pods of the same build emit identical filenames, while still rotating on release so clients invalidate their cache correctly: ETHERPAD_VERSION_STRING env → verbatim (integrator override) else → sha256(version + "|" + gitVersion)[:8] Backwards-compatible: single-pod deployments see the same effective behavior (token rotates each release). Integrators who want to pin the token explicitly — e.g. tying it to their own deploy ID — can set `ETHERPAD_VERSION_STRING` in the environment. Test coverage added in src/tests/backend/specs/settings.ts: - Default shape is an 8-hex-char sha256 prefix. - ETHERPAD_VERSION_STRING override is respected verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7213): call reloadSettings() to exercise ETHERPAD_VERSION_STRING The token is assigned inside reloadSettings, not parseSettings, so a parseSettings-only call never sees the env var. Drive reloadSettings directly, restoring the file paths and the prior token afterwards so other tests see a clean module state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [semver](https://github.com/npm/node-semver) from 7.8.0 to 7.8.1. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](npm/node-semver@v7.8.0...v7.8.1) --- updated-dependencies: - dependency-name: semver dependency-version: 7.8.1 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>
… semantics (ether#7819) (ether#7827) Two related operator-facing docs gaps, both surfaced by ether#7819: 1. settings.json on disk is a *template*; env-var substitution happens at load time in memory only. Operators repeatedly mistake the templated file for a stale config because the docs never spell out that the on-disk file is intentionally unchanged by env vars. 2. The default docker-compose.yml puts settings.json in the container's writable layer with no host mount, which means admin /settings edits are silently lost on `docker compose down && up`, `pull`, or watchtower — but preserved across plain `restart`. Operators don't reliably know which compose verbs recreate the container. Adds two prose sections to doc/docker.md (explaining both gotchas, with a recreate-vs-restart table) and a commented-out `./settings.json:…` bind mount in both docker-compose.yml and the README compose example. Bind mount is opt-in so existing setups behave identically. No runtime change. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.3 to 12.5.4. - [Release notes](https://github.com/tediousjs/node-mssql/releases) - [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt) - [Commits](tediousjs/node-mssql@v12.5.3...v12.5.4) --- updated-dependencies: - dependency-name: mssql dependency-version: 12.5.4 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>
…ther#7836) Closes ether#7835. - src/locales/en.json: add `index.code` (referenced by src/templates/index.html for the session-receive code input but never defined, producing a "Couldn't find translation key" console error on the landing page). - admin/src/utils/LoadingScreen.tsx, admin/src/pages/PadPage.tsx, admin/src/pages/AuthorPage.tsx: every @radix-ui/react-dialog `Dialog.Content` now has a `Dialog.Title` and `Dialog.Description` (visually hidden via `@radix-ui/react-visually-hidden` where there is no visible heading), silencing Radix's a11y console warnings on every admin page load. - src/tests/backend-new/specs/template-l10n-keys.test.ts: regression coverage — fails CI if any `data-l10n-id` in `src/templates/*.html` is missing from `src/locales/en.json`. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…errors (ether#7819) (ether#7826) * feat(admin): explain env-var substitution in /settings, surface auth errors (ether#7819) Three small, env-var-only UX improvements driven by issue ether#7819, where a Docker operator saved an ep_oauth block in the admin /settings raw view and reported it "disappeared" — but the underlying confusion was that settings.json on disk is a *template*, not the effective config. None of these changes is visible to installs that don't use ${VAR} placeholders. * Banner above the editor explaining the template/env-substitution model, only rendered when the loaded file contains a ${VAR} placeholder. Tells the operator that the file is not env-substituted in place and that the Effective tab shows the live values. * Effective tab in the mode toggle, read-only, also gated on ${VAR}. The backend was already emitting redacted runtime settings as `resolved` alongside every `load`; the SPA now exposes them so an operator can verify what Etherpad is actually using. * admin_auth_error event from the /settings socket handler. The handler previously silently returned when the connecting session wasn't admin, which made misrouted Traefik+SSO auth look like "save did nothing" with no error path in the UI. Emit a dedicated event before dropping the socket so the SPA can show a clear toast. Tests: - src/tests/backend/specs/admin/adminSettingsAuthError.ts — new spec for the auth_error/disconnect contract. - src/tests/frontend-new/admin-spec/adminsettings.spec.ts — new Playwright test asserting the banner + Effective tab only appear after a ${VAR} is added to settings.json, and that the Effective view is read-only + shows [REDACTED] for secrets. No behaviour change for installs without ${VAR} placeholders — banner, Effective tab, and auth-error contract are all the same as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): drop fragile pre-condition + add reconnect-loop guard (ether#7819) CI's admin-UI workflow seeds settings.json by copying settings.json.template verbatim, which contains ~30 \${VAR} placeholders. The new Playwright test asserted "banner not present before adding placeholder" — true on a fresh dev machine, false in CI. Drop that assertion: the negative path is covered by the SettingsPage ENV_VAR_PATTERN regex itself; what matters at the UI level is the positive path (banner + Effective tab render correctly when placeholders are present), which this test still exercises. Also: the server's admin_auth_error path calls socket.disconnect(), which the SPA's existing disconnect handler interprets as "io server disconnect" and immediately reconnects — creating a reject/reconnect loop. Track an authErrored flag and suppress the reconnect once an auth_error has been received. Reset on successful connect, so a legitimate re-auth path still works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d-test ELIFECYCLE (ether#7838) * test(ci): heartbeat + running-test pointer in backend test diagnostics Backend tests silently die with code 255 mid-suite ~22% of the time on develop (most often Windows-with-plugins, Node 24). Each kill lands 300±50 ms after the previous test's clean ✔ teardown line and produces no failing-test marker, no error, no Mocha summary, and — despite the unconditional handlers in `diagnostics.ts` — none of the JS-level death events fire either. Recent example: run 26311025244 (`Windows with Plugins (24)`); both attempts crashed at completely different "last test" locations, so the dying test itself isn't to blame. The existing diagnostics only set lastSeenTest in afterEach, so if the kill lands during the NEXT test's setup or body — which is exactly the ~300ms gap we observe — the pointer reads as the previous (passing) test. That hides whether we're between tests or inside one, and which one. Two changes: 1. Track currentTest in beforeEach as well as lastFinishedTest in afterEach. Every diag line now carries both, so the death point is bracketable regardless of which lifecycle phase the kill interrupts. 2. Add a 1Hz heartbeat that writeSyncs the running-test name plus `process.memoryUsage()` (rss, heap) and the active-handle and active-request counts. The interval is unref'd so it never holds the event loop open by itself. Cost is roughly one extra log line per second of mocha runtime (~60-120 lines per CI run). When the next failure fires, the last heartbeat narrows the kill window to ≤1s, the running pointer names the test on the rails at that moment, and the handle/memory trace gives a sparkline that exposes sudden spikes — a leaked socket, an unref'd timer, a runaway map — that would otherwise be invisible at the runner-log level. No behavior change on successful runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: heartbeat _getActiveHandles optional chain bug (Qodo #2) Qodo correctly flagged `_getActiveHandles?.().length` as a latent TypeError: `?.()` guards the call but the call's `undefined` return on a missing method still hits `.length`, which throws. Since the heartbeat fires on a setInterval inside the mocha bootstrap, a Node build without the underscore-prefixed internals would take down the whole backend test run. Capture the array first, then read `.length` only when it actually exists. -1 stays as the "API missing" sentinel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ci): per-test start diag + drop stray console.log noise Follow-up to the heartbeat PR after run 26397693748 confirmed the diagnostic works (the kill landed at importexportGetPost.ts 'Import authorization checks > authn anonymous !exist -> fail', ~300 ms after the previous test's ✔). Two cleanups so the next failure pinpoints faster and reads cleaner: 1. diagnostics.ts: emit a `test start: <name>` diag line in the mocha beforeEach hook, after setting the currentTest pointer. The 1Hz heartbeat misses tests that take less than a second, and the silent kills land ~300 ms after a test boundary — precisely the gap where heartbeat resolution fails. A start line per test gives sub-millisecond resolution on which test was on the rails when the process died. 2. specs/api/importexportGetPost.ts: drop a stray `console.log(importedPads)` debug leftover (and the duplicate `await importEtherpad(records)` only present to feed it) in the `malformed .etherpad files are rejected` block. The leftover dumped a ~600-line reflection of a supertest Response object to the CI log on every successful run, drowning the surrounding test output and making the silent-kill window much harder to read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ci): write node-report on every heartbeat tick Run 26398054688 narrowed the kill to a specific test (pad.ts > Gets text on a pad Id and doesn't have an excess newline) but the test body is a trivial supertest GET — the kill bypasses all JS handlers, so we can't capture stack state at death. Two failures across two runs share the shape: an agent.{get,post} + common.generateJWTToken() call dies ~300-600 ms after test start, with no JS-visible cause. The next step is V8 + native stack. Hook into the existing 1Hz heartbeat to call process.report.writeReport(path) whenever a report directory is set. The Windows backend-tests workflow already wires up `--report-directory=${{ github.workspace }}/node-report` via NODE_OPTIONS and uploads that directory as an artifact on failure, so the rolling snapshots ride for free on the existing upload step. Each report (~50 KB) contains: - V8 + native call stacks for all threads - libuv active handles (open TCP, timers, file handles) - JS heap statistics - resourceUsage + system info - shared-object list On the next reproduction the latest report before ELIFECYCLE will sit ~0-1 s before the kill — enough to see whether the V8 stack is inside jose's WebCrypto sign path, inside supertest's TCP roundtrip, or somewhere unexpected entirely. NODE_REPORT_DIR is also honored as an explicit override for local repro / non-workflow runs. Cost: ~6 files (~300 KB) per Windows backend-test failure, plus ~50 ms event-loop pause per heartbeat. No-op when neither env var is set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: writeReport with bare filename, not mixed-slash absolute path Run 26398830249 exposed the path-separator bug in the previous commit: every heartbeat tick on the Windows runner logged Failed to open Node.js report file: D:\a\etherpad\etherpad/node-report/hb-NNNN-...json directory: D:\a\etherpad\etherpad/node-report (errno: 22) — EINVAL. The workflow sets --report-directory with forward-slash separators on Windows, then this code concatenated another `/` plus the filename, producing a path Node's report writer rejects. writeReport(fileName) takes a BARE filename and resolves it against the configured report directory using the platform-correct separator internally. Switch to that. For local repro overrides via NODE_REPORT_DIR, push the path into process.report.directory (the documented config knob) instead of joining it into the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ci): write node-report on test boundaries, throttled to 4Hz Run 26398985832 proved the heartbeat-only report cadence isn't tight enough: the last report before the kill was hb-0013 at +16201ms, ~1.5 s before ELIFECYCLE at +17701ms — during which ~30 tests fired, including the dying one (`authn anonymous !exist -> fail`). The captured V8 stack is just our heartbeat code, not the dying test. Move the writeReport call to a shared tryWriteReport() helper and invoke it from BOTH the heartbeat AND mocha's beforeEach hook, throttled to one report per 250 ms. That gives ≤250 ms resolution on the kill window — close enough that the latest report captures state from inside the dying test rather than from the test ~30 slots earlier. The heartbeat always writes (so we don't lose the no-test-running ticks during setup); beforeEach only writes when the throttle window has elapsed. Cost ceiling: ~4 reports/sec × ~12 s test phase ≈ 48 reports (~2.5 MB) per failing run. Each writeReport adds ~50 ms of event-loop pause — at 4Hz that's 20% of wall time spent in diagnostics, which is acceptable for a temporary debug-only bootstrap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ci): drop beforeEach report throttle from 250ms to 100ms Run 26399285213's rerun captured a sixth death point on the new 4Hz cadence (`socketio.ts > Duplicate-author handling > cookie identity: same-author second socket kicks the first`, kill at +45953ms, 271ms after test start). The throttle suppressed the dying test's own beforeEach: previous boundary write landed 128 ms earlier and the next 31 ms after that, both inside the 250 ms window. Last captured report (be-0100) is from the previous test. 100 ms is still well above the inter-test cadence in fast burst suites (tests fire 2-5 ms apart, so 20-50 of them get throttled to a single write, ceiling ~10 writes/sec). But it's tight enough that any death-window neighbour ≥100 ms after the previous report — the shape we keep observing — gets its own boundary snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ther#7842) * test(ci): schedule a mid-test snapshot 150ms after every beforeEach Run 26401801404 (PR ether#7841 after merging develop) captured the dying test's beforeEach node-report — be-0258, written 75ms after socketio.ts > "Pad-wide settings creator gate different browsers" entered — but no further state. The kill landed 321ms into the test body, between 1 Hz heartbeat ticks, and the 100ms boundary throttle prevented further beforeEach writes inside the same test. The report we have shows only the listening server socket; the connections that the test body creates (and that presumably precede the kill) never get snapshotted. Schedule an unref'd setTimeout from beforeEach that fires 150ms after the test entered. If it's still the running test at fire time (i.e. slow enough that the death window applies), capture a node-report from INSIDE the test body — the moment when the real TCP / socket.io activity is in flight. Fast tests (<150ms) skip the write because afterEach has already cleared currentTest by the time the timer fires. Result on the next reproduction of the death pattern: - be-NNNN report at +75ms (beforeEach, body not yet started) - mt-MMMM report at +150ms (mid-test, body in flight, before kill at +320ms) - kill, no further reports Cost: only slow tests (>150ms) generate an mt report, so the artifact size growth is bounded by the count of tests that take longer than 150ms — typically a small minority. Locally verified against a 3-test probe: 2 fast tests skipped, 1 300ms test produced the expected mt-NNNN snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ci): bump heartbeat from 1Hz to 5Hz Run 26402211271 produced be-0207 (the dying test's beforeEach snapshot) but no mid-test snapshot, even though setTimeout(150ms) was scheduled and the test body lived another 280 ms after that deadline. setTimeout under Windows-runner load is being starved past the deadline — we already saw the previous test's mt fire at +252 ms (102 ms late) when the deadline was 150 ms, so the dying test's timer was likely scheduled to fire well after the kill at +425 ms. setInterval has fired reliably throughout the investigation (every heartbeat in every run lands within ~1 s of schedule, even when setTimeout misses). Bump heartbeat to 200 ms (5 Hz) so any death window ≥200 ms is sampled inside the test body, independent of how starved setTimeout is. Cost on the Windows runner: the existing log shows writeReport completes in <1 ms (from "Writing Node.js report" to "Node.js report completed" timestamps), so 5 Hz adds ~5 ms/s of overhead. Artifact growth: ~500 reports for a 100 s test phase (~25 MB raw, ~5 MB compressed). The setTimeout mid-test snapshot stays — it's belt-and-suspenders cheap and fires for slow tests where the heartbeat alone might not align with the death window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: isolate mt/hb writes from `be` throttle + gate timer on canWriteReport Qodo flagged two real issues on ether#7842: 1. The single shared `lastReportT` made `mt` writes poison the `be` throttle window. Slow tests trigger an `mt` write at +150 ms, then the test ends a few ms later, and the NEXT test's `beforeEach` landed within the 100 ms throttle from the `mt` write — so its own `be` snapshot was suppressed. That's the exact boundary coverage the throttle is supposed to PROTECT. Local repro with a 180 ms slow test followed by a fast one confirmed: the fast test's `be-0004` is now captured instead of swallowed. Fix: split into `lastBoundaryT` used and updated only by `be` writes. `hb` and `mt` pass `updateThrottle=false` and never advance the boundary timestamp. 2. `setTimeout` was being scheduled in `beforeEach` for every test even when `canWriteReport` is false (Linux backend matrix, local dev). That's a wasted timer per test for no possible diagnostic output. Gate the schedule itself on `canWriteReport`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r#7841) This flag gates the ep_* passthrough on padoptions that shipped in 3.0.0 (PR ether#7698). It was introduced as opt-in, but the intent in shipping it was to let plugins like ep_plugin_helpers' padToggle / padSelect ride the existing broadcast/persist rail out of the box — flipping the default closes the gap. Why now - ep_comments_page#422 (and sibling per-plugin reports discussed on Discord): stock 3.x deployments console.warn on every pad load because the helper detects clientVars.enablePluginPadOptions === false and tells the admin to flip it. With the flag default-true, the warning stops firing on fresh installs while still surfacing for operators who have explicitly opted out. - Plugins that already depend on ep_plugin_helpers >= 0.6 expect the pad-wide path to work; the default-false gate silently no-op'd pad.changePadOption('ep_*', …) and made the helper UI inert. Scope - Settings.ts default flipped to true; comment rewritten to describe the new "operator opt-out" model rather than the old AGENTS.MD §52 opt-in framing (that policy still applies to *new* features; this one has shipped and proven safe). - settings.json.template env-var substitution default flipped to true so docker / supervisor configs without an explicit value get the new behavior. - doc/plugins.md updated to match (default true, opt-out via settings.json) and the PluginCapabilities source comment. - Backend test describe-blocks relabeled — "true" is now "(default)", "false" is now "(operator opt-out)". Both branches still cover the same matrix so the size-cap / namespace-validation paths stay exercised. Compat - Existing deployments with an explicit `"enablePluginPadOptions": false` in settings.json keep that value — no migration needed. - Older clients only read clientVars.enablePluginPadOptions; the protocol shape is unchanged. Closes ep_comments_page#422 (helper warning suppression for stock deployments). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) (ether#7843) * fix(pad): URL view-option params lost to padeditor.init race (ether#7840) `?showLineNumbers=false` and `?useMonospaceFont=true` were being silently clobbered shortly after pad load. Same race that affected `?rtl=false` before ether#7464: 1. _afterHandshake → getParams() sets settings.LineNumbersDisabled (or useMonospaceFontGlobal, noColors). 2. _afterHandshake calls padeditor.init(view).then(postAceInit) — async; ace iframes still loading. 3. Sync tail of _afterHandshake hits the URL-param overrides and calls changeViewOption('showLineNumbers', false) etc. These queue setProperty('showslinenumbers', false) in Ace2Editor's actionsPendingInit queue (loaded=false). 4. ace.init resolves → loaded=true → queue flushes → URL-driven value applied. 5. padeditor.init resumes past its own await and calls setViewOptions(initialViewOptions) — initialViewOptions is built from clientVars.initialOptions.view (server defaults ∨ cookie), which does NOT carry the URL preference. The resulting setProperty('showslinenumbers', true) runs against loaded=true ace and immediately re-shows the gutter. ether#7464 noticed this race for RTL and moved the override into postAceInit. The neighbouring blocks for showLineNumbers / noColors / useMonospaceFontGlobal were left at the synchronous-tail site — generalise the same fix to all three. Direct-browser users typically had a `prefs` cookie with showLineNumbers=false from a prior in-pad toggle, so the initialViewOptions value happened to match the URL param and the race was unobservable. Cross-context iframe embeds (the reporter's configuration) start with no cookie, so the server default true fights the URL false and the race becomes visible. Adds src/tests/frontend-new/specs/url_view_options.spec.ts covering the showLineNumbers=false / =true / useMonospaceFont=true cases on initial-load navigation (the path where the race actually fires). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(types): annotate navigateWithParam helper params Fixes CI ts-check: parameters page/padId/param implicitly had `any`. 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(root): drop redundant top-level files Three small clean-ups to make the project root easier to scan for new contributors: - best_practices.md was a near-duplicate of CONTRIBUTING.md. Merge its two unique bullets (PRs MUST include a description / flag empty descriptions as incomplete) into CONTRIBUTING.md and remove the file. - .pr_agent.toml is a two-line Qodo PR-bot config. It has no functional references in the repo and Qodo currently doesn't review this repo. - tests/ -> src/tests was an unused root symlink. Verified no workflow, script, eslint config, or playwright config relies on the root path (all callers use src/tests/... or the ep_etherpad-lite package path). No CI, build, or docs updates needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(root): fix dead tests/frontend + best_practices.md refs Follow-up on Qodo review feedback for ether#7839: - CONTRIBUTING.md: front-end-tests path now reads src/tests/frontend/ instead of the dead tests/frontend/ (the root tests symlink is gone). The browser URL <yourdomainhere>/tests/frontend stays — that's an Express route served by the test runner, not a filesystem path. - docs/superpowers/specs/...openapi-design.md: drop best_practices.md from the parenthetical list of policy sources (now CONTRIBUTING.md and AGENTS.MD), since best_practices.md has been removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(oidc): fix OIDCAdapter broken flows * fix(oidc): fix storage type to include string for userCode index * test(oidc): add regression tests for OIDCAdapter broken flows
The 'enter is always visible after event' test asserted that the last
line was within the browser viewport using boundingBox().y + height vs
window.innerHeight. Those values live in different coordinate spaces
(boundingBox is outer-page; window is per-frame), and the comparison
is fundamentally unable to model what the editor's auto-scroll actually
guarantees: visibility inside the ace_outer iframe, not within the
outer browser viewport.
Any plugin that adds chrome above or below the editor (toolbar rows,
sidebars, etc.) pushes the iframe's bottom below the browser viewport
while auto-scroll has correctly placed the cursor at the iframe's
bottom — failures look like 'Expected: > 731, Received: 720'. An
earlier attempt to switch to toBeInViewport({ratio: 1}) traded the
false positives for false negatives under chromium + plugins because
the inner iframe's contents can report ratio 0 against the outer
viewport even when the line is visible inside the editor.
Drop the visibility assertion entirely. The test's real value — that
Enter keystrokes produce new lines and the editor's input pipeline
keeps up — is exercised by the per-iteration toHaveCount value-wait
in the loop above. Visibility under plugin chrome is a separate,
plugin-aware concern.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ther#7846) In-process diagnostics (diagnostics.ts heartbeat at 5 Hz + node-report snapshots on every beforeEach and heartbeat tick) merged in ether#7838 and ether#7842 reach a hard ceiling: during every captured death window the V8 main isolate is event-loop-starved for 200-400 ms before the process is externally terminated, so any timer-driven probe (heartbeat, setTimeout, --report-on-signal handler) never gets serviced and we have zero JS-visible state from the actual moment of death. To capture state during the starvation window we need a probe whose own scheduling does not depend on the dying process's libuv event loop. This commit adds a tiny bash background loop to the Windows backend-test steps (both with- and without-plugins). Every 500 ms it appends: - netstat.log: localhost TCP socket state — surfaces TIME_WAIT / CLOSE_WAIT accumulation or ephemeral-port exhaustion that the in-process libuv handle list can't see (libuv only shows handles Node currently knows about; the kernel may hold many more sockets in disposal states). - tasklist.log: node.exe process state from the Windows OS view (handle count, working set, CPU time), independent of whether V8 is responsive. Both files land in $GITHUB_WORKSPACE/node-report/ which is already the artifact-upload target on failure, so they ride for free on existing infrastructure. The watcher is killed cleanly after `pnpm test` returns so it never holds the runner open. On the next captured silent ELIFECYCLE we'll have, for the first time, a 500 ms-resolution external observation of TCP and process state across the death window. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.5.0 to 11.5.1. - [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md) - [Commits](isaacs/node-lru-cache@v11.5.0...v11.5.1) --- updated-dependencies: - dependency-name: lru-cache dependency-version: 11.5.1 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>
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.3 to 3.22.4. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](sidorares/node-mysql2@v3.22.3...v3.22.4) --- updated-dependencies: - dependency-name: mysql2 dependency-version: 3.22.4 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>
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.7 to 8.0.9. - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](nodemailer/nodemailer@v8.0.7...v8.0.9) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 8.0.9 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>
…ate + Node 24.16.0) (ether#7866) * fix(test): stop a single leaked promise rejection from killing the whole backend suite Root cause of the long-standing Windows backend-test "silent ELIFECYCLE" flake (~22% of runs, rotating across random spec files, no mocha summary, no JS-handler trace, bypassing --report-on-fatalerror / Defender / Windows event log / AeDebug). Found by capturing a full-memory dump of the dying node.exe with Sysinternals ProcDump (-t dump-on-terminate) and symbolizing it against Node 24.15.0's node.pdb. The dying thread's stack: exit_or_terminate_process / common_exit (CRT exit) node::Exit node::DefaultProcessExitHandlerInternal node::Environment::Exit node::ReallyExit (process.exit binding) ... v8 MicrotaskQueue::RunMicrotasks ... node::InternalCallbackScope::Close No exception stream — a *clean* ExitProcess, not a crash. The job log pinned the trigger: [INFO] server - Exiting... AssertionError at tests/backend/specs/SessionStore.ts:235 at process.processTicksAndRejections Mechanism: a timing-fragile test (SessionStore touch/expiry specs use real setTimeout against a 200ms-expiry session; socket.io delay-race specs are similar) gets timed out and abandoned by mocha, but its async body keeps running. When its trailing assertion later throws, it surfaces as an ORPHAN unhandled rejection belonging to no awaited test. Three handlers then escalated that into a whole-process exit: - server.ts installed process-global uncaughtException/unhandledRejection handlers that call exports.exit() → process.reallyExit() (production graceful-shutdown behaviour, catastrophic in-process under mocha) - common.ts (PR ether#7663) and diagnostics.ts (PR ether#7838) rethrew the rejection and process.exit(1) Because it's a deliberate, clean exit it bypassed every forensic layer; it rotated across files because the orphan rejection lands during whatever test is running; it's Windows-mostly because event-loop timing makes the abandoned test's assertion fire in a *later* test's window more often there. Fix (two halves): 1. server.ts: gate the process-global uncaughtException / unhandledRejection / signal handlers behind `require.main === module`. They are correct for a real Etherpad process but must not fire when server.start() is called in-process by a test runner — mocha owns process-level error handling there. Mirrors the existing `if (require.main === module) exports.start()` idiom; production (node server.js) is unchanged. 2. common.ts + diagnostics.ts: the backend-test bootstraps now LOG unhandled rejections instead of rethrowing / exiting. Orphan rejections cannot be cleanly attributed to a test, so rethrowing only yields an ERR_MOCHA_MULTIPLE_DONE abort. Real failures are unaffected — an assertion in a test's own awaited path rejects that test's promise and mocha fails it normally, never reaching this global handler. Verified locally: a spec that leaks a delayed rejection during a later test now reports `3 passing` / exit 0 with the rejection logged, instead of aborting the run. Follow-ups (separate PRs): harden the SessionStore / socket.io timing specs to not leak (fake timers); remove the now-unneeded diagnostic scaffolding (diagnostics.ts heartbeat/node-report, the ether#7846 OS sidecar) now that the cause is known. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): run Windows backend tests on Node 25 to dodge the libuv connect overrun Node 24.x's bundled libuv has a stack buffer overrun in the Windows TCP-connect path (uv__tcp_connect / uv__tcp_try_connect), proven by a SilentProcessExit full-memory dump of the dying mocha process: the main thread executes __fastfail(FAST_FAIL_STACK_COOKIE_CHECK_FAILURE) from __report_gsfailure with TCPWrap::Connect -> uv_tcp_connect on the stack. It fires under the backend suite's heavy localhost connection churn, is address-family independent (occurs on both sockaddr_in and sockaddr_in6, so an IPv4 pin does NOT help), and -- being memory corruption -- bypasses all JS/Node observability, rotating across tests as the "silent ELIFECYCLE" flake (~22% of Windows runs). Empirically: Node 25 = 16/16 green; Node 24 (even with an IPv4 pin) = ~39% fail. Node 25's newer bundled libuv does not overrun. Linux stays on Node 24 LTS (the bug is Windows-specific). Revisit once the libuv fix is backported to 24.x. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): pin Windows backend to Node 24.16.0 (libuv fix) instead of 25 Bisect (standalone repro) pinpointed the fix to Node 24.16.0 (libuv 1.52.1): 24.15.0 (libuv 1.51.0) crashes the connect overrun 4/4 on 127.0.0.1, while 24.16.0 is clean 0/8. 24.16.0 stays on the Node 24 "Krypton" LTS line, so prefer it over Node 25 (non-LTS). Pinned explicitly because setup-node's default check-latest:false reuses the runner's pre-cached 24.15.0 for a bare "24". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ci): reference upstream nodejs/node#63620 in the Windows Node-pin comment Links the explicit 24.16.0 pin to the filed upstream issue so the pin can be dropped back to plain "24" once the libuv connect-overrun fix is across the supported 24.x baseline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ether#7868) * test(ci): remove the silent-ELIFECYCLE investigation scaffolding The Windows backend flake is root-caused (server.ts handler gate for the in-process process.exit path; Windows pinned to Node 24.16.0 for the libuv TCP-connect overrun, tracked upstream as nodejs/node#63620). Remove the temporary diagnostics added while hunting it: - delete src/tests/backend/diagnostics.ts (per-test heartbeat + node-report snapshots) and its `--require` from the backend `test` script; - drop the `--report-on-fatalerror`/`-on-signal`/`-uncaught-exception` `NODE_OPTIONS` and the "Upload Node diagnostic reports" steps; - drop the Windows OS-level netstat/tasklist sidecar watcher. Kept: the real fixes — the log-only unhandledRejection guard in tests/backend/common.ts, the lowerCasePadIds socket-teardown tracking (comment de-referenced from the deleted file), and `pnpm test -- --exit` on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: drop the obsolete report-on-fatalerror NODE_OPTIONS guard assertion The backend-tests-flake-mitigation source-lint guard required every backend step to keep the --report-on-fatalerror NODE_OPTIONS + node-report upload. Those diagnostics are removed in this PR now that the flake is root-caused, so drop that assertion. Retain the Windows-only `--exit` checks (still a live invariant) and reframe the file around the resolved root cause. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ith 8 updates (ether#7867) Bumps the dev-dependencies group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives) | `1.2.3` | `1.2.4` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.59.4` | `8.60.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.59.4` | `8.60.0` | | [i18next](https://github.com/i18next/i18next) | `26.2.0` | `26.3.0` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.17.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.0` | `7.76.1` | | [zustand](https://github.com/pmndrs/zustand) | `5.0.13` | `5.0.14` | | [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.132.0` | `0.133.0` | Updates `@radix-ui/react-visually-hidden` from 1.2.3 to 1.2.4 - [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md) - [Commits](https://github.com/radix-ui/primitives/commits) Updates `@typescript-eslint/eslint-plugin` from 8.59.4 to 8.60.0 - [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.60.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.59.4 to 8.60.0 - [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.60.0/packages/parser) Updates `i18next` from 26.2.0 to 26.3.0 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](i18next/i18next@v26.2.0...v26.3.0) Updates `lucide-react` from 1.16.0 to 1.17.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react) Updates `react-hook-form` from 7.76.0 to 7.76.1 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](react-hook-form/react-hook-form@v7.76.0...v7.76.1) Updates `zustand` from 5.0.13 to 5.0.14 - [Release notes](https://github.com/pmndrs/zustand/releases) - [Commits](pmndrs/zustand@v5.0.13...v5.0.14) Updates `oxc-minify` from 0.132.0 to 0.133.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.133.0/napi/minify) --- updated-dependencies: - dependency-name: "@radix-ui/react-visually-hidden" dependency-version: 1.2.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.60.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.60.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: lucide-react dependency-version: 1.17.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: oxc-minify dependency-version: 0.133.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-hook-form dependency-version: 7.76.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: zustand dependency-version: 5.0.14 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>
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.2 to 6.1.3. - [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md) - [Commits](ether/ueberDB@v6.1.2...v6.1.3) --- updated-dependencies: - dependency-name: ueberdb2 dependency-version: 6.1.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>
Port of upstream ether#7605 Converts backend tests from mocha to vitest and migrates to ESM.
ef58b53 to
d5732a8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Port of upstream ether#7605
Converts backend tests from mocha to vitest and migrates to ESM.
Summary by cubic
Migrates backend tests to ESM and
vitest, then modernizes CI and packaging while shipping major admin, privacy, and editor upgrades. Runs on Node ≥22.12 withpnpm, adds Debian/Snap builds, and publishes a signed APT repo.New Features
/updatepage, JSONC settings editor, Authors page with search and GDPR erasure, i18n loader fix, plugin disables surfaced, visual refresh.showMenuRight), improved author‑color contrast, theme‑color meta, Open Graph/Twitter metadata.bin/compactPad,bin/compactAllPads,bin/compactStalePads).html[lang|dir], proper dialog semantics and focus management, accessible toolbar/export controls.CI & Packaging
vitestunder ESM; Playwright discovers plugin specs; “with‑plugins” matrix; feature tags + declared‑disables contract; flakyep_cursortraceexcluded from plugin matrix.pnpmstore caching;.npmrchardlink installs; Dockerfile updated..debvia nfpm with systemd unit; Snap build/publish; signed APT repo at https://etherpad.org/apt; hardened release workflows.oxc-minify), build on PRs, content updates.Written for commit d5732a8. Summary will update on new commits.