Compare commits

...

181 commits

Author SHA1 Message Date
John McLear
9a6109bd1d 10,000 commits. Worth a pause.
Thanks to every contributor: PRs, issues, reviews, translations, plugins, answered questions. This project runs on volunteer effort, not one person or one company. That's by design and it's why it's lasted.

Thanks to everyone running an instance: schools, newsrooms, gov departments, or just yourself. You trusted us with your documents. We don't take that lightly.

Thanks to the wider FOSS community. We've relied on other people's open source work more than we can credit individually, and tried to give some back. If you've never contributed to a FOSS project, this is your sign. Code, docs, translations, bug reports, or just running an instance and telling people about it, all of it counts.

The migration to modern JS tooling has been one of the best things to happen to this codebase. Easier to contribute to, easier to maintain, easier to build on, which is what matters for a 15+ year old project that intends to keep going.

Going forward: keep Etherpad boring, stable, self-hostable, no enshittification, while staying open to the plugins and integrations that make it useful. We need more maintainers and more instances. If that's you, open an issue.

Here's to the next 10,000. John McLear
2026-07-14 09:53:11 +01:00
translatewiki.net
037f4a25b5
Localisation updates from https://translatewiki.net. 2026-07-13 14:04:15 +02:00
dependabot[bot]
4755bd3e17
build(deps-dev): bump the dev-dependencies group with 3 updates (#8043)
Bumps the dev-dependencies group with 3 updates: [i18next](https://github.com/i18next/i18next), [react-i18next](https://github.com/i18next/react-i18next) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `i18next` from 26.3.5 to 26.3.6
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.5...v26.3.6)

Updates `react-i18next` from 17.0.8 to 17.0.9
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.8...v17.0.9)

Updates `vite` from 8.1.3 to 8.1.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.4/packages/vite)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.1.4
  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>
2026-07-13 11:11:18 +01:00
dependabot[bot]
05d0f62403
build(deps): bump @radix-ui/react-switch from 1.3.2 to 1.3.3 (#8029)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.3.2 to 1.3.3.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.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>
2026-07-10 10:27:58 +01:00
dependabot[bot]
3efcbfd358
build(deps): bump ueberdb2 from 6.1.15 to 6.1.16 (#8034)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.15 to 6.1.16.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.15...v6.1.16)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.16
  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>
2026-07-10 10:07:23 +01:00
dependabot[bot]
2b5d521923
build(deps): bump oidc-provider from 9.9.0 to 9.9.1 (#8036)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.9.0 to 9.9.1.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.9.0...v9.9.1)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.9.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>
2026-07-10 10:07:14 +01:00
dependabot[bot]
631caf911c
build(deps): bump lru-cache from 11.5.1 to 11.5.2 (#8037)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.5.1 to 11.5.2.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.5.1...v11.5.2)

---
updated-dependencies:
- dependency-name: lru-cache
  dependency-version: 11.5.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>
2026-07-10 10:07:06 +01:00
dependabot[bot]
0aba424357
build(deps-dev): bump the dev-dependencies group across 1 directory with 2 updates (#8041)
Bumps the dev-dependencies group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [i18next](https://github.com/i18next/i18next).


Updates `@types/node` from 26.1.0 to 26.1.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `i18next` from 26.3.4 to 26.3.5
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.4...v26.3.5)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: i18next
  dependency-version: 26.3.5
  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>
2026-07-09 16:51:09 +01:00
John McLear
194f144940
housekeeping: Adjust dependabot cooldown settings for NPM
Removed cooldown settings for Docker package ecosystem and added them for NPM.
2026-07-09 15:24:26 +01:00
John McLear
c045fcf698
housekeeping: Configure cooldown for Docker dependency updates
Added cooldown settings for Docker dependencies.
2026-07-09 15:23:00 +01:00
translatewiki.net
451df9f01c
Localisation updates from https://translatewiki.net. 2026-07-09 14:03:51 +02:00
SamTV12345
ce27525d97 chore: reinstall lockfile 2026-07-07 21:31:25 +02:00
SamTV12345
9c48900092 chore: updated lockfile 2026-07-07 21:27:35 +02:00
dependabot[bot]
173dbb3448
build(deps): bump undici from 8.6.0 to 8.7.0 (#8028)
Bumps [undici](https://github.com/nodejs/undici) from 8.6.0 to 8.7.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.6.0...v8.7.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.7.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>
2026-07-07 21:20:58 +02:00
dependabot[bot]
24e0c44d91
build(deps): bump oidc-provider from 9.8.6 to 9.9.0 (#8030)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.6 to 9.9.0.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.6...v9.9.0)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.9.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>
2026-07-07 21:20:31 +02:00
dependabot[bot]
f65701eaaf
build(deps): bump mysql2 from 3.22.5 to 3.22.6 (#8031)
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.5 to 3.22.6.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.22.5...v3.22.6)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.22.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 21:20:22 +02:00
dependabot[bot]
2f766d075d
build(deps-dev): bump the dev-dependencies group across 1 directory with 8 updates (#8032)
Bumps the dev-dependencies group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.9` | `4.1.10` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.18` | `1.1.19` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.18` | `1.2.19` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.62.1` | `8.63.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.62.1` | `8.63.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.80.0` | `7.81.0` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.138.0` | `0.139.0` |
| [vitepress](https://github.com/vuejs/vitepress) | `2.0.0-alpha.17` | `2.0.0-alpha.18` |



Updates `vitest` from 4.1.9 to 4.1.10
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest)

Updates `@radix-ui/react-dialog` from 1.1.18 to 1.1.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.18 to 1.2.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@typescript-eslint/eslint-plugin` from 8.62.1 to 8.63.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.63.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.62.1 to 8.63.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.63.0/packages/parser)

Updates `react-hook-form` from 7.80.0 to 7.81.0
- [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](https://github.com/react-hook-form/react-hook-form/compare/v7.80.0...v7.81.0)

Updates `oxc-minify` from 0.138.0 to 0.139.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.139.0/napi/minify)

Updates `vitepress` from 2.0.0-alpha.17 to 2.0.0-alpha.18
- [Release notes](https://github.com/vuejs/vitepress/releases)
- [Changelog](https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.17...v2.0.0-alpha.18)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.81.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.139.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitepress
  dependency-version: 2.0.0-alpha.18
  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>
2026-07-07 21:20:12 +02:00
translatewiki.net
2c94ef9493
Localisation updates from https://translatewiki.net. 2026-07-06 14:03:45 +02:00
dependabot[bot]
fdc23c658f
build(deps): bump tsx from 4.22.4 to 4.23.0 (#8023)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.22.4 to 4.23.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.4...v4.23.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-version: 4.23.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>
2026-07-05 11:25:26 +02:00
dependabot[bot]
e5d6142061
build(deps): bump nano from 11.0.5 to 11.0.6 (#8025)
Bumps [nano](https://github.com/apache/couchdb-nano) from 11.0.5 to 11.0.6.
- [Release notes](https://github.com/apache/couchdb-nano/releases)
- [Commits](https://github.com/apache/couchdb-nano/compare/v11.0.5...v11.0.6)

---
updated-dependencies:
- dependency-name: nano
  dependency-version: 11.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 11:25:10 +02:00
dependabot[bot]
b7d2477840
build(deps): bump undici from 8.5.0 to 8.6.0 (#8021)
Bumps [undici](https://github.com/nodejs/undici) from 8.5.0 to 8.6.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.5.0...v8.6.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.6.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>
2026-07-02 21:35:34 +02:00
dependabot[bot]
4bc80d1439
build(deps-dev): bump vite in the dev-dependencies group (#8020)
Bumps the dev-dependencies group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.1.2 to 8.1.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.1.3
  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>
2026-07-02 21:35:18 +02:00
dependabot[bot]
8a9c0a6e20
build(deps): bump redis from 6.0.1 to 6.1.0 (#8019)
Bumps [redis](https://github.com/redis/node-redis) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@6.0.1...redis@6.1.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.1.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>
2026-07-02 13:46:09 +01:00
dependabot[bot]
df1666cffa
build(deps-dev): bump the dev-dependencies group across 1 directory with 7 updates (#8017)
Bumps the dev-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.1` | `26.1.0` |
| [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) | `21.0.1` | `22.0.0` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.17` | `1.2.18` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.6` | `1.2.7` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.22.0` | `1.23.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.1` | `8.1.2` |



Updates `@types/node` from 26.0.1 to 26.1.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/sinon` from 21.0.1 to 22.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon)

Updates `@radix-ui/react-dialog` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.17 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-visually-hidden` from 1.2.6 to 1.2.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

Updates `lucide-react` from 1.22.0 to 1.23.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.23.0/packages/lucide-react)

Updates `vite` from 8.1.1 to 8.1.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.2/packages/vite)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@types/sinon"
  dependency-version: 22.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 1.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.1.2
  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>
2026-07-02 13:33:17 +01:00
dependabot[bot]
afdd9b9500
build(deps): bump @radix-ui/react-switch from 1.3.1 to 1.3.2 (#8018)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.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>
2026-07-01 21:39:53 +02:00
dependabot[bot]
3b9acd487c
build(deps): bump mssql from 12.5.5 to 12.6.0 (#8010)
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.5 to 12.6.0.
- [Release notes](https://github.com/tediousjs/node-mssql/releases)
- [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt)
- [Commits](https://github.com/tediousjs/node-mssql/compare/v12.5.5...v12.6.0)

---
updated-dependencies:
- dependency-name: mssql
  dependency-version: 12.6.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>
2026-07-01 21:39:45 +02:00
dependabot[bot]
82d1fd63a5
build(deps): bump @tanstack/react-query-devtools from 5.101.0 to 5.101.2 (#8005)
Bumps [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) from 5.101.0 to 5.101.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.101.2/packages/react-query-devtools)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.101.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>
2026-07-01 11:22:09 +01:00
dependabot[bot]
75be62c801
build(deps): bump ueberdb2 from 6.1.14 to 6.1.15 (#8008)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.14 to 6.1.15.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.14...v6.1.15)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.15
  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>
2026-07-01 11:22:03 +01:00
dependabot[bot]
ab4c7beded
build(deps): bump openapi-backend from 5.17.0 to 5.18.0 (#8011)
Bumps [openapi-backend](https://github.com/openapistack/openapi-backend) from 5.17.0 to 5.18.0.
- [Release notes](https://github.com/openapistack/openapi-backend/releases)
- [Commits](https://github.com/openapistack/openapi-backend/compare/5.17.0...5.18.0)

---
updated-dependencies:
- dependency-name: openapi-backend
  dependency-version: 5.18.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>
2026-07-01 11:21:49 +01:00
dependabot[bot]
be8120b14e
build(deps): bump nodemailer from 9.0.1 to 9.0.3 (#8015)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 9.0.1 to 9.0.3.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.3)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.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>
2026-07-01 11:21:39 +01:00
dependabot[bot]
33160db454
build(deps): bump mongodb from 7.3.0 to 7.4.0 (#8009)
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v7.3.0...v7.4.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-version: 7.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>
2026-06-30 22:50:26 +02:00
dependabot[bot]
9e850e17f3
build(deps): bump redis from 6.0.0 to 6.0.1 (#8013)
Bumps [redis](https://github.com/redis/node-redis) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@6.0.0...redis@6.0.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.0.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>
2026-06-30 20:41:41 +02:00
dependabot[bot]
3ac0b0f0a4
build(deps): bump surrealdb from 2.0.3 to 2.0.4 (#8006)
Bumps [surrealdb](https://github.com/surrealdb/surrealdb.js) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/surrealdb/surrealdb.js/releases)
- [Commits](https://github.com/surrealdb/surrealdb.js/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: surrealdb
  dependency-version: 2.0.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>
2026-06-30 20:09:42 +02:00
dependabot[bot]
cce7cc6cd4
build(deps): bump @tanstack/react-query from 5.101.0 to 5.101.2 (#8007)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.101.0 to 5.101.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.2/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.101.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>
2026-06-30 19:40:20 +02:00
dependabot[bot]
bce18df898
build(deps): bump oidc-provider from 9.8.5 to 9.8.6 (#8012)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.5 to 9.8.6.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.5...v9.8.6)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.8.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 19:40:13 +02:00
dependabot[bot]
1442aee756
build(deps): bump awalsh128/cache-apt-pkgs-action from 1.6.1 to 1.6.3 (#8014)
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.6.1 to 1.6.3.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.6.1...v1.6.3)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.6.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>
2026-06-30 19:40:08 +02:00
dependabot[bot]
00e1892704
build(deps-dev): bump the dev-dependencies group across 1 directory with 12 updates (#8016)
Bumps the dev-dependencies group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.0.1` |
| [eslint](https://github.com/eslint/eslint) | `10.5.0` | `10.6.0` |
| [set-cookie-parser](https://github.com/nfriedly/set-cookie-parser) | `3.1.0` | `3.1.1` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.61.1` | `8.62.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.61.1` | `8.62.1` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` |
| [i18next](https://github.com/i18next/i18next) | `26.3.1` | `26.3.4` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.21.0` | `1.22.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.18.0` | `7.18.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.1` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.137.0` | `0.138.0` |



Updates `@playwright/test` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.0...v1.61.1)

Updates `@types/node` from 26.0.0 to 26.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.5.0 to 10.6.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0)

Updates `set-cookie-parser` from 3.1.0 to 3.1.1
- [Changelog](https://github.com/nfriedly/set-cookie-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nfriedly/set-cookie-parser/compare/v3.1.0...v3.1.1)

Updates `@typescript-eslint/eslint-plugin` from 8.61.1 to 8.62.1
- [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.62.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.61.1 to 8.62.1
- [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.62.1/packages/parser)

Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react)

Updates `i18next` from 26.3.1 to 26.3.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.4)

Updates `lucide-react` from 1.21.0 to 1.22.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.22.0/packages/lucide-react)

Updates `react-router-dom` from 7.18.0 to 7.18.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.1/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.1/packages/react-router-dom)

Updates `vite` from 8.0.16 to 8.1.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.1/packages/vite)

Updates `oxc-minify` from 0.137.0 to 0.138.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.138.0/napi/minify)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: set-cookie-parser
  dependency-version: 3.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: i18next
  dependency-version: 26.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 1.22.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.138.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>
2026-06-30 19:40:04 +02:00
translatewiki.net
2ecd748fca
Localisation updates from https://translatewiki.net. 2026-06-29 14:04:24 +02:00
translatewiki.net
0cd01fce41
Localisation updates from https://translatewiki.net. 2026-06-25 14:03:14 +02:00
dependabot[bot]
40327d59d6
build(deps): bump actions/cache from 5 to 6 (#8001)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 11:31:56 +01:00
dependabot[bot]
b45f60e20a
build(deps): bump reitzig/actions-asciidoctor from 2.0.4 to 2.0.5 (#7998)
Bumps [reitzig/actions-asciidoctor](https://github.com/reitzig/actions-asciidoctor) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/reitzig/actions-asciidoctor/releases)
- [Changelog](https://github.com/reitzig/actions-asciidoctor/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reitzig/actions-asciidoctor/compare/v2.0.4...v2.0.5)

---
updated-dependencies:
- dependency-name: reitzig/actions-asciidoctor
  dependency-version: 2.0.5
  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>
2026-06-22 16:55:47 +01:00
dependabot[bot]
f5b61c3388
build(deps): bump semver from 7.8.4 to 7.8.5 (#8000)
Bumps [semver](https://github.com/npm/node-semver) from 7.8.4 to 7.8.5.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.4...v7.8.5)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.5
  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>
2026-06-22 16:55:42 +01:00
dependabot[bot]
ff0bd2e819
build(deps): bump ueberdb2 from 6.1.13 to 6.1.14 (#7999)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.13 to 6.1.14.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.13...v6.1.14)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.14
  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>
2026-06-22 16:55:22 +01:00
dependabot[bot]
8e04130969
build(deps): bump undici from 7.27.2 to 8.5.0 (#7997)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:01:12 +01:00
translatewiki.net
c8b544802a
Localisation updates from https://translatewiki.net. 2026-06-22 14:03:35 +02:00
John McLear
01500ca70c
chore(deps): vendor the unmaintained security escaper into core (#7993)
* chore(deps): vendor the unmaintained `security` escaper into core

The `security` npm package (escapeHTML / escapeHTMLAttribute and the JS/CSS
encoders) has had no release since 2012, yet it sits directly in Etherpad's
client-side XSS-defense path (pad_utils, domline) and the server-side HTML
export. Rather than keep a 14-year-old, single-maintainer dependency guarding
output encoding, vendor its implementation into core.

- static/js/security.ts now contains the escaping logic directly (reproduced
  verbatim from security@1.0.0, MIT, Chad Weider — byte-identical output) and
  no longer does `require('security')`. The full public API is preserved, so
  plugins that `require('ep_etherpad-lite/static/js/security')` keep working
  unchanged.
- pad_utils.ts requires the local './security' module instead of the bare
  'security' specifier (domline.ts and ExportHtml.ts already did).
- Drop `security` from src/package.json dependencies and from Minify's
  LIBRARY_WHITELIST (no bare specifier is served to the browser anymore).

Added tests/backend/specs/security.ts locking the byte-for-byte escaping
output so the vendored copy can never silently drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: use ESM named exports so vitest can resolve the security module

CI "Run the new vitest tests" failed with `Cannot find module './security'`
from pad_utils.ts. vitest/vite's CJS require() shim doesn't add a `.ts`
extension when resolving a relative specifier, so `require('./security')`
couldn't locate security.ts. (The old bare `require('security')` resolved to
a real .js in node_modules, which is why this only surfaced after vendoring.)

- security.ts now uses ESM `export const` for the seven helpers instead of a
  `module.exports = {...}` block.
- pad_utils.ts imports it as `import * as Security from './security'`, which
  goes through vite's resolver (knows .ts) and is also properly typed.

CJS consumers (domline.ts, ExportHtml.ts, the backend spec) keep working via
tsx/esbuild ESM->CJS interop. Verified: tsc clean, full vitest suite 721
passing, and the mocha security/export/import specs 27 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: force fresh run (prior run used a stale merge ref after reopen)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: remove ReDoS in vendored JSON-string-literal regex

CodeQL flagged a high-severity exponential-backtracking alert on the
JSON-string-literal regex vendored from the `security` package:
`/"(?:\\.|[^"])*"/`. The `[^"]` class also matches a backslash, so it overlaps
with the `\\.` alternative and backtracks exponentially on adversarial input
like `"\!\!\!...` (no closing quote). The original lived inside node_modules so
it was never scanned; vendoring it surfaced the alert.

Fix to the canonical linear form `/"(?:[^"\\]|\\.)*"/`, where the backslash is
excluded from the character class so the two alternatives are mutually
exclusive. It matches exactly the same well-formed JSON string literals (and
encodeJavaScriptData only ever runs it over JSON.stringify output), so behaviour
is unchanged for valid input.

Added tests: encodeJavaScriptData output + a ReDoS guard that runs the regex
over 50k adversarial chars and asserts it returns in well under a second.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:56:40 +01:00
John McLear
7168f14f0a
chore(deps): drop three unmaintained dependencies (unorm, find-root, jsonminify) (#7992)
* chore(deps): drop three unmaintained dependencies from core

Remove dependencies whose upstreams are effectively abandoned, replacing
each with a maintained alternative or native API. No behaviour change for
users; reduces the production dependency surface.

- unorm (last publish 2019): replace `UNorm.nfc(s)` in contentcollector
  with native `String.prototype.normalize('NFC')`, available in every
  supported Node and browser. Also drop it from Minify's LIBRARY_WHITELIST.
- find-root (last publish 2017): inline a ~10-line equivalent in
  AbsolutePaths.findEtherpadRoot(), mirroring find-root's semantics
  (closest ancestor containing package.json, throw if none).
- jsonminify (last publish 2021): swap settings parsing to jsonc-parser
  (already used by the admin workspace, actively maintained). The old
  `jsonminify(str).replace(',]', ']').replace(',}', '}')` had two bugs that
  jsonc-parser's allowTrailingComma fixes: String#replace only swapped the
  FIRST trailing comma of each kind, and the blind replace corrupted ',]' /
  ',}' byte sequences inside string values (e.g. URLs).

Added a regression test in settings.ts covering multiple trailing commas
and ',]'/',}' inside string values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: drop stale $unorm bundle entry from tar.json

The unorm removal left `$unorm/lib/unorm.js` listed in the ace2_inner.js
client bundle manifest. With unorm uninstalled, getTar() would point at a
node_modules asset that no longer exists, producing 404s when loading that
bundle. Nothing imports unorm anymore (contentcollector now uses native
String.prototype.normalize), so the entry is dead and removed.

Caught by Qodo review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: update container loadSettings helper to jsonc-parser

tests/container/loadSettings.js is a standalone helper (separate from
node/utils/Settings.ts) that parsed settings.json.docker with jsonminify
directly. Removing jsonminify from dependencies broke the container test
suite with MODULE_NOT_FOUND. Switch it to jsonc-parser to match Settings.ts.

Verified loadSettings() parses settings.json.docker and applies the
container ip/port overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:53:27 +01:00
John McLear
01d0b08a4e
docs: migrate useful wiki content into the manual (#7990) (#7994)
* docs: migrate useful wiki content into the VitePress manual (#7990)

The GitHub wiki is being retired; documentation should ship with the
software. This migrates the still-accurate, non-duplicate wiki pages into
the published VitePress site (doc/**/*.md + the sidebar in
doc/.vitepress/config.mts) so they are versioned, searchable and portable:

- deployment.md: reverse-proxy configs (Nginx/Apache/Caddy/Traefik/
  HAProxy) with the WebSocket-upgrade rules, subdirectory hosting via
  X-Proxy-Path, native HTTPS via the ssl block, a systemd unit, and the
  Istio manifest (with the Redis-adapter multi-replica caveat).
- accessibility.md: editor keyboard shortcuts (verified against
  ace2_inner.ts / broadcast_slider.ts / pad_editbar.ts), toolbar
  navigation, NVDA notes.
- faq.md: install methods, URL-path reference, listing/deleting pads
  (API-first), backup/restore, and history pruning.
- development.md: source-tree tour, the pad<->format conversion pipeline,
  the internal DB API, and the Fontello toolbar-icon workflow.
- database.md: the key/value schema plus connecting MySQL/PostgreSQL/Redis
  backends and a pgloader MySQL->PostgreSQL migration (database docs were
  previously absent from the VitePress site).

Every page was checked against the current source before inclusion:
corrected the apt instructions to the live signed repo (stable/main,
signed-by key), dropped the unpublished snap, fixed the Redis dbSettings
(flat host/port/password or url, not the obsolete client_options),
dropped charset from the PostgreSQL example, and removed a phantom
getEtherpad API reference. The VitePress site builds cleanly
(pnpm run docs:build) with the dead-link checker enabled.

Closes #7990

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: add verified hands-on changeset/atext walkthrough (#7990)

Migrate the practical Changeset-library tutorial from the wiki into
changeset_library.md, rewritten against the current API: unpack(),
deserializeOps() (replacing the deprecated opIterator) and
new AttributePool() (replacing the removed AttributePoolFactory). Every
example output was produced by running the code against the current
Changeset.ts / AttributePool.ts, not copied from the wiki. Also fixes a
stale ether/etherpad-lite source link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:52:33 +01:00
Etherpad Release Bot
41dc87edf9 Merge branch 'master' into develop 2026-06-21 18:45:44 +00:00
Etherpad Release Bot
93e5bcc1e2 bump version 2026-06-21 18:45:43 +00:00
Etherpad Release Bot
3c90fa07c3 Merge branch 'develop' 2026-06-21 18:45:43 +00:00
SamTV1998
851b1fb613 feat(changelog): added readme for 3.3.2 2026-06-21 20:43:09 +02:00
dependabot[bot]
9047aacc0a
build(deps): bump undici from 7.27.2 to 8.5.0 (#7991)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 13:11:58 +01:00
John McLear
773a9042a7
fix(import): correct outdated import-format help message (#7988) (#7989)
* fix(import): correct outdated import-format help message (#7988)

The import dialog's "no converter" notice claimed only plain text and
HTML could be imported and linked users to the legacy AbiWord wiki page,
prompting them to install LibreOffice for formats that already work
natively.

Etherpad imports .txt, .html, .docx (via mammoth) and .etherpad files
without LibreOffice; only .pdf/.odt/.doc/.rtf still need it. Update the
message to say so and move the help link to the ether/etherpad org.

Closes #7988

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): point help link to new LibreOffice wiki page (Qodo #7989)

The AbiWord wiki pages were vandalized/empty. Added a clean LibreOffice
setup page on the wiki and point the import dialog there instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): reference Etherpad docs instead of wiki for LibreOffice

The wiki is being retired, so don't link to it. Point users at the
documentation site for installing LibreOffice for extra import formats.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:08:46 +01:00
dependabot[bot]
926c94ea8a
build(deps): bump undici from 7.27.2 to 8.5.0 (#7986)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 12:39:27 +01:00
dependabot[bot]
bf5988580e
build(deps-dev): bump the dev-dependencies group across 1 directory with 2 updates (#7987)
Bumps the dev-dependencies group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [react-hook-form](https://github.com/react-hook-form/react-hook-form).


Updates `@types/node` from 25.9.3 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `react-hook-form` from 7.79.0 to 7.80.0
- [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](https://github.com/react-hook-form/react-hook-form/compare/v7.79.0...v7.80.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.80.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>
2026-06-21 12:38:16 +01:00
dependabot[bot]
b9ec85779f
build(deps): bump pg from 8.21.0 to 8.22.0 (#7985)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.21.0 to 8.22.0.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.22.0/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-20 16:02:17 +01:00
dependabot[bot]
acba429e1a
build(deps): bump actions/checkout from 6 to 7 (#7977)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 12:47:45 +01:00
John McLear
8c5de8446c
fix(bin): migrate importSqlFile & migrateDirtyDBtoRealDB to ueberdb2 promise API (#7983)
Both scripts still called the pre-v6 callback-style ueberdb2 API, producing
type errors (masked in places by `// @ts-ignore`) against the current
promise-based signatures (`set(key, value)`, `init()`, `close()` — no
callback/extra args):

  importSqlFile.ts(73)            initDb(null)        Expected 0 arguments, but got 1
  migrateDirtyDBtoRealDB.ts(51)   db.set(k,v,bcb,wcb) Expected 2 arguments, but got 4
  migrateDirtyDBtoRealDB.ts(56)   db.close(null)      Expected 0 arguments, but got 1
  migrateDirtyDBtoRealDB.ts(57)   dirty.close(null)   Expected 0 arguments, but got 1

- importSqlFile: drop the unused `util` import and the `util.promisify`
  wrappers; `await db.init()`, `await db.set(...)`, `await db.close()`
  directly. Removes two `// @ts-ignore` that were hiding the broken calls.
- migrateDirtyDBtoRealDB: replace the bcb/wcb callback machinery with
  `await db.set(key, value)` in the loop and call `close()` with no args.
  Also fixes the progress log which referenced an undefined `length`
  instead of `keys.length`.

Pure type/correctness cleanup; behaviour is unchanged (writes are now
awaited, which is equivalent or safer). `tsc --noEmit` on the bin package
is now clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:51:34 +01:00
John McLear
9d2dae1b63
fix(bin): close DBs in migrateDB so it flushes and exits (#7982)
`bin/migrateDB.ts` opens a source and target ueberdb2 Database, copies all
keys, then resolves without closing either connection or calling
process.exit(). Two problems with ueberdb2 6.1.x:

- 6.1.x keeps an internal keep-alive timer running until close() is called,
  so the migration process hangs forever after "Done syncing dbs" instead
  of exiting. (Pre-6.1.x it exited on its own once the work was done.)
- Target writes are buffered and only guaranteed flushed to disk on close()
  /flush(), so an operator who Ctrl-Cs the apparently-finished process could
  end up with an incomplete migration.

Close the target then the source on both the success and error paths (which
flushes buffered writes and clears the keep-alive timer) and exit with an
explicit status code, matching the pattern already used in
migrateDirtyDBtoRealDB.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:48:39 +01:00
John McLear
32249d99e2
fix(ci): reap whole server tree in installer smoke test so it can't hang 6h (#7981)
The "Installer test" workflow has hung for 6 hours (until GitHub's job
ceiling cancels it) on every ubuntu/macos run since v3.2.0. The smoke
test starts `pnpm run prod` in the background, confirms /api responds,
then tears it down with:

    kill "$PID"
    wait "$PID"

`pnpm run prod` is a nested launcher (pnpm -> pnpm --filter -> node), so
$PID is only the outer pnpm. SIGTERM is forwarded down the chain and the
script then `wait`s on it, but if the node server doesn't exit (e.g. a
live flush timer keeping the event loop alive) the wait blocks forever
and the step never releases its output pipe -> 6h hang. Windows passed
because it uses `Stop-Process -Force`.

Fix the teardown to be robust regardless of server shutdown behaviour:
- `set -m` so the launcher gets its own process group
- kill the whole group (SIGTERM, then SIGKILL fallback) via a trap
- drop the blocking `wait`
- add `timeout-minutes: 8` to both smoke steps as a hard backstop so a
  future hang fails in minutes, not 6 hours

This unblocks CI on PRs that touch the installer workflow. The
underlying clean-shutdown regression (server not exiting on SIGTERM,
likely the ueberDB flush-timer setInterval) is tracked separately.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:37:04 +01:00
dependabot[bot]
2109c05ad4
build(deps): bump undici from 7.27.2 to 8.5.0 (#7980)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 22:31:52 +01:00
dependabot[bot]
f6f665ff60
build(deps-dev): bump the dev-dependencies group with 2 updates (#7978)
Bumps the dev-dependencies group with 2 updates: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) and [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify).


Updates `lucide-react` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.21.0/packages/lucide-react)

Updates `oxc-minify` from 0.136.0 to 0.137.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.137.0/napi/minify)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.137.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>
2026-06-18 21:48:32 +01:00
dependabot[bot]
b680076534
build(deps): bump ueberdb2 from 6.1.9 to 6.1.13 (#7979)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.9 to 6.1.13.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.9...v6.1.13)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.13
  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>
2026-06-18 21:47:16 +01:00
translatewiki.net
b1792469b1
Localisation updates from https://translatewiki.net. 2026-06-18 14:02:55 +02:00
dependabot[bot]
3aa13197e4
build(deps): bump nodemailer from 9.0.0 to 9.0.1 (#7976)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 9.0.0 to 9.0.1.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.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>
2026-06-18 10:19:51 +01:00
dependabot[bot]
dfdbceebe2
build(deps-dev): bump the dev-dependencies group across 1 directory with 7 updates (#7970)
Bumps the dev-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.16` | `1.2.17` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.5` | `1.2.6` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.61.0` | `8.61.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.61.0` | `8.61.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.18.0` | `1.20.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.17.0` | `7.18.0` |



Updates `@radix-ui/react-dialog` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.16 to 1.2.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-visually-hidden` from 1.2.5 to 1.2.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

Updates `@typescript-eslint/eslint-plugin` from 8.61.0 to 8.61.1
- [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.61.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.61.0 to 8.61.1
- [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.61.1/packages/parser)

Updates `lucide-react` from 1.18.0 to 1.20.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react)

Updates `react-router-dom` from 7.17.0 to 7.18.0
- [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.18.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 1.20.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.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>
2026-06-17 22:23:04 +01:00
John McLear
a698e34772
chore(release): park the non-functional ep_etherpad npm publish (#7922)
The releaseEtherpad workflow renames ep_etherpad-lite -> ep_etherpad and
publishes ./src to npm, but that publish is not load-bearing:

- `ep_etherpad` has 0 dependents on npm; nothing in this repo depends on it.
- Plugins import `ep_etherpad-lite` resolved from the LOCAL core install, and
  plugin CI clones `ether/etherpad` rather than `npm install`-ing core.
- Etherpad is run via git clone / Docker / zip / snap, never `npm install`.

It has been failing with E404 (the ep_etherpad package has no OIDC trusted
publisher configured on npmjs.com), which is why npm is stuck at 2.5.0 while
3.0/3.1/3.2/3.3 shipped fine without it.

Rather than chase a trusted-publisher setup for a publish nobody consumes,
park the workflow: gate the job behind an explicit `confirm: true` dispatch
input so a stray run fails fast with a clear message instead of a confusing
404, and document the status in the header + the AGENTS.MD Releasing section.

This is the package owner's (samtv12345) call: either finish the trusted-
publisher config to revive it, or remove the workflow. Parked pending that
decision; nothing about the release depends on it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:41 +01:00
John McLear
ce038b0b0f
fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959) (#7960)
* fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959)

The token-less "Delete pad" button (#delete-pad) was nested inside the
enablePadWideSettings-gated pad-settings section, so disabling pad-wide
settings removed the only way to delete a pad without a recovery token.
Combined with #7926 hiding the token disclosure when deletion needs no
token (e.g. allowPadDeletionByAllUsers), a user who was allowed to delete
could be left with no deletion UI at all.

Pad deletion is unrelated to pad-wide settings, so:

- Move #delete-pad out of the enablePadWideSettings block in pad.html; it
  is now always rendered and hidden by default.
- Add a canDeletePad clientVar (isCreator || allowPadDeletionByAllUsers)
  and drive the button's visibility from it in pad_editor.ts, mirroring the
  existing canDeleteWithoutToken handling for the token disclosure.

The two controls are now mutually coherent and neither depends on
enablePadWideSettings: the plain button shows when this session can delete
without a token, the recovery-token disclosure shows otherwise.

Tests:
- backend padDeletionUiPlacement.ts: #delete-pad is rendered with
  enablePadWideSettings both on and off (fails without the template move).
- backend socketio.ts: canDeletePad reflects the creator/allow-all matrix,
  including a non-creator who only gains it under allowPadDeletionByAllUsers.
- frontend pad_settings.spec.ts: asserts #delete-pad is no longer a
  descendant of #pad-settings-section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pad): never let readonly sessions delete via token-less paths (#7959)

Qodo review of #7960: `canDeletePad` was `isCreator || allowPadDeletionByAllUsers`,
so under allowPadDeletionByAllUsers a readonly viewer received
canDeletePad=true and the relocated #delete-pad button unhid for them.
Worse, the server-side handlePadDelete `flagOk`/`creatorOk` branches never
checked session.readonly either, so a readonly-link holder could actually
delete the pad without a token — a data-loss hole that the new always-rendered
button would expose.

Exclude readonly sessions from both the clientVar and the server's token-less
authorization paths. A valid recovery token (tokenOk) stays a sufficient
credential regardless of session mode.

Test: socketio.ts asserts a readonly viewer gets canDeletePad=false and that a
token-less PAD_DELETE from a readonly session leaves the pad intact (red before
this change on the clientVar assertion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:23 +01:00
dependabot[bot]
d67aa3ebd3
build(deps): bump undici from 8.4.1 to 8.5.0 (#7972)
Bumps [undici](https://github.com/nodejs/undici) from 8.4.1 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.4.1...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.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>
2026-06-17 15:17:49 +01:00
dependabot[bot]
4ba72b3919
build(deps): bump oidc-provider from 9.8.4 to 9.8.5 (#7973)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.4 to 9.8.5.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.4...v9.8.5)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.8.5
  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>
2026-06-17 15:17:36 +01:00
dependabot[bot]
bfe3fad256
build(deps): bump @radix-ui/react-switch from 1.3.0 to 1.3.1 (#7974)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.3.0 to 1.3.1.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.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>
2026-06-17 15:17:10 +01:00
John McLear
3e53962fb5
fix(deps): force @opentelemetry/core >=2.8.0 (CVE-2026-54285) (#7975)
Pin the transitive @opentelemetry/core dep (pulled in via
@elastic/elasticsearch -> @elastic/transport) to >=2.8.0 to clear
GHSA-8988-4f7v-96qf / CVE-2026-54285: W3CBaggagePropagator.extract()
did not enforce W3C size limits on inbound baggage headers, allowing
unbounded memory allocation. @elastic/transport declares the dep as
"2.x" so 2.8.0 satisfies the existing range with no parent bump, and
2.8.0's @opentelemetry/api peer range (>=1.0.0 <1.10.0) is satisfied
by the 1.9.1 already in the tree.

Override added to pnpm-workspace.yaml alongside the other CVE
force-bumps (pnpm 11 ignores root package.json pnpm.overrides).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:53:20 +01:00
John McLear
a59b310dbf
deps: pin ueberdb2 to 6.1.9 (fixes standalone-server startup exit) + run deb smoke on PRs (#7969)
ueberdb2 6.1.10 rewrote the cache/buffer layer (CacheAndBufferLayer.ts),
replacing the constructor's always-on, *referenced* `setInterval` flush timer
with a lazily-armed `setTimeout` that is `.unref()`'d and only created when
there are dirty keys. On a fresh, empty dirty DB there are no dirty keys, so no
flush timer is ever armed and ueberdb2 no longer anchors Node's event loop. In
the packaged (.deb/systemd) production boot this exposes a startup window where
the loop has no referenced handle and the process exits cleanly (code 0) before
`server.listen()` binds the port — so the server never serves /health.

Symptom on develop: the Debian-package amd64 smoke test failed on three
consecutive pushes (#7966 ueberdb2 6.1.9->6.1.12, then #7965, #7967), with the
service logging the version banner then "Deactivated successfully" and the
health check on :9001 never connecting. Backend/Docker/downstream-smoke stayed
green because they keep the loop alive by other means; only the bare
fresh-empty-dirty-DB packaged boot hits the gap. ueberdb2 6.1.12 is the latest
published release, so there is no fixed version to roll forward to yet — pin
back to the last green release (6.1.9) on both src/ and bin/.

Also add a `pull_request` trigger to the Debian-package workflow (scoped to the
same production-footprint paths). The smoke step is the only check that catches
this "boots then exits before binding" class of regression, but it previously
ran only on push to develop — i.e. *after* merge — which is exactly why the
Dependabot bump turned develop red instead of being blocked at PR time. The
release/apt-publish jobs are tag-guarded, so PRs run the build+smoke job only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:39:37 +01:00
forkivan
0c0f5a8ec4
PadManager: reject unreachable '.' and '..' pad ids (#7962)
* PadManager: reject unreachable '.' and '..' pad ids

isValidPadId accepted pad ids that consist only of URL dot-segments
('.' and '..'). Per the WHATWG URL standard a browser normalises
"/p/." to "/p/" and "/p/.." to "/", so such a pad can never be opened
or exported: the request arrives at "/p/" and Etherpad answers
"Cannot GET /p/". The pad is created in the database but is forever
unreachable.

Reject these ids in isValidPadId so the broken pads can no longer be
created, and add a regression test that fails without the fix.

* adminsettings: allow deleting legacy '.'/'..' pads

The isValidPadId tightening makes getPad() reject the pad ids '.' and
'..', which also blocks their deletion: a pad with such an id created
before the change still exists (doesPadExists is true), so the admin
deletePad handler takes the "healthy" branch where getPad() now throws.
The outer catch swallowed that error without emitting a terminal
results:deletePad, leaving an undeletable orphan in the admin UI.

Fall back to the existing raw key purge when getPad() throws, so these
pads can still be removed. Adds a regression test.

* tests: run the isValidPadId regression under the mocha suite

The original unit test lived in the vitest backend-new suite, but PadManager
loads DB, Pad and customError with CommonJS require() at import time. Under
vitest those require() calls are resolved by Node natively and fail on the .ts
sources ("Cannot find module '../utils/customError'"); vi.mock could not
intercept them, so the suite errored before any test ran. It also used a
top-level `await import`, which tripped tsc TS1378 under the project tsconfig.

Move the test to the mocha backend suite, which runs with --import=tsx and
resolves the .ts requires natively, so PadManager can be required directly with
no mocking. isValidPadId is a pure function and DB only connects lazily in
DB.init(), so loading the module has no side effects and no database is needed.
2026-06-16 11:13:02 +01:00
John McLear
099de84cd1
deps: resolve open Dependabot security alerts (#7967)
* deps: resolve open Dependabot security alerts

Bump transitive dependencies flagged by Dependabot via pnpm-workspace
overrides. Refreshes stale override floors that the advisory ranges have
since grown past, and adds overrides for newly-flagged packages:

- form-data  -> >=4.0.6  (GHSA-hmw2-7cc7-3qxx, high)
- ws         -> >=8.21.0 (GHSA-96hv-2xvq-fx4p / GHSA-58qx-3vcg-4xpx, high/med)
- esbuild    -> >=0.28.1 (GHSA-gv7w-rqvm-qjhr / GHSA-g7r4-m6w7-qqqr, high/low)
- basic-ftp  -> >=5.3.1  (GHSA-rpmf-866q-6p89, high) [stale floor: 5.3.0 still vuln]
- tar        -> >=7.5.16 (GHSA-vmf3-w455-68vh, med)  [stale floor: 7.5.11]
- js-yaml    -> >=4.2.0  (GHSA-h67p-54hq-rp68, med)  [stale floor: 4.1.1 now vuln]
- qs         -> >=6.15.2 (GHSA-q8mj-m7cp-5q26, med)  [stale floor: 6.14.2 still vuln]
- ip-address -> >=10.1.1 (GHSA-v2v4-37r5-5v8g, med)
- @babel/core-> >=7.29.6 (GHSA-4x5r-pxfx-6jf8, low)

ts-check, backend test-utils, and the vite/esbuild production build all
pass locally on Node 24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* deps: cap basic-ftp override to 5.x to avoid major bump

Qodo flagged the open-ended `basic-ftp@<5.3.1: '>=5.3.1'` override as
resolving to 6.0.1 (a surprise major) on the runtime plugin-install path
(live-plugin-manager -> proxy-agent -> get-uri -> basic-ftp). The CVE fix
(5.3.1) exists on the 5.x line, so bound the override to '>=5.3.1 <6.0.0'
for the minimal, lowest-risk patch. Now resolves to basic-ftp@5.3.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:32:06 +01:00
dependabot[bot]
8a4e1feed8
build(deps): bump awalsh128/cache-apt-pkgs-action from 1.6.0 to 1.6.1 (#7963)
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.6.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>
2026-06-15 19:08:07 +01:00
dependabot[bot]
2fdf202089
build(deps): bump nodemailer from 8.0.11 to 9.0.0 (#7965)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.11 to 9.0.0.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:08:00 +01:00
dependabot[bot]
abdfa03f84
build(deps): bump ueberdb2 from 6.1.9 to 6.1.12 (#7966)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.9 to 6.1.12.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.9...v6.1.12)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.12
  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>
2026-06-15 19:07:47 +01:00
dependabot[bot]
c8e3248bcd
build(deps-dev): bump the dev-dependencies group with 6 updates (#7964)
Bumps the dev-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` |
| [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.78.0` | `7.79.0` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.135.0` | `0.136.0` |


Updates `@playwright/test` from 1.60.0 to 1.61.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/HEAD/packages/vitest)

Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3)

Updates `react-hook-form` from 7.78.0 to 7.79.0
- [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](https://github.com/react-hook-form/react-hook-form/compare/v7.78.0...v7.79.0)

Updates `oxc-minify` from 0.135.0 to 0.136.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.136.0/napi/minify)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.79.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.136.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>
2026-06-15 16:38:06 +01:00
translatewiki.net
4371af77c2
Localisation updates from https://translatewiki.net. 2026-06-15 14:03:51 +02:00
John McLear
2ba72de1de
feat(pad): suppress deletion token for durable identities + relabel recovery action (#7926) (#7930)
* feat(pad): suppress deletion token for durable identities; relabel recovery action (#7926)

Builds on the allowPadDeletionByAllUsers suppression with the rest of the
ideas discussed on the issue.

Server (handleClientReady):
- A creator's deletion token is now also withheld when they have a *durable*
  identity: authenticated (req.session.user with a username) AND the deployment
  pins that identity to a stable authorID via a getAuthorId hook. Only then does
  the creator path (author === revision-0 author) survive a cookie clear or a
  different device, making the recovery token redundant.
- This deliberately tightens the previous `requireAuthentication => always
  suppress` rule: without a getAuthorId hook the authorID still comes from the
  per-browser token cookie, so an authenticated user on a second device is NOT
  the creator. Withholding the token there would strand them, so they now keep
  getting one. SSO deployments using the documented getAuthorId pattern get the
  clean no-modal experience.
- New `canDeleteWithoutToken` clientVar (allowPadDeletionByAllUsers OR durable
  identity) drives the client label.

Client (pad_editor.ts):
- When canDeleteWithoutToken, the recovery-token disclosure summary is labelled
  plainly "Delete Pad" (reusing the already-translated pad.settings.deletePad
  key) instead of the jargon "Delete with token".

Tests (socketio.ts): anonymous -> token; allowPadDeletionByAllUsers -> none;
authenticated without a getAuthorId hook -> token; authenticated with one ->
none. Verified in a browser for both label/modal outcomes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pad): render recovery disclosure for all sessions; hide it (not relabel) when no token is needed

Addresses Qodo review on #7930:

1. Token UI fully suppressed when not needed (was: only the summary relabelled).
   When canDeleteWithoutToken (allowPadDeletionByAllUsers or a durable identity),
   pad_editor.ts now hides the whole #delete-pad-with-token disclosure — label,
   token field and submit — so no deletion-token wording remains. With the plain
   "Delete Pad" button present this is also the cleaner UX.

2. Recovery disclosure no longer gated on !requireAuthentication. Because a token
   can now be issued under requireAuthentication when the deployment lacks a
   durable getAuthorId mapping, the template must render the recovery form there
   too — otherwise an authenticated creator gets a token with no UI to enter it
   on another device. It is rendered hidden by default and shown by the client
   only when canDeleteWithoutToken is false.

3. API.createPad aligned: also returns null deletionToken under
   allowPadDeletionByAllUsers, matching the socket/UI path.

Tests: deletePad.ts gains a createPad-under-allowPadDeletionByAllUsers case.
Verified live in Chromium: allowAll -> disclosure hidden, no modal; default
anonymous -> disclosure visible, modal shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:47:11 +01:00
dependabot[bot]
6a5ad2d90d
build(deps-dev): bump lucide-react in the dev-dependencies group (#7951)
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.17.0 to 1.18.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.18.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>
2026-06-13 14:34:55 +01:00
John McLear
f79cabe74c
Env-var overrides for offline/air-gapped installs (update check, plugin catalog, updater) (#7917)
* feat(settings): env-var overrides for update-check/plugin-catalog/updater (offline installs)

Air-gapped and firewalled deployments could not disable Etherpad's outbound
calls (the hourly version check, the admin plugin catalogue, and the
self-updater) without editing settings.json inside the container image — the
shipped settings.json.docker hardcoded updates.tier and omitted the privacy
block entirely, so there was no env-var to flip.

Wire the relevant keys through the existing ${ENV:default} substitution in both
settings.json.docker and settings.json.template:

  - PRIVACY_UPDATE_CHECK            (privacy.updateCheck, default true)
  - PRIVACY_PLUGIN_CATALOG          (privacy.pluginCatalog, default true)
  - UPDATES_TIER                    (updates.tier, default notify; "off" = no calls)
  - UPDATES_SOURCE / UPDATES_CHANNEL / UPDATES_CHECK_INTERVAL_HOURS /
    UPDATES_GITHUB_REPO / UPDATES_REQUIRE_ADMIN_FOR_STATUS (docker)
  - UPDATE_SERVER                   (updateServer endpoint)

Document the full set in doc/docker.md (new "Updates & privacy" section) and
cross-link from doc/admin/updates.md. Add backend regression tests that parse
the shipped settings.json.docker and settings.json.template and assert the
overrides apply with correct boolean/numeric coercion, so a future edit that
drops the ${ENV} placeholders fails loudly.

Addresses #7911 (item 2). No code changes — config + docs + tests only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: address Qodo review — point to PRIVACY.md, clarify tier=off scope

- Outbound-call docs/comments referenced doc/privacy.md (the storage/logging
  doc); the canonical outbound-call inventory is repo-root PRIVACY.md, which the
  runtime messages in UpdateCheck.ts / Settings.ts also reference. Re-point
  settings.json.{template,docker} and doc/docker.md there.
- doc/admin/updates.md said updates.tier="off" means "no HTTP request will leave
  the instance", but the legacy UpdateCheck.ts call to ${updateServer}/info.json
  is gated by privacy.updateCheck, not updates.tier. Clarify that air-gapped
  installs must set PRIVACY_UPDATE_CHECK=false too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: link PRIVACY.md via absolute URL to satisfy VitePress dead-link check

PRIVACY.md lives at the repo root, outside the doc/ tree VitePress builds, so a
relative link to it (../PRIVACY.md / ../../PRIVACY.md) is flagged as a dead link
and fails `docs:build`. Use the absolute GitHub URL instead, matching how
doc/configuration.md already links settings.json.template.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:32:46 +01:00
dependabot[bot]
662678275a
build(deps): bump esbuild from 0.28.0 to 0.28.1 (#7952)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.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>
2026-06-12 21:29:42 +02:00
dependabot[bot]
8e4eb2646a
build(deps): bump semver from 7.8.3 to 7.8.4 (#7943)
Bumps [semver](https://github.com/npm/node-semver) from 7.8.3 to 7.8.4.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.3...v7.8.4)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.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>
2026-06-12 12:42:25 +01:00
dependabot[bot]
e313366b49
build(deps): bump nodemailer and @types/nodemailer (#7950)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) and [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer). These dependencies needed to be updated together.

Updates `nodemailer` from 8.0.10 to 8.0.11
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.10...v8.0.11)

Updates `@types/nodemailer` from 8.0.0 to 8.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/nodemailer"
  dependency-version: 8.0.1
  dependency-type: direct:development
  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>
2026-06-12 12:42:14 +01:00
dependabot[bot]
3cddff7b27
build(deps): bump mongodb from 7.2.0 to 7.3.0 (#7941)
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 7.2.0 to 7.3.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v7.2.0...v7.3.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 12:36:39 +01:00
dependabot[bot]
817ca2546e
build(deps-dev): bump @types/node in the dev-dependencies group (#7944)
Bumps the dev-dependencies group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 25.9.2 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  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>
2026-06-12 12:15:23 +01:00
dependabot[bot]
eefdfc44b9
build(deps): bump pdfkit from 0.19.0 to 0.19.1 (#7945)
Bumps [pdfkit](https://github.com/foliojs/pdfkit) from 0.19.0 to 0.19.1.
- [Release notes](https://github.com/foliojs/pdfkit/releases)
- [Changelog](https://github.com/foliojs/pdfkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/foliojs/pdfkit/compare/v0.19.0...v0.19.1)

---
updated-dependencies:
- dependency-name: pdfkit
  dependency-version: 0.19.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>
2026-06-12 12:15:17 +01:00
John McLear
73a9b88bb4
test(timeslider): port legacy mocha specs to Playwright, retire orphaned suite (#7949)
* test(timeslider): port legacy mocha specs to Playwright, retire orphaned suite

The legacy src/tests/frontend/specs/ mocha suite is run by no CI workflow,
so its timeslider coverage was dead — a regression in the in-pad history UI
(#7659/#7946) sailed through CI. Port the still-meaningful cases to
frontend-new Playwright specs, re-targeted at the real in-pad UI (outer
banner / slider / export links that pad_mode.ts drives) rather than the
isolated ?embed=1 iframe the existing specs use:

- timeslider_revision_labels.spec.ts (from timeslider_labels.js): the
  #history-banner shows 'Version N' + a valid (non-NaN) date and timer, and
  both update when scrubbing to revision 0.
- timeslider_export_links.spec.ts (from timeslider_numeric_padID.js and the
  'checks the export url' case of timeslider_revisions.js): the outer export
  hrefs target /p/<pad>/<rev>/export/<type> for the viewed revision, including
  a numeric pad id, and follow the slider to revision 0.
- timeslider_deeplink.spec.ts (from the 'jumps to a revision given in the url'
  case): a #rev/N hash — and the legacy #N shortlink form — boots straight
  into history mode at that revision.

Delete the three now-ported legacy specs. Verified passing on Chromium and
Firefox. The star-marker case of timeslider_revisions.js is already covered by
timeslider_saved_revisions.spec.ts (#7948).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(timeslider): make deep-link spec robust on Firefox

Assert the canonical #rev/0 URL and the slider landing on revision 0 rather
than the #history-banner-rev label text. The banner label is populated via a
MutationObserver bridge that races the iframe load on the bootstrap path in
Firefox (it is already covered for the normal button-entry flow by
timeslider_revision_labels.spec.ts); the URL + slider value are the
deterministic signals that the deep link entered history at the right revision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(timeslider): address Qodo review on #7949

- Pin locale to en-US in the revision-labels spec so the localized 'Version N'
  label and 'Saved <Month> <day>, <year>' date assertions are deterministic and
  the date stays Date-parseable, instead of depending on the runner's locale.
- Use a high-entropy numeric pad id (timestamp + random) in the export-links
  spec so reruns against a persistent DB can't collide on the same pad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:14:39 +01:00
John McLear
a65e3b75a4
fix(pad): show saved-revision markers in in-pad history mode (#7946) (#7948)
* fix(pad): show saved-revision markers in in-pad history mode (#7946)

When #7659 moved the timeslider into the pad as an embedded iframe, the
user-facing control became the outer #history-slider-input range input.
The saved-revision stars are still drawn by broadcast_slider.ts into the
iframe's #ui-slider-bar, but that DOM is hidden in embed mode, and
pad_mode.ts bridged rev/max/value/timer/authors to the outer slider but
never the saved revisions. Result: clicking "Save Revision" appeared to
work but no markers showed in the timeslider (regression in 3.3.x).

Bridge the embedded timeslider's clientVars.savedRevisions onto the outer
slider as percentage-positioned star markers, rendered on first sync and
re-rendered when the slider max changes. Markers are a purely visual,
aria-hidden overlay (keyboard/SR users already reach any revision via the
slider + step buttons) with a click-to-seek convenience for mouse users.

Adds a frontend-new Playwright spec exercising the real user flow (save a
revision in the pad, enter in-pad history mode, assert a visible marker on
the outer slider). The only prior coverage lived in the legacy mocha
suite (src/tests/frontend/specs/timeslider_revisions.js) which no CI
workflow runs, and the modern timeslider specs drive the ?embed=1 iframe
directly and never exercise the outer history UI — so this regression was
invisible to CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pad): live saved-revision markers + Qodo review fixes (#7946)

Address Qodo review on #7948:

- Live updates (bug 1): the server's SAVE_REVISION handler never broadcast
  NEW_SAVEDREV, so the client handler that adds a star was dead and no open
  timeslider ever updated live. Wire it up: Pad.addSavedRevision returns the
  new revision (undefined on duplicate); handleSaveRevisionMessage broadcasts
  NEW_SAVEDREV to the pad room. pad_mode.ts now sources outer markers from the
  embedded slider's live #ui-slider-bar .star DOM (labels from clientVars) and
  observes that bar so a revision saved by a collaborator appears on an
  already-open history slider. Live editors ignore the unknown message type.

- Single-revision pad (bug 2): allow max === 0 so a revision saved at rev 0
  still renders a marker instead of being cleared by the old max <= 0 guard.

- Test rigor (bug 3): assert the marker's inline left percentage directly
  instead of falling back to a layout coordinate, which let left:0% pass.

Adds a two-client Playwright test for the live path (verified it fails with
the server broadcast removed). Backend Pad + pad API specs (88) still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:12:36 +01:00
translatewiki.net
8b6a0a2f3a
Localisation updates from https://translatewiki.net. 2026-06-11 14:04:05 +02:00
Etherpad Release Bot
92e23bf7d4 Merge branch 'develop' 2026-06-10 10:03:40 +00:00
Etherpad Release Bot
4fe94c9b68 Merge branch 'master' into develop 2026-06-10 10:03:40 +00:00
Etherpad Release Bot
59062ce11a bump version 2026-06-10 10:03:40 +00:00
John McLear
fb7e7af6c9
chore(release): prep 3.3.1 — bump ueberdb2 to ^6.1.9 + changelog (#7939)
Bumps ueberdb2 6.1.8 -> 6.1.9 (postgres pool-error handling + TCP
keep-alive for #7878; redis/rethink connection-error handlers so a
dropped DB connection no longer crashes the process) and adds the
3.3.1 CHANGELOG entry required by bin/release.ts before the release
workflow can run.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 10:48:32 +01:00
John McLear
ddf84fa6f2
fix(admin): don't let one unreadable pad empty the Manage-pads list (#7935) (#7938)
* fix(admin): don't let one unreadable pad empty the Manage-pads list (#7935)

The admin /settings `padLoad` handler hydrates every pad via getPad() to
build the listing (the default lastEdited sort forces a full scan).
findKeys('pad:*','*:*:*') returns every key under the `pad:` prefix,
including legacy/foreign or migration-corrupted records — e.g. a value
stored as a JSON string rather than a pad object, which a botched
dirty.db -> PostgreSQL migration produces. Loading such a record makes
Pad.init throw `Cannot use 'in' operator to search for 'pool'`.

The handler had no try/catch, so one bad record rejected the whole
request: no `results:padLoad` was emitted (the SPA showed an empty
"No results" state forever — the reported symptom) and the unhandled
rejection could exit the server.

Make loadMeta resilient — a failed getPad() logs a warning and returns
zeroed metadata so the bad pad still surfaces for deletion instead of
hiding every other pad — and wrap the handler so it always emits a
terminal reply and never bubbles to the process-level handler.

Tests:
- backend: tests/backend/specs/admin/padLoadResilience.ts asserts a
  corrupt pad:* record no longer hides the readable pads (fails without
  the fix: no results:padLoad reply + server exit).
- e2e: tests/frontend-new/admin-spec/admin_pads_page.spec.ts covers
  create pad -> welcome-page recent-pads -> /admin Manage-pads UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(admin): sanitize padLoad error logs to avoid leaking pad content

Qodo review: the error logs (and the error field returned to the SPA)
used `${err}` / err.message verbatim. For the corruption case the
TypeError message embeds the raw stored value ("...'pool' in <value>"),
which for a real pad can be document text — so logging it verbatim could
leak content, bloat logs, and let embedded newlines forge log lines.

Add a safeErr() helper (error name + single-line, 120-char-capped
message, control chars stripped) and use it in both the per-pad warning
and the outer handler error log, and for the error field emitted to the
client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(admin): make surfaced corrupt pads deletable from the admin UI

Qodo review: padLoad now surfaces unreadable pad:* records (zeroed
metadata) so admins can delete them, but deletePad's normal path
(doesPadExists -> getPad -> Pad.remove) can't touch such a record:
doesPadExists() returns false for a non-object value and getPad() throws,
so the surfaced pad was effectively undeletable and the handler emitted
nothing.

When doesPadExists() is false but a raw `pad:${id}` value is present,
fall back to a raw key purge: sweep sub-keys (revs/chat/deletionToken/…)
via findKeys, drop the readonly mapping, then padManager.removePad()
(which removes the main key + pad-list + cache entry). Always emit a
terminal results:deletePad reply (including for an already-absent id) so
the UI clears the row instead of silently doing nothing, and wrap the
handler so failures are logged (sanitized) rather than swallowed.

Adds a backend test asserting a surfaced corrupt pad can be deleted and
disappears from the listing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(admin): assert corrupt-pad delete via listing, not raw db.get

The delete test probed `db.get('pad:<id>')` for null right after deletePad.
That passed on postgres but failed on the dirty backend (Windows CI):
ueberdb2's per-backend read/write buffering can return the just-removed
value immediately after remove(), so it asserted storage internals rather
than behaviour. The deletion is still durable (same db.remove() every pad
uses; the in-memory pad-list entry is dropped synchronously). Assert the
behavioural outcome instead — the corrupt pad disappears from the padLoad
listing while the good pad remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 10:46:37 +01:00
John McLear
f47626049c
fix(pad): don't issue a deletion token (or show its modal) when allowPadDeletionByAllUsers is on (#7929)
When `allowPadDeletionByAllUsers` is true, anyone can delete a pad with no
token at all (handlePadDelete's flagOk branch), so the one-time deletion
token is pointless and the "Save your pad deletion token" modal only
overwhelms users who will never need it.

Gate token issuance on `!settings.allowPadDeletionByAllUsers` so no token
reaches clientVars; the client's showDeletionTokenModalIfPresent() then
returns early and the modal never appears. No new setting — it derives
automatically from the existing one.

Closes #7926.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:55:13 +01:00
dependabot[bot]
1e7c4b8c21
build(deps): bump rate-limiter-flexible from 11.1.1 to 11.2.0 (#7934)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 11.1.1 to 11.2.0.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](https://github.com/animir/node-rate-limiter-flexible/compare/v11.1.1...v11.2.0)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-version: 11.2.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>
2026-06-10 09:54:21 +01:00
dependabot[bot]
ad2e8b18a9
build(deps): bump semver from 7.8.2 to 7.8.3 (#7933)
Bumps [semver](https://github.com/npm/node-semver) from 7.8.2 to 7.8.3.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.2...v7.8.3)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.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>
2026-06-10 09:54:13 +01:00
dependabot[bot]
12bd199f94
build(deps-dev): bump the dev-dependencies group with 2 updates (#7932)
Bumps the dev-dependencies group with 2 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser).


Updates `@typescript-eslint/eslint-plugin` from 8.60.1 to 8.61.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.61.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.60.1 to 8.61.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.61.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.61.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>
2026-06-10 09:53:48 +01:00
John McLear
d828185efc
security: escape and integer-coerce numbered-list start attribute (GHSA-f7h5-v9hm-548j) (#7937)
The numbered-list branch of `domline.appendSpan` interpolated the line's
`start` attribute value into an `<ol start=...>` tag unquoted and unescaped,
then assigned the result to `node.innerHTML`. The value comes verbatim from
the attribute pool, which an attacker can populate via a crafted `.etherpad`
import (`ImportEtherpad.setPadRaw` validates attribute keys but not values).
A space-free value such as `1><svg/onload=...>` therefore broke out of the
tag and produced a live element, executing attacker JavaScript in the pad's
origin for every subsequent viewer of the pad or timeslider.

An `<ol>` start is only ever an integer, so coerce it with `Number.parseInt`,
omit the attribute entirely when the value is not a number, and HTML-escape +
quote it (the same `Security.escapeHTMLAttribute` treatment the sibling
`listType` class already gets, and that #7905 applied to the export sink).

Adds a backend regression test that drives the real domline render path
through jsdom and asserts the malicious value cannot produce a live element.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:53:26 +01:00
Stefan Müller
68d322957f
feat: Add test step to check etherpad works offline (#7936) 2026-06-10 09:53:16 +01:00
John McLear
5b146f0b03
fix(pad): show detected language in settings dropdown (#7925) (#7928)
* fix(pad): show detected language in settings dropdown (#7925)

When the UI language was auto-detected from the browser (no language
cookie and no pad-wide lang set), refreshMyViewControls() and
refreshPadSettingsControls() set the language dropdown to
`<option>.lang || 'en'`. Since the detected language lives only in
html10n (not in padOptions/effectiveOptions), the value was undefined
and the dropdown fell back to hardcoded English — even though the pad
UI itself rendered correctly in the detected language.

Fall back to html10n.getLanguage() before 'en' so the dropdown reflects
the language actually rendered. getLanguage() returns the matched
lowercase locale key, which matches the <option value> keys.

Adds a regression test that loads a pad under a de-DE browser locale and
asserts the dropdown reads "Deutsch" without any manual selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(language): assert German toolbar tooltip (Qodo #7928)

The detection-works precondition computed a boolean via locator.evaluate()
but never asserted it, so it could not fail. Assert the bold button's
parent title is the German "Fett (Strg-B)" with toHaveAttribute instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:23:01 +01:00
John McLear
67c0e391fd
fix(skin): paint the root canvas so iOS dark mode has no white status bar (#7606) (#7931)
The OP still saw a white strip above the dark pad on iOS Safari even with the
theme-color metas in place. theme-color only tints the address-bar chrome;
the status-bar safe area at the very top is painted by iOS from the ROOT
canvas background. The colibris background variants only colour inner
containers (#editorcontainerbox, body descendants), leaving <html> and <body>
transparent (computed rgba(0,0,0,0)) with color-scheme: normal — so the canvas
fell back to the UA light default (white). Android tints its chrome from
theme-color, which is why it looked fine there but iOS did not.

Paint the <html> root per toolbar variant with the toolbar colour (matching
theme-color and the OP's request that the bar match the toolbar) and set
color-scheme so the safe area, toolbar, and address bar are one seamless
colour. Verified in a mobile viewport: dark-OS root background is now
rgb(72,83,101) (#485365, == --super-dark-color, the toolbar colour); light-OS
stays white. Colours mirror skin_toolbar_colors.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:13:11 +01:00
John McLear
b420cf4850
ci(downstream): robust per-client error handling in run-clients.sh (#7927)
* ci(downstream): robust per-client error handling in run-clients.sh (Qodo)

- Move clone/fetch/checkout inside the per-client guarded block with explicit
  '|| exit 1' on every step. set -e is suspended inside a subshell used as an
  '||' operand, so relying on it silently swallowed clone/checkout failures
  (and continued from the wrong cwd); explicit guards make one client's failure
  a per-client fail=1 while the loop continues, and the run exits non-zero.
- Stop suppressing fetch errors; fetch only when the pinned commit isn't already
  reachable, and surface the real error.
- Run manifest commands via 'bash -c' instead of 'eval' (trusted in-repo
  allowlist; avoids double-parsing / leaking into this script's shell).

Verified: two bogus clients -> both reported, loop continues, exit 1; happy
path (cli, no server) -> vectors pass, smoke skips, exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(downstream): pin clients to their merged-commit SHAs

The three client Phase-2 PRs merged; bump each manifest ref from its
pre-merge PR-branch tip (now deleted / unfetchable) to its squash-merge
commit on main:
  etherpad-pad        -> 91620c6 (ether/pad#1)
  etherpad-cli-client -> ebc516e (ether/etherpad-cli-client#136)
  etherpad-desktop    -> ab83da6 (ether/etherpad-desktop#78)

Verified each merged main carries the test entry points
(vectors.rs/smoke.rs; test:vectors/test:smoke scripts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(downstream): run manifest commands under bash strict mode (Qodo)

bash -c spawns a fresh shell without -e/-u/-o pipefail, so a pipeline-stage
failure inside a client's vectorTest/smokeCmd could be masked. Use
'bash -euo pipefail -c' so those failures surface as a non-zero exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:31:05 +01:00
John McLear
e56a33efd0
ci(downstream): enable pad/cli/desktop smoke clients (Phase 2) (#7924)
Implements the per-kind orchestration (clone @ pinned ref, inject core's
freshly-generated wire-vectors fixture via ETHERPAD_WIRE_VECTORS, run each
client's vectorTest + smokeCmd against the booted server) in a testable
run-clients.sh, and flips the three manifest entries to enabled, pinned to the
commits that carry their Phase 2 tests:
  ether/pad#1, ether/etherpad-cli-client#136, ether/etherpad-desktop#78.

Validated end-to-end locally against a real core: all three clients' vectors +
live smoke pass. Refs should be bumped to the squash-merge commits once those
client PRs land.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:49:42 +01:00
John McLear
860ab68c04
test: downstream client compatibility gate (Phase 1) (#7923)
* docs: design for downstream client compatibility tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: Phase 1 implementation plan for downstream compat tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(downstream): add golden wire-vector generator

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(downstream): add committed golden wire-vectors fixture

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(downstream): assert wire-vectors fixture stability + consistency

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(downstream): pin socket.io handshake + USER_CHANGES sequence

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(downstream): snapshot client-facing HTTP API shapes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(downstream): add client manifest (entries disabled pending Phase 2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(downstream): add downstream-smoke workflow (boot/self-check/teardown + manifest scaffold)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(downstream): validate settings rewrite + ignore docs/** (Qodo)

Fail fast if the template's port/auth literals drift so a no-op sed can't
silently boot the smoke server on the wrong port/auth. Also ignore docs/**
(not just doc/**) so docs-only PRs don't trigger the boot job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:40:58 +01:00
Etherpad Release Bot
67f7d914f4 Merge branch 'develop' 2026-06-09 08:54:33 +00:00
Etherpad Release Bot
9af3a4de77 Merge branch 'master' into develop 2026-06-09 08:54:33 +00:00
Etherpad Release Bot
bb2ef93a1b bump version 2026-06-09 08:54:32 +00:00
John McLear
62b9c711a2
docs(changelog): fold post-3.3.0-draft commits into the 3.3.0 notes (#7921)
The `# 3.3.0` changelog section was written before ~8 commits landed on
develop. #7909 (dark mode) and #7918/#7911 (offline Docker boot) were already
captured; this adds the remaining user-facing change and dependency bumps so
the 3.3.0 release notes are complete:

- Notable fixes: #7910 — Firefox authorship-colour flake (early keystrokes
  tagged author='' before the async userAuthor propagated, producing an
  unattributed insert the pad-corruption guard rejected).
- Dependencies: mysql2 →3.22.5 (#7915), undici →8.4.1 (#7914), pdfkit
  →0.19.0 (#7916), @radix-ui/react-switch →1.3.0 (#7913), and the extra
  dev-dependency group bump (#7912).

Localisation (translatewiki) is already covered by the existing entry.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:52:40 +01:00
John McLear
53407f090d
docs(release): document release procedure in AGENTS.MD; remove deprecated createRelease.sh (#7920)
* chore(release): remove deprecated bin/createRelease.sh

This script has carried a "DEPRECATED since Etherpad 1.7.0 (2018-08-17),
left here just for documentation" banner for years and is dead code:

- It authenticates to the GitHub API with the `?access_token=` query
  parameter, which GitHub removed in 2021 — every API call (token check,
  branch merge, release publish) now fails outright.
- It targets the old `ether/etherpad-lite` repo paths and calls
  `bin/buildForWindows.sh` / `make docs`, neither of which is how releases
  are built anymore.
- Nothing references it (no workflow, script, or doc).

The current release flow is the "Release etherpad" workflow
(.github/workflows/release.yml) driving bin/release.ts, then the tag-push
triggers handleRelease.yml + releaseEtherpad.yml. createRelease.sh only adds
confusion, so remove it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agents): document the release procedure and docs publishing

Add a "Releasing" section to AGENTS.MD so maintainers have a single
reference for cutting a release, instead of reverse-engineering it from
bin/release.ts and the workflow files.

Covers:
- Prerequisites: the CHANGELOG `# X.Y.Z` guard, and the requirement that all
  four package.json files agree (release.ts reads the current version from
  src/package.json) — the desync that blocked the 3.3.0 release.
- The one-dispatch flow: "Release etherpad" -> bin/release.ts -> tag push,
  and what the vX.Y.Z tag auto-triggers (handleRelease GitHub Release,
  docker, snap-publish), plus the separate manual npm publish dispatch.
- Documentation: the two distinct kinds of doc work — per-PR doc/ updates in
  behaviour-change PRs, and the automated release-time versioned-docs publish
  into the ether.github.com sibling repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:32:25 +01:00
John McLear
b19ad89eb0
Fix offline/air-gapped Docker boot: stop pnpm self-provisioning the pinned version (#7911) (#7918)
* fix(docker): don't let pnpm self-provision a pinned version on offline boot (#7911)

The official Docker image installs pnpm directly via npm (corepack was dropped
for Node 25+). Standalone pnpm still honours the "packageManager" pin in
package.json: the image's pnpm intentionally lags that pin (pnpm 11.1.x enforces
a minimum-release-age policy the frozen-lockfile build can't satisfy), so pnpm
treats every invocation — including the informational `pnpm --version` probe
Etherpad runs at startup — as a request to download and run the pinned build.
Behind a corporate firewall / in an air-gapped install that download fails:

  [WARN] plugins - Failed to get pnpm version: Error: Command exited with
  code 1: pnpm --version

which is what #7911 reported.

Fix — neutralise the gap instead of closing it (closing it would break the
frozen-lockfile build on 11.1.x):

  - Dockerfile build stage sets `pnpm_config_pm_on_fail=ignore` (the pnpm 11
    successor to managePackageManagerVersions), inherited by the development and
    production runtime stages. pnpm then uses the installed pnpm instead of
    fetching the pinned one. It does not change which pnpm runs the build-time
    install, so the frozen-lockfile build is unaffected.
  - plugins.ts startup probe and the updater's pnpm-on-PATH checks run with the
    same flag, so the fix also covers non-Docker offline installs and the probe
    can never fail-loud.

Add a backend spec that fails CI if the offline guard is dropped while the image
pnpm differs from the package.json pin.

Verified with a standalone (non-corepack) pnpm: a "packageManager" mismatch
makes `pnpm --version` exit 1 by default (tries to fetch the pinned build), and
exit 0 reading the local version with pm_on_fail=ignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: scope pnpm offline-guard check to the runtime-inherited build stage

Address Qodo review: the regression spec matched ENV pnpm_config_pm_on_fail
anywhere in the Dockerfile, so it would still pass if the guard were removed
from the `build` stage (which the runtime stages inherit) but left in the
throwaway `adminbuild` stage — reintroducing the offline failure. Extract the
`build` stage block and assert the ENV is present there specifically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:31:00 +01:00
John McLear
809b75ca22
fix(release): sync src + bin package.json to 3.2.0 so release dispatch targets 3.3.0 (#7919)
The Release workflow has failed on the last two dispatches with:

    Error: No changelog record for 3.4.0, please create changelog record
    at bin/release.ts:146

3.3.0 was never released (latest tag/release is v3.2.0). The repo already
carries a complete `# 3.3.0` changelog section, and the intent is to ship
3.3.0 next. A prior commit reverted the root and admin/ package.json back to
3.2.0 to set up a clean 3.2.0 -> 3.3.0 minor bump, but missed src/package.json
and bin/package.json, which were left at 3.3.0.

bin/release.ts reads the current version from src/package.json, so it computed
minor(3.3.0) = 3.4.0 and aborted on the missing 3.4.0 changelog. Syncing src
and bin back to 3.2.0 (matching root + admin) makes the next `minor` dispatch
compute 3.2.0 -> 3.3.0, which satisfies the existing changelog check.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:30:40 +01:00
SamTV12345
d09227b1e7 3.2.0 2026-06-08 22:19:50 +02:00
dependabot[bot]
54af41276c
build(deps): bump @radix-ui/react-switch from 1.2.6 to 1.3.0 (#7913)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.2.6 to 1.3.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 21:54:42 +02:00
dependabot[bot]
271ded5e49
build(deps): bump mysql2 from 3.22.4 to 3.22.5 (#7915)
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.4 to 3.22.5.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.22.4...v3.22.5)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.22.5
  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>
2026-06-08 21:54:32 +02:00
dependabot[bot]
d6837f9013
build(deps): bump undici from 8.3.0 to 8.4.1 (#7914)
Bumps [undici](https://github.com/nodejs/undici) from 8.3.0 to 8.4.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.3.0...v8.4.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.4.1
  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>
2026-06-08 21:54:23 +02:00
John McLear
ef205bf2ac
fix(theme-color): dark address bar + no light-flash on dark-OS load (#7606) (#7909)
* fix(theme-color): emit media-scoped dark variant for iOS Safari (#7606)

The theme-color meta only had a single light value rendered server-side;
dark mode was applied purely by JS (skin_variants.ts) after page load.
iOS Safari colors the address bar at parse time and does not reliably
repaint when JS mutates the meta later, so dark-mode iPhone users kept a
white address bar above a dark toolbar (the green Chromium Playwright test
masked this because Chrome does honor the dynamic update).

Emit a prefers-color-scheme media-scoped pair server-side so the correct
color is chosen at first paint without JS:

  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
  <meta name="theme-color" content="#485365" media="(prefers-color-scheme: dark)">

- Add SkinColors.darkToolbarColor() (reuses toolbarColorForTokens).
- Expose enableDarkMode via getPublicSettings so the templates can gate the
  dark variant on it (no dark variant when dark mode can't be reached).
- Apply to both pad.html and timeslider.html.
- updateThemeColorMeta now updates every theme-color meta so a manual
  #options-darkmode toggle still wins over the media scoping on
  desktop/Android.
- Backend + frontend tests updated to assert the media-scoped pair and the
  enableDarkMode-off case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(theme-color): prevent the light-mode flash on dark-OS load (#7606)

The theme-color meta fix corrects the address-bar tint, but dark-OS users
still saw the whole page painted light before the JS bundle ran and applied
the dark skin classes in postAceInit — a visible flash on every browser,
not just the mobile address bar.

Add a tiny blocking inline script in <head>, before the stylesheet, that
applies the dark skin classes to <html> synchronously during parse when the
client is in dark mode (matchMedia + no localStorage white-mode override).
The condition mirrors pad.ts's auto-switch, which still runs on init to wire
up the #options-darkmode toggle and theme the editor iframes (those don't
exist yet at parse time). Gated on the same enableDarkMode + colibris check
as the dark theme-color variant. Applied to pad.html and timeslider.html.

Verified in Chromium: at domcontentloaded a dark-OS client's <html> already
carries super-dark-editor/dark-background/super-dark-toolbar (no flash), and
a light-OS client is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note the dark-mode address-bar + flash fix (#7606)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(theme-color): guard pre-paint script on #skinvariantsbuilder; ignore updater state

Address PR review:
- Copilot: the inline pre-paint dark-mode script must skip the auto-dark
  switch on the #skinvariantsbuilder hash, matching pad.ts — otherwise it
  forces super-dark classes on a dark-OS client and fights the variants
  builder UI. Added the guard to pad.html and timeslider.html and a backend
  assertion so it can't regress.
- Qodo: ignore var/update-state.json (runtime updater cache) so the server
  run that regenerates it can't dirty the tree or be committed accidentally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:54:08 +02:00
dependabot[bot]
c8350782dd
build(deps): bump pdfkit from 0.18.0 to 0.19.0 (#7916)
Bumps [pdfkit](https://github.com/foliojs/pdfkit) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/foliojs/pdfkit/releases)
- [Changelog](https://github.com/foliojs/pdfkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/foliojs/pdfkit/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: pdfkit
  dependency-version: 0.19.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>
2026-06-08 20:41:41 +01:00
dependabot[bot]
2c64e85e90
build(deps-dev): bump the dev-dependencies group with 8 updates (#7912)
Bumps the dev-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/jquery](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jquery) | `4.0.0` | `4.0.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `25.9.2` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.15` | `1.2.16` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.4` | `1.2.5` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.16` | `19.2.17` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.77.0` | `7.78.0` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.134.0` | `0.135.0` |


Updates `@types/jquery` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jquery)

Updates `@types/node` from 25.9.1 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@radix-ui/react-dialog` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.15 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-visually-hidden` from 1.2.4 to 1.2.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

Updates `@types/react` from 19.2.16 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-hook-form` from 7.77.0 to 7.78.0
- [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](https://github.com/react-hook-form/react-hook-form/compare/v7.77.0...v7.78.0)

Updates `oxc-minify` from 0.134.0 to 0.135.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.135.0/napi/minify)

---
updated-dependencies:
- dependency-name: "@types/jquery"
  dependency-version: 4.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.78.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.135.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>
2026-06-08 20:41:25 +01:00
John McLear
78943da92b
fix(editor): tag inserts with clientVars author until userAuthor propagates (Firefox authorship flake) (#7910)
* fix(editor): tag inserts with clientVars author until userAuthor propagates

Fixes the intermittent Firefox `clear_authorship_color` flake ("clear authorship
colors can be undone to restore author colors").

Root cause: the inner editor's `thisAuthor` starts '' and is only populated when
collab_client's `setProperty('userAuthor', userId)` reaches the inner frame —
that call is queued by the outer ace wrapper via pendingInit until the iframe
loads, so it is applied asynchronously. Under Firefox timing the first keystrokes
can beat it, so freshly typed text is tagged author=''. An empty author
canonicalizes to no-author, producing an unattributed insert (`Z:1>5+5$Hello`)
that the server's pad-corruption guard rejects ("submitted an insert without an
author attribute"), dropping the whole USER_CHANGES and losing authorship. Undo
then cannot restore an author color and the test sees `<span class="">`.

Fix: a `getLocalAuthor()` helper that falls back to `clientVars.userId` (the same
author id, available synchronously in the inner frame — already used as `myId`
elsewhere) whenever `thisAuthor` is still empty, applied at the three new-text
insert sites. The intentional clear-authorship path (`['author','']`) and the
server-side guard are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): also seed line-attribute author to close the same race

The first commit fixed text inserts, but line-attribute changes (lists, headings,
alignment) emit a line-marker insert tagged with AttributeManager.author, which
likewise starts '' and is only set when the async setProperty('userAuthor')
lands. An early list/heading could therefore emit an unattributed line-marker
insert and hit the same server-side rejection.

Seed documentAttributeManager.author from getLocalAuthor() (clientVars fallback)
right after it is constructed; the userauthor handler keeps it in sync after.

Audited the other author-tagging paths: all ace2_inner text-insert sites go
through getLocalAuthor()/authorizer now, and contentcollector (paste/import)
derives authorship from the pasted DOM's author- classes via className2Author —
a different mechanism, not this async race — and self-guards on `if (state.author)`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): read the local author from the TOP pad window, not the inner frame

v1 of this fix fell back to `window.clientVars?.userId`, but that did NOT resolve
the flake — CI reproduced the identical failure and the server log still showed
the unattributed-insert rejection.

Ground truth via direct DOM measurement of a running pad: the inner editor
iframe's `window.clientVars` is NEVER populated (undefined at t=0/1/3/6s for the
life of the pad), so the v1 fallback always returned ''. The author id lives on
the TOP pad window (`window.top.clientVars.userId`), set by pad.ts when the
CLIENT_VARS message arrives — which necessarily precedes editor creation, so it
is reliably available from the inner frame the moment the editor can accept
input.

getLocalAuthor() now reads window.top.clientVars.userId (guarded with try/catch
for cross-origin embedded pads). thisAuthor still takes precedence once the
queued setProperty('userAuthor') lands. All text-insert sites and the
line-attribute seed already route through getLocalAuthor(), so both paths are
covered.

Verification: ts-check passes; fix source confirmed by direct measurement (the
top window reliably holds the author at editor-init time). Local end-to-end
repro was not possible — the dev server's require-kernel bundle did not reflect
working-tree edits — so this is validated against the measured source + CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): walk ancestor frames for the author instead of window.top

Addresses a correctness gap flagged in review: reading window.top.clientVars
fails for same-origin embeds, where window.top is the host page (accessible, so
no exception is thrown) and has no Etherpad clientVars — getLocalAuthor() then
returns '' and the unattributed-insert bug recurs.

Walk up the ancestor chain (inner -> ace_outer -> pad window) and return the
first frame that has clientVars.userId. This stops at the pad window and never
depends on window.top, so it is correct for normal pads, same-origin embeds, and
cross-origin embeds (the try/catch ends the walk if an ancestor is cross-origin,
though the pad window — always same-origin with the editor frames — is reached
first). Behavior in the non-embed case is unchanged (pad window is 2 hops up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(collab): stamp the author onto outgoing inserts in collab_client

Replaces the earlier getLocalAuthor attempts (v1-v3, reverted from ace2_inner.ts),
which all failed because they tried to *read* an author that genuinely does not
exist yet: the local author id (clientVars.userId) only arrives with the
CLIENT_VARS socket message, and under load (Firefox + plugins) the editor can
become editable and the user can type before that message lands. At that instant
there is no author anywhere client-side, so the editor emits an unattributed
insert that the server's pad-corruption guard rejects — dropping the change and
losing authorship (the clear_authorship_color flake).

Fix where the author IS reliably available: collab_client only exists after
CLIENT_VARS has arrived, so its `userId` is always populated. A new pure helper
stampAuthorOnInserts() rewrites the outgoing wire changeset so every '+' op
carries an author, stamping userId onto any that lack one, just before it goes on
the wire (both the USER_CHANGES send and the reconnect/further-changeset path).
Independent of editor-init timing.

Verification:
- Unit test (stampAuthorOnInserts.test.ts, 5 cases): stamps the exact flake
  changeset `Z:1>5+5$Hello` -> authored + checkRep-valid + text preserved; leaves
  already-attributed inserts and keep/remove-only changesets unchanged; never
  invents an author when userId is empty; preserves other attributes.
- ace2_inner.ts reverted to develop (the editor is unchanged; the fix is fully
  localized to the send path).
- Local run of the clear_authorship_color spec with the fix: all pass, no
  server-side unattributed-insert rejections; full backend-new vitest green
  (the unrelated backend-tests-glob env-only failure aside).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:41:13 +01:00
translatewiki.net
29441cdfba
Localisation updates from https://translatewiki.net. 2026-06-08 14:03:38 +02:00
SamTV12345
1510e1e6f6 bump version
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:04:29 +02:00
SamTV12345
ab60c80c39 chore: added changelog for v3.3.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:04:29 +02:00
John McLear
3e7645004f
docker-compose: require explicit credentials and default TRUST_PROXY off (#7907)
- Require ADMIN_PASSWORD and the database password to be provided explicitly
  (no implicit fallback).
- Default TRUST_PROXY to false.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:51:25 +02:00
John McLear
7ea9970648
Hardening: API request handling, random IDs, and plugin loading (#7906)
* Hardening: API request handling, token generation, and plugin loading

- pad_utils.randomString: generate the random IDs via crypto.getRandomValues
  (CSPRNG) instead of Math.random.
- OAuth2Provider: constant-time password comparison and a uniform failure delay
  on the OIDC interaction login; own-property user lookup.
- API.appendChatMessage: require the pad to already exist (getPadSafe),
  consistent with the other content API methods.
- RestAPI /api/2: forward only the authorization header rather than merging all
  request headers into the API field set.
- LinkInstaller: validate plugin dependency names before building filesystem
  paths from them.
- admin file server: return a generic error message and log details server-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review: timingSafeEqual on raw bytes; align /api/2 auth fallback

- OAuth2Provider.constantTimeEquals: compare raw UTF-8 bytes with
  crypto.timingSafeEqual instead of hashing them first. Resolves the CodeQL
  "password hash with insufficient computational effort" alert while keeping a
  content-independent comparison (length difference is covered by the uniform
  failure delay).
- RestAPI /api/2: fall back to the authorization header whenever the field is
  falsy (not only null), matching the openapi.ts handler so the two routers
  authenticate identically (Qodo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* appendChatMessage: throw explicit error instead of getPadSafe (review)

Per review: replace the getPadSafe(padID, true) existence check with an
explicit `throw new CustomError('padID does not exist', 'apierror')` so chat
messages can't create pads, without fetching the pad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:17:39 +02:00
John McLear
86c56cf827
Escape exported data attributes and warn on default/placeholder credentials (#7905)
* Escape exported data-* attributes; require explicit deploy credentials

- ExportHtml: escape the name and value of attributes emitted by the
  exportHtmlAdditionalTagsWithData hook, consistent with the URL/text
  escaping already applied when generating exported HTML.
- docker-compose: require ADMIN_PASSWORD and the database password to be set
  explicitly (no default fallback); default TRUST_PROXY to false.
- Settings: log a warning (error level in production) when an account uses a
  default/placeholder password from the shipped config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Settings: also warn on default OIDC client secrets

Extend the placeholder-credential check to cover sso.clients[].client_secret,
so a deployment that enables SSO without setting ADMIN_SECRET / USER_SECRET is
flagged (error level under NODE_ENV=production) the same way default account
passwords are.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Move docker-compose changes to a separate PR

The deployment-default changes (required credentials, TRUST_PROXY) are being
discussed separately; this PR keeps only the non-breaking export escaping and
credential-warning changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:05:57 +01:00
dependabot[bot]
e417bc1047
build(deps): bump rate-limiter-flexible from 11.1.0 to 11.1.1 (#7904)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 11.1.0 to 11.1.1.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](https://github.com/animir/node-rate-limiter-flexible/compare/v11.1.0...v11.1.1)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-version: 11.1.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>
2026-06-05 22:40:01 +02:00
John McLear
e1476cf100
fix(history-mode): lay the history iframe in the editor's flex slot (#7903)
In-pad history mode positioned the timeslider iframe (#history-frame-mount)
as an absolute, inset:0 overlay over #editorcontainerbox. That filled the
editor area but took the iframe out of flow, so any in-flow side panel in
#editorcontainerbox — e.g. ep_webrtc's video column (#rtcbox) — ended up
hidden beneath the overlay. In live mode the same panel sits beside the
editor, so the two modes disagreed (ether/ep_webrtc rendered nothing in the
timeslider).

Make the iframe occupy the same in-flow flex slot the live editor uses
(flex: 1 1 auto in the row-flex #editorcontainerbox) so side panels lay out
identically in both modes.

This surfaced a latent bug: `body.history-mode #editorcontainer { display:
none }` (specificity 0-1-1) was being outranked by the two-id layout rule
`#editorcontainerbox #editorcontainer { display: flex }` (0-2-0), so the live
editor was never actually removed from flow — the absolute overlay merely
painted over it. With the iframe now in normal flow that no longer hides the
editor, so the hide rule is given matching specificity
(body.history-mode #editorcontainerbox #editorcontainer) to win the cascade.

Added a padmode.spec.ts regression test that injects a left-pinned in-flow
side panel, enters history mode, and asserts the live editor is display:none,
the iframe is not absolutely positioned, and the iframe lays out beside the
panel filling the editor's remaining width (no overlap).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:49:45 +01:00
John McLear
189ea4a29b
fix: URL-encode pad names in admin 'Open' button and recent pads (#7865) (#7895)
* fix: URL-encode pad names in admin 'Open' button and recent pads (#7865)

- encodeURIComponent in admin PadPage 'Open' button href
- decodeURIComponent when reading pad name from URL pathname
  in pad_userlist.ts and colibris/pad.js (recent pads storage)
- encodeURIComponent in colibris/index.js recent pads href;
  display text uses stored name directly (no double-decode)
- add recent_pads spec asserting encoded URLs
- add share dialog spec asserting URL encoding of special chars

* fix: address Qodo review on recent-pads encoding and admin Open button

- Normalize legacy URL-encoded recentPads names before re-encoding the
  href in colibris/index.js, preventing double-encoding (%2F -> %252F)
  of entries stored by older versions.
- Add noopener,noreferrer to the admin 'Open' window.open call to
  prevent reverse tabnabbing, matching the pattern used elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:02:35 +01:00
John McLear
3e6f9d7bb2
fix(pad): attribute default welcome text to the system author (#7885) (#7887)
* fix(pad): attribute default welcome text to the system author (#7885)

When a user opens a brand-new pad, CLIENT_READY calls
getPad(padId, null, session.author), and Pad.init attributed the
auto-generated default content (settings.defaultPadText or a
padDefaultContent hook substitution) to that author. The welcome text —
which the user never wrote — therefore carried the creator's `author`
attribute and rendered in their authorship colour.

Track whether the initial text came from the default-content path and,
if so, attribute the initial changeset to the stable system author
(Pad.SYSTEM_AUTHOR_ID) instead of the creating user. Explicitly provided
text (e.g. HTTP API createPad with text + author) keeps the real author.

The creating user becomes a listed author only once they actually type;
their "ownership" of the pad (pad-wide settings defaults, the author
token) does not depend on owning the default text, so it is unaffected.
listAuthorsOfPad already filters out the system author, so the public
API reports zero contributors for an untouched default pad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pad): keep creator as revision-0 author, only de-colour the text

CI (socketio "Pad-wide settings creator gate") and Qodo both caught that
attributing revision 0 to the system author stripped the creating user's
ownership: isPadCreator() / the pad-wide settings gate and the deletion
token all key off getRevisionAuthor(0).

Decouple the two: the initial revision's meta.author stays the real
creator (ownership preserved), while only the welcome text's `author`
*attribute* — the thing that colours it — becomes Pad.SYSTEM_AUTHOR_ID.
The system fallback for the revision author now applies solely when no
author was supplied at all.

Tests assert both halves: default text is system-coloured (not the
creator's colour) AND the creator remains the revision-0 author; explicit
text stays coloured with the creator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(wcag): target the user-authored span, not the system welcome text

The wcag_author_color spec picked the first `span[class*="author-"]` on
the page to measure the user's colour contrast. With #7885 the default
welcome text is now owned by the system author and renders with no
background colour, so that first span is no longer the current user's —
the contrast read came back as transparent rgba(0,0,0,0) and the three
assertions failed.

Match the author span by the text we just typed ("contrast smoke")
instead, so the test measures the actual user-authored content it
intends to. Verified locally: 3/3 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:44:35 +01:00
John McLear
2d1a09dbe0
docs: refresh docs for 3.2.0 — correct stale content, document recent features (#7888)
* docs: refresh docs for 3.2.0 — correct stale content, document recent features

The hand-maintained VitePress docs under doc/ had drifted behind a lot of
recent work. They are authored prose (not generated from the OpenAPI spec),
so they need manual upkeep. This pass corrects content that was actively
wrong and documents features shipped since they were last touched.

Corrections (was wrong / misleading):
- cli.md: every command used `node bin/foo.js`, but the scripts are
  TypeScript run via pnpm — copy-paste failed. Rewrote to
  `pnpm run --filter bin <script>`, documented ~13 previously-undocumented
  operator tools, and split running-vs-stopped requirements. Registered the
  missing `compactStalePads` script in bin/package.json so the documented
  invocation actually works.
- stats.md: described a pre-Prometheus world. Rewrote for the gated
  `/stats` (JSON) and `/stats/prometheus` endpoints, the live metric set,
  the opt-in `scalingDiveMetrics` instruments (#7756), and `measured-core`.
- admin/updates.md: removed three false "SMTP not yet wired" claims (it is,
  via nodemailer + the `mail.*` block), documented the `node-engine-mismatch`
  preflight check and the rollback/preflight failure emails, and stripped
  obsolete "PR 1 / PR 2" staging language now that all tiers ship.
- api/http_api.md: added the undocumented `anonymizeAuthor` (GDPR Art. 17)
  call, fixed copyPad/movePad version annotations (1.2.8 → 1.2.9), corrected
  getPadID's param name (readOnlyID → roID), and dropped a reference to a
  non-existent `getEtherpad` API call.
- skins.md: colibris is the current default, not an "experimental" skin for
  a future 2.0.
- localization.md: bare `window._('key')` is unbound and returns undefined;
  recommend `window.html10n.get(...)` / data-l10n-id instead.
- README.md: bumped the v2.2.5 upgrade example to v3.2.0; fixed a
  docker.adoc link to docker.md.
- docker.md: added MAIL_*, ENABLE_METRICS, GDPR_AUTHOR_ERASURE_ENABLED,
  PRIVACY_BANNER_*, PUBLIC_URL, AUTHENTICATION_METHOD, ENABLE_DARK_MODE,
  ENABLE_PAD_WIDE_SETTINGS; fixed the SOCKETIO_MAX_HTTP_BUFFER_SIZE default
  (50000 → 1000000).

New documentation:
- configuration.md (new): how settings + `${VAR:default}` substitution work,
  trustProxy, and — the previously-undocumented feature — running under a
  subpath/ingress via x-proxy-path / X-Forwarded-Prefix / X-Ingress-Path,
  with the sanitizer rules and Traefik/NGINX examples. Wired into the
  VitePress sidebar and the index hero.
- hooks_server-side.md: ccRegisterBlockElements (the server-side companion
  plugin authors miss), exportConvert, exportHTMLSend, createServer,
  restartServer, and clientReady (marked deprecated).
- hooks_client-side.md: aceDrop, acePaste, handleClientTimesliderMessage_<name>.

VitePress build passes. The legacy .adoc set was intentionally left in place
— it still feeds the per-version doc archives published to ether.github.com
at release time (bin/release.ts), so it is not dead and is out of scope here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: address Qodo review — drop .js invocations from configuration.md and CLI help

- configuration.md: the settings-override example referenced a nonexistent
  `node src/node/server.js`. Use the supported launcher instead
  (`bin/run.sh -s <file>`), and note the runtime is server.ts via tsx.
- compactStalePads.ts / compactPad.ts / compactAllPads.ts: their header
  comments and runtime usage output still printed `node bin/*.js`, which
  points at files that don't exist. Switched to the documented
  `pnpm run --filter bin <script>` form so the --help text matches the docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:44:25 +01:00
John McLear
7b33892a76
css: restore URL wrapping in pad editor (#7894) (#7896)
* css: restore URL wrapping in pad editor (#7894)

Long URLs in the pad editor overflow instead of wrapping because the
global a { white-space: nowrap } rule in pad.css overrides the wrapping
properties set on #innerdocbody. Add explicit white-space, word-wrap,
and overflow-wrap to #innerdocbody a so URLs wrap inside the editor
while preserving no-wrap behavior for links elsewhere in the UI.

Fixes #7894

* test: add regression test for long URL wrapping in pad editor (#7894)

* test: add regression test for long URL wrapping in pad editor (#7894)
2026-06-05 13:44:15 +01:00
John McLear
9e8f9cb513
fix: pad-wide view settings apply to the creator's own view (#7900) (#7902)
* fix: pad-wide view settings apply to the creator's own view (#7900)

The pad creator is never "enforced upon themselves" (isPadSettingsEnforcedForMe
is false whenever canEditPadSettings is true), so getEffectivePadOptions always
merges their personal view overrides (cookies) on top of the pad-wide options.

As a result, a creator who had at some point toggled e.g. their personal "Read
content from right to left" carried a stale rtlIsTrue=false cookie that silently
masked the pad-wide value they later set — toggling the pad-wide control (and
then enforcing it) appeared to do nothing on the creator's own screen.

Fix: when the creator changes a pad-wide view option, sync their personal pref
to the chosen value so their own view adopts the pad-wide setting immediately.
This deliberately does NOT change the precedence model — the creator can still
override the setting afterwards via the "My view" controls, so the existing
behaviour where a creator keeps personal overrides while enforcing settings for
other users (pad_settings.spec.ts) is preserved.

Adds a frontend test reproducing the stale-personal-cookie scenario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: address Qodo review — clear cookies on the page's own context (#7900)

browser.newContext()+clearCookies() cleared cookies on a throwaway context
(not the page fixture under test) and leaked the context. Use
page.context().clearCookies() so the regression test reliably starts without a
stale rtlIsTrue pref.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:41:13 +01:00
John McLear
9af77e7e3a
timeslider: respect showAuthorshipColors and padFontFamily settings (#7899)
* timeslider: respect author colors, font, line numbers from pad editor

- nice-select.ts: dispatch native change event after jQuery trigger so
  addEventListener-based bridges (pad_mode.ts) fire (jQuery 3.7.1
  trigger() does not dispatch native DOM events)
- timeslider.ts: fix font-family reset (jQuery 3 ignores null css value);
  add showAuthorColors + showLineNumbers + padFontFamily support;
  wire cookie read/write for all three settings
- broadcast.ts: gate author color CSS on .authorColors class
- pad_mode.ts: bridge author colors, font family, line numbers checkboxes
  from pad settings into embedded timeslider iframe
- add Playwright test for showAuthorshipColors

* timeslider: tidy settings-bridge wiring

Refactor only — no behaviour change.

pad_mode.ts: collapse the five ad-hoc listener stores (playbackChange-
Listener, followChangeListener, authorColorsListeners, fontFamily-
Listeners, lineNumbersListeners) into the single outerControlListeners
list via a shared bindOuter() helper, so every outer-control listener is
registered and torn down through one uniform path. The three view-setting
bridges (author colours, font family, line numbers) become one
data-driven bridgeView() over their element ids instead of three
near-identical closures.

timeslider.ts: hoist applyPadFontFamily to a top-level helper next to
applyShowAuthorColors (keeping the '' reset for jQuery 3), and co-locate
the three BroadcastSlider view-setting methods that were split across the
function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:39:41 +01:00
John McLear
61aee16cf6
fix: RTL content option no longer flips the whole page (#7900) (#7901)
* fix: RTL content option no longer flips the whole page (#7900)

The pad's RTL content option (rtlIsTrue) is a per-pad setting that should
only change the direction of the editor's text contents. The setter in
ace2_inner.ts wrote the direction to the top-level `document.documentElement`
(`document` there resolves to the main pad page, since the editor iframes are
derived from it), which flipped the entire page — toolbar and chrome included.

The page direction is owned solely by the UI language (l10n.ts sets it from
html10n.getDirection()). Apply the content direction to the inner editor
document (`targetDoc.documentElement`) instead, so enabling RTL content leaves
the surrounding interface untouched.

Adds a frontend test asserting the inner editor flips to RTL while the
top-level <html> dir stays ltr.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: address Qodo review — robust frame access and locale-independent page-dir assertion (#7900)

- Use a frameLocator chain (Playwright auto-waits) instead of
  page.frame('ace_inner')! which can race with inner-editor readiness.
- Don't hardcode dir='ltr' on the top-level <html> (the UI language can
  legitimately pick rtl). Capture the initial page dir and assert it is
  unchanged after toggling the pad RTL option — that is the real invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:24:26 +01:00
dependabot[bot]
0110d880aa
build(deps): bump @tanstack/react-query from 5.100.14 to 5.101.0 (#7889)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.100.14 to 5.101.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.101.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>
2026-06-05 12:30:02 +01:00
dependabot[bot]
6a2673d411
build(deps-dev): bump react-router-dom in the dev-dependencies group (#7897)
Bumps the dev-dependencies group with 1 update: [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `react-router-dom` from 7.16.0 to 7.17.0
- [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.17.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.17.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>
2026-06-05 10:42:07 +01:00
dependabot[bot]
a88d2fc275
build(deps): bump semver from 7.8.1 to 7.8.2 (#7898)
Bumps [semver](https://github.com/npm/node-semver) from 7.8.1 to 7.8.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.1...v7.8.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.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>
2026-06-05 10:41:59 +01:00
dependabot[bot]
2209e37857
build(deps): bump @tanstack/react-query-devtools (#7890)
Bumps [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) from 5.100.14 to 5.101.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.101.0/packages/react-query-devtools)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.101.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>
2026-06-04 15:48:44 +01:00
dependabot[bot]
4ba2b2953e
build(deps): bump tsx from 4.22.3 to 4.22.4 (#7891)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.22.3 to 4.22.4.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.3...v4.22.4)

---
updated-dependencies:
- dependency-name: tsx
  dependency-version: 4.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>
2026-06-04 15:48:28 +01:00
dependabot[bot]
37c8217d4f
build(deps): bump mssql from 12.5.4 to 12.5.5 (#7892)
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.4 to 12.5.5.
- [Release notes](https://github.com/tediousjs/node-mssql/releases)
- [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt)
- [Commits](https://github.com/tediousjs/node-mssql/compare/v12.5.4...v12.5.5)

---
updated-dependencies:
- dependency-name: mssql
  dependency-version: 12.5.5
  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>
2026-06-04 15:48:18 +01:00
dependabot[bot]
bd627b55fa
build(deps-dev): bump i18next in the dev-dependencies group (#7893)
Bumps the dev-dependencies group with 1 update: [i18next](https://github.com/i18next/i18next).


Updates `i18next` from 26.3.0 to 26.3.1
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.0...v26.3.1)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.1
  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>
2026-06-04 15:48:10 +01:00
translatewiki.net
5aebfa8ebf
Localisation updates from https://translatewiki.net. 2026-06-04 14:03:32 +02:00
dependabot[bot]
1eecaa03bf
build(deps): bump redis from 5.12.1 to 6.0.0 (#7869)
Bumps [redis](https://github.com/redis/node-redis) from 5.12.1 to 6.0.0.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@5.12.1...redis@6.0.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 16:21:54 +01:00
dependabot[bot]
72a7a74915
build(deps): bump @tanstack/react-query from 5.100.11 to 5.100.14 (#7850)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.100.11 to 5.100.14.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.100.14/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.100.14
  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>
2026-06-02 15:49:09 +01:00
dependabot[bot]
02dad883cd
build(deps): bump ueberdb2 from 6.1.3 to 6.1.8 (#7873)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.3 to 6.1.8.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.3...v6.1.8)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.5
  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>
2026-06-02 11:22:48 +01:00
dependabot[bot]
5688679fc4
build(deps-dev): bump the dev-dependencies group across 1 directory with 11 updates (#7882)
Bumps the dev-dependencies group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [eslint](https://github.com/eslint/eslint) | `10.4.0` | `10.4.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.7` | `4.1.8` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.60.0` | `8.60.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.60.0` | `8.60.1` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.16` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.77.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.15.1` | `7.16.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.14` | `8.0.16` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.133.0` | `0.134.0` |



Updates `eslint` from 10.4.0 to 10.4.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.4.1)

Updates `vitest` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

Updates `@typescript-eslint/eslint-plugin` from 8.60.0 to 8.60.1
- [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.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.60.0 to 8.60.1
- [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.1/packages/parser)

Updates `react` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `@types/react` from 19.2.15 to 19.2.16
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `react-hook-form` from 7.76.1 to 7.77.0
- [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](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.77.0)

Updates `react-router-dom` from 7.15.1 to 7.16.0
- [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.16.0/packages/react-router-dom)

Updates `vite` from 8.0.14 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

Updates `oxc-minify` from 0.133.0 to 0.134.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.134.0/napi/minify)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.60.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.60.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.77.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.134.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>
2026-06-02 11:22:27 +01:00
dependabot[bot]
1f5946c464
build(deps): bump oidc-provider from 9.8.3 to 9.8.4 (#7872)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.3 to 9.8.4.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.3...v9.8.4)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.8.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>
2026-06-02 11:06:44 +01:00
dependabot[bot]
e8ff208b32
build(deps): bump @elastic/elasticsearch from 9.4.1 to 9.4.2 (#7858)
Bumps [@elastic/elasticsearch](https://github.com/elastic/elasticsearch-js) from 9.4.1 to 9.4.2.
- [Release notes](https://github.com/elastic/elasticsearch-js/releases)
- [Changelog](https://github.com/elastic/elasticsearch-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/elastic/elasticsearch-js/compare/v9.4.1...v9.4.2)

---
updated-dependencies:
- dependency-name: "@elastic/elasticsearch"
  dependency-version: 9.4.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>
2026-06-02 10:43:56 +01:00
dependabot[bot]
a0e0d776b9
build(deps): bump @tanstack/react-query-devtools (#7848)
Bumps [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) from 5.100.11 to 5.100.14.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.100.14/packages/react-query-devtools)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.100.14
  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>
2026-06-02 10:43:20 +01:00
dependabot[bot]
ce96504718
build(deps): bump ejs from 5.0.2 to 6.0.1 (#7860)
Bumps [ejs](https://github.com/mde/ejs) from 5.0.2 to 6.0.1.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/RELEASE_NOTES_v5.md)
- [Commits](https://github.com/mde/ejs/compare/v5.0.2...v6.0.1)

---
updated-dependencies:
- dependency-name: ejs
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 10:41:36 +01:00
dependabot[bot]
75600a2598
build(deps): bump js-cookie from 3.0.7 to 3.0.8 (#7874)
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.7 to 3.0.8.
- [Release notes](https://github.com/js-cookie/js-cookie/releases)
- [Commits](https://github.com/js-cookie/js-cookie/compare/v3.0.7...v3.0.8)

---
updated-dependencies:
- dependency-name: js-cookie
  dependency-version: 3.0.8
  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>
2026-06-02 10:41:23 +01:00
dependabot[bot]
61ed74ec16
build(deps): bump nodemailer from 8.0.9 to 8.0.10 (#7875)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.9 to 8.0.10.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v8.0.10)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.10
  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>
2026-06-02 10:40:49 +01:00
translatewiki.net
d426181971
Localisation updates from https://translatewiki.net. 2026-06-01 14:05:39 +02:00
dependabot[bot]
eb820ba2fd
build(deps): bump ueberdb2 from 6.1.2 to 6.1.3 (#7851)
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](https://github.com/ether/ueberDB/compare/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>
2026-05-29 19:55:28 +02:00
dependabot[bot]
a7016ad7c5
build(deps-dev): bump the dev-dependencies group across 1 directory with 8 updates (#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](https://github.com/i18next/i18next/compare/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](https://github.com/react-hook-form/react-hook-form/compare/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](https://github.com/pmndrs/zustand/compare/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>
2026-05-28 18:45:58 +01:00
John McLear
d3074e3707
test(ci): remove the silent-ELIFECYCLE flake investigation scaffolding (#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>
2026-05-28 16:49:22 +01:00
John McLear
d582398826
fix: eliminate the Windows backend silent-ELIFECYCLE flake (handler gate + Node 24.16.0) (#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 #7663) and diagnostics.ts (PR #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 #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>
2026-05-28 16:12:34 +01:00
dependabot[bot]
8f2ec5b04c
build(deps): bump nodemailer from 8.0.7 to 8.0.9 (#7857)
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](https://github.com/nodemailer/nodemailer/compare/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>
2026-05-28 15:26:33 +02:00
dependabot[bot]
42334bcce5
build(deps): bump mysql2 from 3.22.3 to 3.22.4 (#7859)
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](https://github.com/sidorares/node-mysql2/compare/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>
2026-05-28 15:23:35 +02:00
dependabot[bot]
854416086e
build(deps): bump lru-cache from 11.5.0 to 11.5.1 (#7864)
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](https://github.com/isaacs/node-lru-cache/compare/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>
2026-05-28 15:22:57 +02:00
translatewiki.net
8351dad390
Localisation updates from https://translatewiki.net. 2026-05-28 14:03:21 +02:00
John McLear
c4d2467cb3
test(ci): OS-level sidecar watcher for the Windows silent ELIFECYCLE (#7846)
In-process diagnostics (diagnostics.ts heartbeat at 5 Hz + node-report
snapshots on every beforeEach and heartbeat tick) merged in #7838 and
#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>
2026-05-26 12:06:29 +01:00
SamTV12345
118d48ca9e
test(enter): drop fragile viewport assertion (#7845)
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>
2026-05-25 17:28:14 +02:00
Андрій Шовкошитний
eeac0a6703
fix(oidc): fix OIDCAdapter broken flows (#7837)
* 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
2026-05-25 16:39:21 +02:00
John McLear
4b30653a3d
chore(root): drop three redundant top-level files (#7839)
* 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 #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>
2026-05-25 15:15:17 +01:00
John McLear
bfaf442935
fix(pad): URL view-option params lost to padeditor.init race (#7840) (#7843)
* fix(pad): URL view-option params lost to padeditor.init race (#7840)

`?showLineNumbers=false` and `?useMonospaceFont=true` were being
silently clobbered shortly after pad load. Same race that affected
`?rtl=false` before #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.

#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>
2026-05-25 16:10:12 +02:00
John McLear
79e3d46127
feat(settings): default settings.enablePluginPadOptions to true (#7841)
This flag gates the ep_* passthrough on padoptions that shipped in 3.0.0
(PR #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>
2026-05-25 14:43:29 +01:00
John McLear
29ee19bd8a
test(ci): schedule a mid-test snapshot 150ms after every beforeEach (#7842)
* test(ci): schedule a mid-test snapshot 150ms after every beforeEach

Run 26401801404 (PR #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 #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>
2026-05-25 14:38:47 +01:00
John McLear
98dbba4f1f
test(ci): heartbeat + running-test pointer to debug the silent backend-test ELIFECYCLE (#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>
2026-05-25 13:50:49 +01:00
John McLear
d9dabe352a
feat(admin): explain env-var substitution in /settings, surface auth errors (#7819) (#7826)
* feat(admin): explain env-var substitution in /settings, surface auth errors (#7819)

Three small, env-var-only UX improvements driven by issue #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 (#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>
2026-05-25 13:23:10 +01:00
translatewiki.net
40f0a4021a
Localisation updates from https://translatewiki.net. 2026-05-25 14:03:23 +02:00
John McLear
d106ed9c5e
fix(a11y): add Dialog titles/descriptions and missing index.code key (#7836)
Closes #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>
2026-05-25 10:43:10 +01:00
dependabot[bot]
53aa7a1c5a
build(deps): bump mssql from 12.5.3 to 12.5.4 (#7810)
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](https://github.com/tediousjs/node-mssql/compare/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>
2026-05-25 11:04:31 +02:00
John McLear
4ff3363b9b
docs(docker): document settings.json writable-layer + env-var-vs-file semantics (#7819) (#7827)
Two related operator-facing docs gaps, both surfaced by #7819:

1. settings.json on disk is a *template*; env-var substitution happens
   at load time in memory only. Operators repeatedly mistake the
   templated file for a stale config because the docs never spell out
   that the on-disk file is intentionally unchanged by env vars.

2. The default docker-compose.yml puts settings.json in the container's
   writable layer with no host mount, which means admin /settings edits
   are silently lost on `docker compose down && up`, `pull`, or
   watchtower — but preserved across plain `restart`. Operators don't
   reliably know which compose verbs recreate the container.

Adds two prose sections to doc/docker.md (explaining both gotchas, with
a recreate-vs-restart table) and a commented-out `./settings.json:…`
bind mount in both docker-compose.yml and the README compose example.
Bind mount is opt-in so existing setups behave identically.

No runtime change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:39:25 +02:00
SamTV12345
689dd9d434 chore: fixed backend tests 2026-05-22 22:43:25 +02:00
Etherpad Release Bot
50cd1d16f4 Merge branch 'master' into develop 2026-05-22 17:44:57 +00:00
196 changed files with 10716 additions and 3655 deletions

View file

@ -17,4 +17,11 @@ updates:
open-pull-requests-limit: 30
groups:
dev-dependencies:
dependency-type: "development"
dependency-type: "development"
cooldown:
default-days: 1 # fallback for anything not covered below
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 1
include:
- "*"

View file

@ -32,8 +32,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -51,7 +51,7 @@ jobs:
cache: pnpm
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -66,24 +66,7 @@ jobs:
run: pnpm build
-
name: Run the backend tests
env:
# --report-on-fatalerror and friends write a Node diagnostic report
# (V8 stack, libuv handles, OS info) on fatal errors that bypass JS
# handlers — the failure mode we've been chasing on Windows + Node
# 24 since PR #7663. Reports land in node-report/ and are uploaded
# as an artifact if the step fails.
NODE_OPTIONS: "--report-on-fatalerror --report-uncaught-exception --report-on-signal --report-compact --report-directory=${{ github.workspace }}/node-report"
run: |
mkdir -p "${{ github.workspace }}/node-report"
pnpm test
- name: Upload Node diagnostic reports on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: node-diagnostic-report-${{ runner.os }}-node${{ matrix.node }}-${{ github.job }}
path: node-report/
if-no-files-found: ignore
retention-days: 7
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
@ -105,8 +88,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -124,7 +107,7 @@ jobs:
cache: pnpm
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -153,19 +136,7 @@ jobs:
ep_table_of_contents
-
name: Run the backend tests
env:
NODE_OPTIONS: "--report-on-fatalerror --report-uncaught-exception --report-on-signal --report-compact --report-directory=${{ github.workspace }}/node-report"
run: |
mkdir -p "${{ github.workspace }}/node-report"
pnpm test
- name: Upload Node diagnostic reports on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: node-diagnostic-report-${{ runner.os }}-node${{ matrix.node }}-${{ github.job }}
path: node-report/
if-no-files-found: ignore
retention-days: 7
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
@ -180,14 +151,25 @@ jobs:
fail-fast: false
matrix:
# Etherpad requires Node >= 24 (see package.json engines.node).
node: ${{ fromJSON('[24]') }}
# Windows pins Node 24.16.0, not the runner-default 24.15.0. Node 24.15.0's
# bundled libuv (1.51.0) has a stack buffer overrun in the Windows TCP-connect
# path (uv__tcp_connect), proven by a /GS __fastfail full-memory dump under the
# backend suite's heavy localhost connection churn -- the long-standing Windows
# backend silent-ELIFECYCLE flake (memory corruption, so no JS/Node
# observability). Bisect: 24.15.0 crashes (4/4 on 127.0.0.1), 24.16.0
# (libuv 1.52.1) is clean (0/8). Stays on the 24 LTS line. Pin explicitly
# because setup-node's default check-latest:false reuses the runner's
# pre-cached 24.15.0 for a bare "24".
# Tracking: https://github.com/nodejs/node/issues/63620 — drop this pin
# back to plain "24" once the fix is across the supported 24.x baseline.
node: ${{ fromJSON('["24.16.0"]') }}
name: Windows without plugins
runs-on: windows-latest
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -218,23 +200,9 @@ jobs:
name: Run the backend tests
shell: bash
working-directory: src
env:
NODE_OPTIONS: "--report-on-fatalerror --report-uncaught-exception --report-on-signal --report-compact --report-directory=${{ github.workspace }}/node-report"
run: |
mkdir -p "${{ github.workspace }}/node-report"
# --exit forces process.exit(failures) after the suite completes,
# closing the post-suite event-loop drain window where Windows +
# Node 24 hard-kills the process. Scoped to Windows so Linux/local
# runs still surface real handle leaks via natural drain.
pnpm test -- --exit
- name: Upload Node diagnostic reports on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: node-diagnostic-report-${{ runner.os }}-node${{ matrix.node }}-${{ github.job }}
path: node-report/
if-no-files-found: ignore
retention-days: 7
# --exit makes mocha call process.exit() after the run so a leaked handle
# cannot hang the job on Windows.
run: pnpm test -- --exit
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
@ -248,15 +216,26 @@ jobs:
fail-fast: false
matrix:
# Etherpad requires Node >= 24 (see package.json engines.node).
node: ${{ fromJSON('[24]') }}
# Windows pins Node 24.16.0, not the runner-default 24.15.0. Node 24.15.0's
# bundled libuv (1.51.0) has a stack buffer overrun in the Windows TCP-connect
# path (uv__tcp_connect), proven by a /GS __fastfail full-memory dump under the
# backend suite's heavy localhost connection churn -- the long-standing Windows
# backend silent-ELIFECYCLE flake (memory corruption, so no JS/Node
# observability). Bisect: 24.15.0 crashes (4/4 on 127.0.0.1), 24.16.0
# (libuv 1.52.1) is clean (0/8). Stays on the 24 LTS line. Pin explicitly
# because setup-node's default check-latest:false reuses the runner's
# pre-cached 24.15.0 for a bare "24".
# Tracking: https://github.com/nodejs/node/issues/63620 — drop this pin
# back to plain "24" once the fix is across the supported 24.x baseline.
node: ${{ fromJSON('["24.16.0"]') }}
name: Windows with Plugins
runs-on: windows-latest
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -315,23 +294,9 @@ jobs:
name: Run the backend tests
shell: bash
working-directory: src
env:
NODE_OPTIONS: "--report-on-fatalerror --report-uncaught-exception --report-on-signal --report-compact --report-directory=${{ github.workspace }}/node-report"
run: |
mkdir -p "${{ github.workspace }}/node-report"
# --exit forces process.exit(failures) after the suite completes,
# closing the post-suite event-loop drain window where Windows +
# Node 24 hard-kills the process. Scoped to Windows so Linux/local
# runs still surface real handle leaks via natural drain.
pnpm test -- --exit
- name: Upload Node diagnostic reports on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: node-diagnostic-report-${{ runner.os }}-node${{ matrix.node }}-${{ github.job }}
path: node-report/
if-no-files-found: ignore
retention-days: 7
# --exit makes mocha call process.exit() after the run so a leaked handle
# cannot hang the job on Windows.
run: pnpm test -- --exit
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest

View file

@ -36,15 +36,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache vitepress build
with:
path: doc/.vitepress/cache

View file

@ -25,7 +25,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View file

@ -15,6 +15,24 @@ on:
- 'src/node/utils/run_cmd.ts'
- 'src/static/js/pluginfw/**'
- 'settings.json.template'
# Also build + smoke-test the package on PRs that touch the production
# footprint. The smoke step boots the packaged server and waits for /health,
# which is the only check that catches "server starts then exits before
# binding the port" startup regressions (e.g. a dependency bump whose change
# lets the event loop drain mid-boot). Previously this workflow ran only on
# push to develop, so such a regression was caught *after* merge — by which
# point develop was already red. Running it pre-merge blocks the PR instead.
# The release/apt-publish jobs are tag-guarded, so PRs run the build job only.
pull_request:
paths:
- 'packaging/**'
- '.github/workflows/deb-package.yml'
- 'src/package.json'
- 'pnpm-lock.yaml'
- 'src/node/server.ts'
- 'src/node/utils/run_cmd.ts'
- 'src/static/js/pluginfw/**'
- 'settings.json.template'
workflow_dispatch:
inputs:
ref:
@ -44,7 +62,7 @@ jobs:
runner: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.ref }}
@ -328,7 +346,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout etherpad source (for packaging/apt/key.asc)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1

View file

@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 'Dependency Review'
uses: actions/dependency-review-action@v5

View file

@ -26,7 +26,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -42,7 +42,7 @@ jobs:
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -151,6 +151,32 @@ jobs:
docker volume rm ep7718-plugins >/dev/null 2>&1 || true
[ "$ok" = "1" ] || exit 1
-
# Regression test: Etherpad container must boot without external
# network access (e.g., air-gapped host or restrictive runtime policy).
name: Regression — boot with disabled network (--network none)
working-directory: etherpad
run: |
docker run --rm -d \
--network none \
--name test-offline ${{ env.TEST_TAG }}
docker logs -f test-offline &
ok=0
for i in $(seq 1 60); do
status=$(docker container inspect -f '{{.State.Health.Status}}' test-offline 2>/dev/null) || {
echo "container exited prematurely while offline"
docker logs test-offline || true
break
}
case ${status} in
healthy) ok=1; echo "offline boot healthy after $((i*2))s"; break;;
starting) sleep 2;;
*) echo "unexpected status: ${status}"; docker logs test-offline || true; break;;
esac
done
docker rm -f test-offline >/dev/null 2>&1 || true
[ "$ok" = "1" ] || exit 1
build-test-local-plugin:
# Regression coverage for #7687: the Docker image's
# `bin/installLocalPlugins.sh` step runs as the `etherpad` user and
@ -164,7 +190,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -235,7 +261,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -332,7 +358,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -390,7 +416,7 @@ jobs:
enable-url-completion: true
- name: Check out ether-charts
if: github.ref == 'refs/heads/develop'
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: ether-charts
repository: ether/ether-charts

108
.github/workflows/downstream-smoke.yml vendored Normal file
View file

@ -0,0 +1,108 @@
name: Downstream smoke
# Boots a real Etherpad from the PR and verifies the separate downstream clients
# (Rust terminal editor, Node CLI, desktop/mobile) still round-trip against it.
# Phase 1 lands the boot/healthcheck/self-check/teardown harness + manifest; the
# per-client matrix activates as each client flips `enabled:true` in clients.json.
on:
pull_request:
paths-ignore:
- "doc/**"
- "docs/**"
schedule:
- cron: '0 4 * * *' # nightly against the default branch
permissions:
contents: read
jobs:
smoke:
name: Boot + downstream clients
runs-on: ubuntu-latest
timeout-minutes: 25
env:
PNPM_HOME: ~/.pnpm-store
APIKEY: downstream-smoke-key
steps:
- name: Checkout core (PR)
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Boot Etherpad on :9003 (apikey auth)
run: |
# The template ships a literal "port": 9001 and sso auth; rewrite both.
# (PORT env is ignored once the settings file specifies a port.)
sed -e 's#"port": 9001,#"port": 9003,#' \
-e 's#${AUTHENTICATION_METHOD:sso}#apikey#' \
settings.json.template > settings.json
# Fail fast if the template format drifted and sed silently no-op'd.
grep -q '"port": 9003,' settings.json \
|| { echo "::error::port rewrite failed — settings.json.template format changed"; exit 1; }
grep -q '"authenticationMethod": "apikey"' settings.json \
|| { echo "::error::auth rewrite failed — settings.json.template format changed"; exit 1; }
printf '%s' "$APIKEY" > APIKEY.txt
pnpm run prod > /tmp/ep.log 2>&1 &
echo $! > /tmp/ep.pid
echo "booted pid $(cat /tmp/ep.pid)"
- name: Wait for healthcheck
run: |
for i in $(seq 1 60); do
if curl -fsS "http://localhost:9003/api/" >/dev/null 2>&1; then
echo "server up after ${i} tries"; exit 0
fi
sleep 2
done
echo "::error::server did not come up"; tail -50 /tmp/ep.log; exit 1
- name: Self-check — authenticated create + read roundtrip
run: |
K="$APIKEY"
curl -fsS "http://localhost:9003/api/1/createPad?apikey=${K}&padID=smoke&text=hi%0A" | tee /tmp/create.json
grep -q '"code":0' /tmp/create.json
curl -fsS "http://localhost:9003/api/1/getText?apikey=${K}&padID=smoke" | tee /tmp/get.json
grep -q '"text":"hi' /tmp/get.json
- name: Generate canonical wire-vectors
run: cd src && pnpm run vectors:gen
# Rust toolchain for the `rust`-kind client (etherpad-pad). Other kinds
# use the node+pnpm already set up above.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run enabled downstream clients
env:
SMOKE_URL: http://localhost:9003
SMOKE_APIKEY: ${{ env.APIKEY }}
run: bash src/tests/downstream/run-clients.sh
- name: Teardown (by PID, never pkill)
if: always()
run: |
if [ -f /tmp/ep.pid ]; then
kill "$(cat /tmp/ep.pid)" 2>/dev/null || true
echo "killed $(cat /tmp/ep.pid)"
fi

View file

@ -27,8 +27,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -48,7 +48,7 @@ jobs:
name: Install all dependencies and symlink for ep_etherpad-lite
run: pnpm i
- name: Cache Playwright browsers
uses: actions/cache@v5
uses: actions/cache@v6
id: playwright-cache
with:
path: ~/.cache/ms-playwright

View file

@ -20,15 +20,15 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright
@ -92,15 +92,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright
@ -168,15 +168,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright
@ -269,15 +269,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright

View file

@ -27,8 +27,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -23,7 +23,7 @@ jobs:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Run shellcheck on installer.sh
run: |
sudo apt-get update
@ -38,7 +38,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
@ -70,13 +70,30 @@ jobs:
- name: Smoke test - start Etherpad and curl /api
shell: bash
# Hard backstop: if teardown ever fails to reap the server the step
# fails in minutes instead of burning to GitHub's 6h job ceiling.
timeout-minutes: 8
env:
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
run: |
set -eu
# Enable job control so the backgrounded launcher gets its own
# process group, letting us reap the whole pnpm -> node tree below.
set -m
cd "$ETHERPAD_DIR"
pnpm run prod >/tmp/etherpad.log 2>&1 &
PID=$!
# `pnpm run prod` is a nested launcher (pnpm -> pnpm --filter -> node),
# so killing $PID alone orphans the node server, which keeps the step's
# output pipe open and hangs CI. Kill the entire process group, with a
# SIGKILL fallback in case SIGTERM is swallowed (e.g. a live flush timer
# keeping the event loop alive), so the step always exits cleanly.
reap() {
kill -TERM "-$PID" 2>/dev/null || true
sleep 5
kill -KILL "-$PID" 2>/dev/null || true
}
trap reap EXIT
# Wait up to 60s for the API to come up.
ok=0
for i in $(seq 1 60); do
@ -90,17 +107,14 @@ jobs:
if [ "$ok" != "1" ]; then
echo "Etherpad did not start within 60s. Last 200 lines of log:" >&2
tail -200 /tmp/etherpad.log >&2 || true
kill "$PID" 2>/dev/null || true
exit 1
fi
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
installer-windows:
name: end-to-end install (windows-latest)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
@ -131,6 +145,7 @@ jobs:
- name: Smoke test - start Etherpad and curl /api
shell: pwsh
timeout-minutes: 8
env:
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
run: |

View file

@ -24,8 +24,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -62,8 +62,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -125,8 +125,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -24,8 +24,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -27,8 +27,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: ether/etherpad-lite
path: etherpad
@ -42,12 +42,12 @@ jobs:
git checkout develop
git reset --hard origin/develop
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: ether/ether.github.com
path: ether.github.com
token: '${{ secrets.ETHER_RELEASE_TOKEN }}'
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -82,7 +82,7 @@ jobs:
with:
ruby-version: 2.7
- uses: reitzig/actions-asciidoctor@v2.0.4
- uses: reitzig/actions-asciidoctor@v2.0.5
with:
version: 2.0.18
- name: Prepare release

View file

@ -1,9 +1,33 @@
# PARKED — npm publish of the core package is not part of the standard release.
#
# This workflow renames `ep_etherpad-lite` -> `ep_etherpad` and publishes
# `./src` to npm. As of 2026-06, that publish serves no load-bearing purpose:
# - `ep_etherpad` has 0 dependents on npm and nothing in this repo depends on it;
# - plugins import `ep_etherpad-lite` resolved from the LOCAL core install,
# and plugin CI clones `ether/etherpad` rather than `npm install`-ing core;
# - Etherpad is run via git clone / Docker / zip / snap, never `npm install`.
# The publish has been failing (E404 PUT — the `ep_etherpad` package has no OIDC
# trusted publisher configured on npmjs.com), which is why npm is stuck at 2.5.0
# while 3.x shipped fine without it.
#
# It is therefore gated behind an explicit `confirm: true` dispatch input so a
# stray run fails fast with a clear message instead of a confusing 404. To
# actually publish, the npm owner of `ep_etherpad` (samtv12345) must first
# configure a trusted publisher: npmjs.com -> ep_etherpad -> Settings ->
# Trusted Publisher -> repo `ether/etherpad`, workflow `releaseEtherpad.yml`.
# Decision pending: finish that config, or remove this workflow. See AGENTS.MD.
name: releaseEtherpad.yaml
permissions:
contents: read
id-token: write # for npm OIDC trusted publishing
on:
workflow_dispatch:
inputs:
confirm:
description: 'PARKED — publish ep_etherpad to npm? Requires a trusted publisher configured on npmjs.com first (see workflow header). Set true only if that is done.'
required: true
default: false
type: boolean
env:
PNPM_HOME: ~/.pnpm-store
@ -12,8 +36,16 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Guard — refuse unless explicitly confirmed
if: ${{ inputs.confirm != true }}
run: |
echo "::error::releaseEtherpad is PARKED. The ep_etherpad npm publish is non-functional"
echo "::error::(no trusted publisher configured on npmjs.com; 0 dependents on npm)."
echo "::error::Re-run with confirm=true only after the owner configures a trusted"
echo "::error::publisher. See the workflow header / AGENTS.MD 'Releasing' section."
exit 1
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
# OIDC trusted publishing needs npm >= 11.5.1, which requires
@ -22,7 +54,7 @@ jobs:
registry-url: https://registry.npmjs.org/
- name: Upgrade npm to >=11.5.1 (required for trusted publishing)
run: npm install -g npm@latest
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -34,7 +34,7 @@ jobs:
name: Wrapper unit tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Run snap/tests/run-all.sh
run: bash snap/tests/run-all.sh
@ -43,7 +43,7 @@ jobs:
needs: wrapper-tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Install snapcraft
run: sudo snap install --classic snapcraft

View file

@ -35,7 +35,7 @@ jobs:
snap-file: ${{ steps.build.outputs.snap }}
steps:
- name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Build snap
id: build

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out etherpad-lite
uses: actions/checkout@v6
uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
name: Install pnpm

View file

@ -32,13 +32,13 @@ jobs:
steps:
-
name: Check out latest release
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
-
name: Check out latest release tag
run: git checkout "$(git tag --list 'v*' --sort=-version:refname | head -n1)"
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -55,7 +55,7 @@ jobs:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0

View file

@ -1,5 +0,0 @@
[pr_reviewer]
run_on_pr_sync = true
[pr_description]
run_on_pr_sync = true

View file

@ -211,6 +211,38 @@ pnpm run plugins ls # List installed
### Settings
Configured via `settings.json`. A template is available at `settings.json.template`. Environment variables can override any setting using `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`.
## Releasing
Releases are driven almost entirely by GitHub Actions. A maintainer dispatches **one** workflow; the version bump, tagging, GitHub Release, Docker images, and snap all cascade off the pushed tag. The npm publish is a separate manual dispatch.
### Prerequisites (check these before dispatching)
- **A `# X.Y.Z` changelog section for the _target_ version must already be at the top of `CHANGELOG.md`.** `bin/release.ts` aborts with `No changelog record for X.Y.Z, please create changelog record` if it's missing. Write the section before dispatching.
- **All four `package.json` files must agree on the current version:** root `package.json`, `src/package.json`, `admin/package.json`, `bin/package.json`. `release.ts` reads the *current* version from **`src/package.json`** and computes the next with `semver.inc(current, type)`. If the files are out of sync (e.g. one was hand-edited), the computed target is wrong and the changelog guard fails. *(This exact desync — `src`/`bin` left at 3.3.0 while root/admin were 3.2.0 — blocked the 3.3.0 release in June 2026.)*
### Cutting a release
1. **Actions → "Release etherpad"** → Run workflow (`workflow_dispatch`), choose `patch` / `minor` / `major`. Cadence is monthly **minors** (3.0.0 → 3.1.0 → 3.2.0 → …); use `major` only for breaking changes.
2. The **Prepare release** step runs `bin/release.ts`, which:
- sanity-checks the tree (clean working dir, on `develop`, `develop`/`master` upstreams in sync, `../ether.github.com` cloned & clean on `master`);
- bumps the version in all four `package.json` files;
- commits `bump version`, merges `develop``master`, creates both `X.Y.Z` **and** `vX.Y.Z` tags, merges `master` back to `develop`;
- builds and stages the versioned docs into the website repo (see **Documentation** below).
3. The **Push after release** step (`bin/push-after-release.sh`) pushes `master`, `develop`, the tag, `--tags`, and the `ether.github.com` docs commit.
4. The pushed **`vX.Y.Z` tag auto-triggers** three workflows:
- `handleRelease.yml` → builds Etherpad, extracts the matching changelog section via `generateChangelog` (`bin/generateReleaseNotes.ts`), and publishes the **GitHub Release** (`make_latest: true`);
- `docker.yml` → builds & pushes the Docker images;
- `snap-publish.yml` → publishes the snap.
5. **npm publish — PARKED, not part of the release.** `releaseEtherpad.yaml` publishes the core as `ep_etherpad` to npm, but that package is **not load-bearing**: it has 0 dependents, nothing depends on it (plugins import `ep_etherpad-lite` from the *local* core install; plugin CI clones the repo), and Etherpad is run via clone/Docker/zip/snap — never `npm install`. The publish currently fails with `E404` because `ep_etherpad` has no OIDC trusted publisher configured on npmjs.com, which is why npm sits at 2.5.0 while 3.x shipped fine without it. The workflow is gated behind a `confirm: true` input so it can't run by accident. **Skip it for a normal release.** To revive it, the npm owner of `ep_etherpad` (`samtv12345`) configures a trusted publisher (npmjs.com → ep_etherpad → Settings → Trusted Publisher → repo `ether/etherpad`, workflow `releaseEtherpad.yml`); otherwise the workflow can be removed. Decision pending.
### Documentation
Two distinct things, both important:
**1. Per-PR doc updates — your responsibility in every behaviour-change PR.**
The `doc/` workspace holds the user/admin/API docs (Markdown + AsciiDoc). Whenever a PR changes API responses, CLI flags, settings keys, hooks, or error formats, update the relevant files in `doc/` **in the same PR** — don't defer it to release time. The HTTP API reference is `doc/api/http_api.{md,adoc}` (keep both in sync). Preview locally with `pnpm run makeDocs`.
**2. Release-time versioned docs publishing — automated, no manual step.**
During a release, `release.ts` runs `pnpm run makeDocs` (→ `bin/make_docs.ts`) to render `doc/` into `out/doc/`, then copies it into the sibling website repo at `../ether.github.com/public/doc/vX.Y.Z`, bumps that repo's version, and commits `X.Y.Z docs`; `push-after-release.sh` pushes it, publishing the versioned docs on etherpad.org. This requires `ether.github.com` checked out as a **sibling directory** — the **Release etherpad** workflow checks it out automatically. If you ever run `release.ts` by hand, clone it first: `cd .. && git clone git@github.com:ether/ether.github.com.git`.
## Monorepo Structure
This project uses pnpm workspaces. The workspaces are:

View file

@ -1,3 +1,113 @@
# 3.3.2
3.3.2 is a bug-fix and dependency-hardening follow-up to 3.3.1. It rounds out the pad-deletion UX rework (suppressing the recovery token for durable identities, keeping the token-less Delete button reachable, and closing a read-only deletion hole), restores the saved-revision markers that went missing from in-pad history mode in 3.3.x, and adds env-var overrides so air-gapped installs can switch off Etherpad's outbound calls without editing the image. It also fixes the `migrateDB` / `importSqlFile` / `migrateDirtyDBtoRealDB` CLI scripts against the promise-based ueberdb2 API, rejects unreachable `.`/`..` pad ids, and clears a batch of dependency security advisories (including CVE-2026-54285). On the CI side it unblocks the installer smoke test (which had been hanging the full 6-hour job ceiling since 3.2.0) and pins ueberdb2 past a startup-exit regression in the packaged boot.
### Security
- **Force `@opentelemetry/core` ≥ 2.8.0 (GHSA-8988-4f7v-96qf / CVE-2026-54285, #7975).** The transitive dep (pulled in via `@elastic/elasticsearch``@elastic/transport`) had a `W3CBaggagePropagator.extract()` that did not enforce W3C size limits on inbound baggage headers, allowing unbounded memory allocation. Pinned via a `pnpm-workspace.yaml` override; satisfies the existing `2.x` range with no parent bump.
- **Resolve open Dependabot security alerts (#7967).** Refreshes stale override floors and adds new ones via `pnpm-workspace` overrides: `form-data` ≥ 4.0.6, `ws` ≥ 8.21.0, `esbuild` ≥ 0.28.1, `basic-ftp` ≥ 5.3.1 (capped `<6.0.0` to avoid a surprise major on the plugin-install path), `tar` ≥ 7.5.16, `js-yaml` ≥ 4.2.0, `qs` ≥ 6.15.2, `ip-address` ≥ 10.1.1, and `@babel/core` ≥ 7.29.6.
- **Reject read-only deletion via token-less paths (part of #7959 / #7960).** Under `allowPadDeletionByAllUsers` a read-only viewer was granted `canDeletePad=true`, and the server's `flagOk`/`creatorOk` branches never checked `session.readonly` — so a read-only link holder could delete a pad without a token. Read-only sessions are now excluded from both the client var and the server's token-less authorization paths; a valid recovery token stays sufficient regardless of session mode.
### Notable enhancements
- **Pad deletion — suppress the recovery token for durable identities and relabel the action (#7926 / #7930).** Building on the `allowPadDeletionByAllUsers` suppression, a creator's deletion token is now also withheld when they have a *durable* identity — authenticated (`req.session.user` with a username) **and** the deployment pins that identity to a stable `authorID` via a `getAuthorId` hook — since only then does the creator survive a cookie clear or a different device, making the token redundant. This tightens the previous "require authentication ⇒ always suppress" rule: without `getAuthorId` the authorID still comes from the per-browser cookie, so an authenticated user on a second device is *not* the creator and keeps getting a token. A new `canDeleteWithoutToken` client var hides the whole recovery-token disclosure (label, field, submit) when no token is needed, and the recovery form now renders for all sessions (hidden by default) so an authenticated creator without a durable mapping still has UI to enter their token. `API.createPad` returns a `null` `deletionToken` under `allowPadDeletionByAllUsers`, matching the socket/UI path.
- **Offline/air-gapped installs — env-var overrides for the update check, plugin catalog, and updater (#7917, addresses #7911).** Firewalled deployments could not disable Etherpad's outbound calls without editing `settings.json` inside the image. The relevant keys are now wired through the `${ENV:default}` substitution in `settings.json.docker` and `settings.json.template`: `PRIVACY_UPDATE_CHECK`, `PRIVACY_PLUGIN_CATALOG`, `UPDATES_TIER` (`off` = no calls), `UPDATE_SERVER`, plus the docker-only `UPDATES_SOURCE` / `UPDATES_CHANNEL` / `UPDATES_CHECK_INTERVAL_HOURS` / `UPDATES_GITHUB_REPO` / `UPDATES_REQUIRE_ADMIN_FOR_STATUS`. A new "Updates & privacy" section in `doc/docker.md` documents the set; backend tests parse the shipped configs and fail if the `${ENV}` placeholders are dropped. Config, docs, and tests only — no runtime code change.
### Notable fixes
- **Pad — keep the token-less Delete button reachable without pad-wide settings (#7959 / #7960).** The token-less `#delete-pad` button was nested inside the `enablePadWideSettings`-gated section, so disabling pad-wide settings removed the only no-token deletion path — and combined with #7926 hiding the token disclosure when no token is needed, a user allowed to delete could be left with no deletion UI at all. The button is now always rendered (hidden by default) and driven by a `canDeletePad` client var (creator or `allowPadDeletionByAllUsers`, excluding read-only sessions), so the plain button and the recovery-token disclosure are mutually coherent and neither depends on pad-wide settings.
- **History mode — restore the saved-revision markers (#7946 / #7948).** When #7659 moved the timeslider into the pad as an embedded iframe, the user-facing control became the outer `#history-slider-input`, but the saved-revision stars were still drawn into the now-hidden iframe `#ui-slider-bar`, so "Save Revision" appeared to do nothing in in-pad history mode (a 3.3.x regression). `pad_mode.ts` now bridges the embedded slider's saved revisions onto the outer slider as percentage-positioned, aria-hidden star markers (with click-to-seek for mouse users), and the server's `SAVE_REVISION` handler broadcasts `NEW_SAVEDREV` to the pad room so a revision saved by a collaborator appears live on an already-open history slider. A single revision saved at rev 0 now renders too. Adds Playwright coverage for both the single-client and two-client live paths.
- **Import dialog — correct the outdated "no converter" help message (#7988 / #7989).** The notice claimed only plain text and HTML could be imported and linked to the legacy AbiWord wiki, prompting LibreOffice installs for formats that already work natively. Etherpad imports `.txt`, `.html`, `.docx` (via mammoth) and `.etherpad` without LibreOffice; only `.pdf`/`.odt`/`.doc`/`.rtf` still need it. The message now says so and points at the documentation site.
- **PadManager — reject unreachable `.` and `..` pad ids (#7962).** `isValidPadId` accepted ids consisting only of URL dot-segments, but per the WHATWG URL standard a browser normalises `/p/.` to `/p/` and `/p/..` to `/`, so such a pad could be created in the database yet never opened or exported. These ids are now rejected, and the admin `deletePad` handler falls back to a raw key purge when `getPad()` throws so any legacy `.`/`..` pad can still be removed.
### Internal / contributor-facing
- **CLI — fix the database migration/import scripts against the ueberdb2 promise API (#7982 / #7983).** `migrateDB.ts` opened source and target databases, copied all keys, then resolved without closing either — so under ueberdb2 6.1.x the keep-alive timer kept the process hanging after "Done syncing dbs", and buffered target writes were only guaranteed flushed on `close()`. It now closes both databases (flushing writes, clearing the timer) on success and error paths and exits with an explicit status. `importSqlFile.ts` and `migrateDirtyDBtoRealDB.ts` were ported off the pre-v6 callback API to `await db.init()` / `db.set(k, v)` / `db.close()`, removing two `@ts-ignore`s that hid broken calls and fixing an undefined `length` in a progress log; `tsc --noEmit` on the bin package is now clean.
- **CI — stop the installer smoke test hanging the 6-hour job ceiling (#7981).** The "Installer test" had hung on every ubuntu/macOS run since 3.2.0: `pnpm run prod` is a nested launcher, so `kill "$PID"; wait "$PID"` only signalled the outer pnpm and blocked forever if the node server didn't exit on SIGTERM. Teardown now runs the launcher in its own process group, kills the whole group (SIGTERM then SIGKILL), drops the blocking `wait`, and adds an 8-minute `timeout-minutes` backstop to both smoke steps.
- **CI — run the Debian-package smoke test on PRs (#7969).** The packaged-boot smoke test previously ran only on push to `develop` — i.e. after merge — which is why the ueberdb2 startup-exit regression turned `develop` red instead of being blocked at PR time. A `pull_request` trigger (scoped to production-footprint paths) now runs the build+smoke job on PRs; the release/apt-publish jobs stay tag-guarded.
- **Release — park the non-functional `ep_etherpad` npm publish (#7922).** The `releaseEtherpad` workflow republished `./src` as `ep_etherpad`, a package with zero dependents that nothing in the repo or any deployment path consumes, and it had been failing with E404 (no OIDC trusted publisher configured). The job is now gated behind an explicit `confirm: true` dispatch input so a stray run fails fast with a clear message, with the status documented in the workflow header and `AGENTS.MD`.
- **Tests — port the orphaned legacy timeslider specs to Playwright (#7949).** The `src/tests/frontend/specs/` mocha suite is run by no CI workflow, so its timeslider coverage was dead — which is how the #7946 history-mode regression reached a release. The still-meaningful cases (revision labels, export links, deep-link entry) were ported to `frontend-new` Playwright specs re-targeted at the real in-pad UI, and the three now-ported legacy specs were deleted.
### Dependencies
- `ueberdb2` pinned to `6.1.13`. 6.1.10 rewrote the cache/buffer layer to lazily arm an `.unref()`'d flush timer only when there are dirty keys, so on a fresh empty dirty DB nothing anchored Node's event loop and the packaged (.deb/systemd) boot could exit cleanly (code 0) before `server.listen()` bound the port — failing the Debian-package health check. The dep was pinned back to the last green release (6.1.9, #7969) and then rolled forward to the now-fixed `6.1.13` (#7979), pinned exactly rather than with a caret.
- `nodemailer` 8.x → 9.0.1 (#7965 / #7950 / #7976), `mongodb` 7.1.1 → 7.3.0 (#7941), `pg` 8.21.0 → 8.22.0 (#7985), `undici` → 8.5.0 (#7980 etc.), `oidc-provider` 9.8.4 → 9.8.5 (#7973), `pdfkit` 0.19.0 → 0.19.1 (#7945), `semver` 7.8.3 → 7.8.4 (#7943), and `@radix-ui/react-switch` 1.3.0 → 1.3.1 (#7974).
- Dev/build dependency group updates (#7964, #7970, #7978, #7987, #7944, #7951, #7952, and others), including `@types/node` 25 → 26, `esbuild` 0.28.0 → 0.28.1, `eslint` 10.4.1 → 10.5.0, `@playwright/test` 1.60 → 1.61, `vitest` 4.1.8 → 4.1.9, and `actions/checkout` 6 → 7 (#7977).
# 3.3.1
3.3.1 is a small bug-fix and hardening follow-up to 3.3.0. It closes a stored-XSS vector in the numbered-list `start` attribute, hardens the database layer so a dropped connection to PostgreSQL / Redis / RethinkDB no longer crashes the process (via ueberdb2 6.1.9), and fixes a handful of pad and admin regressions — the iOS dark-mode status bar, the settings language dropdown, the pad-deletion modal under `allowPadDeletionByAllUsers`, and a single unreadable pad blanking the admin Manage-pads list.
### Security
- **Pad editor — escape and integer-coerce the numbered-list `start` attribute (GHSA-f7h5-v9hm-548j, #7937).** A crafted `<ol start>` value flowed unescaped into `domline.ts`, a distinct client-side sink from the export-path fix in 3.3.0's #7905. The value is now integer-coerced and HTML-escaped before it reaches the DOM. A jsdom regression test covers the sink.
### Notable fixes
- **Skin — paint the root canvas so iOS dark mode has no white status bar (#7606 / #7931).** iOS Safari paints the top safe area from the `html` root background, which `theme-color` (an Android address-bar hint) does not affect, so dark-mode pads showed a white status-bar strip on iOS. Colibris now sets the root background and `color-scheme` so the safe area matches the editor.
- **Settings — show the detected language in the dropdown (#7925 / #7928).** The settings language `<select>` did not reflect the language Etherpad had actually auto-detected; it now shows the active selection.
- **Pad — don't issue a deletion token (or show its modal) when `allowPadDeletionByAllUsers` is on (#7929).** With pad deletion open to all users the client still minted a deletion token and surfaced the confirm modal; both are now suppressed in that configuration.
- **Admin — one unreadable pad no longer empties the Manage-pads list (#7935 / #7938).** A single pad that failed to read could throw out of the list-hydration path and blank the entire admin Manage-pads view; the read is now guarded per-pad so the rest of the list still renders.
### Internal / contributor-facing
- **CI — downstream client compatibility gate (#7923 / #7924 / #7927).** A new gate smoke-tests the published `etherpad-pad`, `etherpad-cli`, and `etherpad-desktop` clients against the server build (Phase 1 + Phase 2), with robust per-client error handling in `run-clients.sh` so one client's failure is reported rather than masking the others.
- **CI — verify Etherpad boots offline (#7936).** Adds a test step that confirms a built Etherpad starts with no network access.
### Dependencies
- `ueberdb2` 6.1.8 → 6.1.9 — PostgreSQL pool errors are now handled and TCP keep-alive is enabled (fixes #7878), and the Redis and RethinkDB drivers attach connection-error handlers so a dropped database connection no longer crashes the Etherpad process.
- `semver` 7.8.2 → 7.8.3 (#7933), `rate-limiter-flexible` 11.1.1 → 11.2.0 (#7934), plus a dev-dependencies group update (#7932).
# 3.3.0
3.3 is primarily a security-hardening release. A defence-in-depth pass tightens the HTTP API entry points, switches random-id generation to a CSPRNG, escapes exported `data-*` attributes, and flips the shipped Docker deployment defaults so a fresh install no longer boots with implicit credentials or a trusting proxy. Alongside that, the `ep_*` pad-options passthrough that shipped opt-in in 3.0.0 is now on by default, the in-pad timeslider learns to honour the editor's view settings (authorship colours, font family, line numbers), and a long tail of pad-editor layout, RTL, and URL-encoding fixes lands. The release also carries the root-cause fix for the long-standing Windows backend-test "silent ELIFECYCLE" flake.
### Notable enhancements
- **Plugin pad options on by default — `settings.enablePluginPadOptions` now defaults to `true` (#7841).** The flag that gates the `ep_*` passthrough on pad options (shipped opt-in in 3.0.0, #7698) is flipped to default-on, so plugins such as `ep_plugin_helpers`' `padToggle` / `padSelect` ride the existing broadcast/persist rail out of the box. This closes `ep_comments_page#422` — stock 3.x deployments `console.warn`ed on every pad load because the helper detected `enablePluginPadOptions === false`. The `settings.json.template` env-var default is flipped to match, so Docker/supervisor configs without an explicit value get the new behaviour. Existing deployments with an explicit `"enablePluginPadOptions": false` keep that value — no migration needed — and the protocol shape is unchanged for older clients.
- **Timeslider — honour the editor's view settings (#7899).** The in-pad timeslider now respects `showAuthorshipColors`, `padFontFamily`, and line-numbers, bridged from the pad-settings checkboxes into the embedded timeslider iframe so the two views agree. `nice-select.ts` dispatches a native `change` event after the jQuery trigger so the `addEventListener`-based bridge in `pad_mode.ts` fires (jQuery 3.7.1's `trigger()` does not dispatch native DOM events), and the font-family reset is fixed for jQuery 3 (which ignores a `null` css value). The five ad-hoc listener stores in `pad_mode.ts` are consolidated into one `bindOuter()` path and the three view-setting bridges into a single data-driven `bridgeView()` (refactor only).
- **Admin settings — explain env-var substitution and surface auth errors (#7819 / #7826).** Three env-var-only UX improvements driven by #7819 (a Docker operator saved an `ep_oauth` block in the Raw view and reported it "disappeared", not realising `settings.json` on disk is a *template*, not the effective config): a banner above the editor explaining the template/substitution model (rendered only when the loaded file contains a `${VAR}` placeholder); a read-only **Effective** tab exposing the redacted runtime settings the backend already emitted as `resolved` (also gated on `${VAR}`); and an `admin_auth_error` event so a misrouted Traefik+SSO session that isn't admin gets a clear toast instead of a silent "save did nothing". A reconnect-loop guard suppresses the SPA's auto-reconnect once an auth error has been received. No behaviour change for installs without `${VAR}` placeholders.
### Security hardening
A defence-in-depth pass across the API, token, export, and deployment surfaces:
- **HTTP API request handling, random IDs, and plugin loading (#7906).** `pad_utils.randomString` now generates random IDs via `crypto.getRandomValues` (CSPRNG) instead of `Math.random`. `OAuth2Provider` compares passwords with `crypto.timingSafeEqual` on the raw UTF-8 bytes (resolving the CodeQL "insufficient computational effort" alert) behind a uniform failure delay, and looks users up via own-property access only. `API.appendChatMessage` throws `padID does not exist` rather than creating the pad, consistent with the other content API methods. The `/api/2` REST router forwards only the `authorization` header (not the full request header set) and falls back to it whenever the field is falsy, matching the `openapi.ts` handler so both routers authenticate identically. `LinkInstaller` validates plugin dependency names before building filesystem paths from them, and the admin file server returns a generic error while logging details server-side.
- **Escape exported `data-*` attributes; warn on default/placeholder credentials (#7905).** `ExportHtml` now escapes the name and value of attributes emitted by the `exportHtmlAdditionalTagsWithData` hook, consistent with the URL/text escaping already applied to exported HTML. `Settings` logs a warning (error level under `NODE_ENV=production`) when an account uses a default/placeholder password from the shipped config, and the check is extended to cover `sso.clients[].client_secret` so enabling SSO without setting `ADMIN_SECRET` / `USER_SECRET` is flagged the same way.
- **Docker deployment defaults — require explicit credentials, default `TRUST_PROXY` off (#7907).** The shipped `docker-compose` now requires `ADMIN_PASSWORD` and the database password to be provided explicitly (no implicit fallback) and defaults `TRUST_PROXY` to `false`. Operators relying on the previous implicit defaults must now set these values explicitly.
### Notable fixes
- **History mode — lay the timeslider iframe in the editor's flex slot (#7903).** In-pad history mode positioned `#history-frame-mount` as an `inset:0` absolute overlay over `#editorcontainerbox`, which took the iframe out of flow and hid any in-flow side panel (e.g. `ep_webrtc`'s `#rtcbox` video column) beneath it — so history mode and live mode disagreed. The iframe now occupies the same in-flow flex slot the live editor uses, and a latent specificity bug (the `body.history-mode #editorcontainer { display: none }` hide rule was outranked by the two-id layout rule, so the live editor was only ever painted over) is fixed by giving the hide rule matching specificity. Adds a `padmode.spec.ts` regression test.
- **Pad editor — restore URL wrapping (#7894 / #7896).** Long URLs in the pad editor overflowed instead of wrapping because the global `a { white-space: nowrap }` rule overrode the wrapping properties on `#innerdocbody`. Explicit `white-space` / `word-wrap` / `overflow-wrap` on `#innerdocbody a` restores wrapping inside the editor while preserving no-wrap for links elsewhere in the UI.
- **RTL content option no longer flips the whole page (#7900 / #7901).** The per-pad RTL content option (`rtlIsTrue`) wrote the direction to the top-level `document.documentElement`, flipping the entire page — toolbar and chrome included. The content direction is now applied to the inner editor document (`targetDoc.documentElement`); page direction stays owned by the UI language (`l10n.ts`). Adds a frontend test asserting the inner editor flips while the top-level `<html>` dir is unchanged.
- **Pad-wide view settings apply to the creator's own view (#7900 / #7902).** Because a creator is never "enforced upon themselves", a stale personal view-override cookie (e.g. `rtlIsTrue=false` from an earlier toggle) silently masked the pad-wide value they later set, so the control appeared to do nothing on their own screen. Changing a pad-wide view option now syncs the creator's personal pref to the chosen value; the precedence model is unchanged (the creator can still override afterwards via "My view").
- **URL view-option params lost to a `padeditor.init` race (#7840 / #7843).** `?showLineNumbers=false` and `?useMonospaceFont=true` were silently clobbered shortly after load — the same race #7464 fixed for `?rtl=false`, but the neighbouring `showLineNumbers` / `noColors` / `useMonospaceFontGlobal` blocks were left at the synchronous-tail site. The fix is generalised to all three (moved into `postAceInit`). Mostly observable in cross-context iframe embeds that start with no `prefs` cookie. Adds `url_view_options.spec.ts`.
- **Default welcome text attributed to the system author (#7885 / #7887).** Auto-generated default pad content (`settings.defaultPadText` / `padDefaultContent` hook) carried the creating user's `author` attribute and rendered in their authorship colour, even though they never wrote it. The welcome text's `author` *attribute* is now `Pad.SYSTEM_AUTHOR_ID`, while revision 0's `meta.author` stays the real creator so ownership (pad-wide settings gate, deletion token) is preserved. Explicitly provided text (e.g. HTTP API `createPad` with text + author) keeps the real author.
- **URL-encode pad names in the admin 'Open' button and recent pads (#7865 / #7895).** Pad names are `encodeURIComponent`-d in the admin `PadPage` Open href and the colibris recent-pads href, and `decodeURIComponent`-d when read back from the URL pathname; legacy URL-encoded recent-pads names are normalised before re-encoding to prevent double-encoding (`%2F``%252F`). The admin Open `window.open` gains `noopener,noreferrer`.
- **OIDC — fix broken `OIDCAdapter` flows (#7837).** Repairs the adapter flows and widens the storage type to include `string` for the `userCode` index; adds regression tests.
- **Accessibility — dialog titles/descriptions and a missing l10n key (#7835 / #7836).** Adds the `index.code` key referenced by `index.html` but never defined (which produced a "Couldn't find translation key" console error on the landing page), and gives every admin `@radix-ui/react-dialog` `Dialog.Content` a `Dialog.Title` and `Dialog.Description` (visually hidden where there's no visible heading), silencing Radix's a11y warnings. A new backend spec fails CI if any `data-l10n-id` in `src/templates/*.html` is missing from `en.json`.
- **Offline/air-gapped Docker boot — stop pnpm self-provisioning a pinned version (issue #7911).** The official image installs pnpm directly (corepack was dropped for Node 25+). Because the image's pnpm intentionally lags the `packageManager` pin in `package.json` (pnpm 11.1.x enforces a minimum-release-age policy the frozen-lockfile build can't satisfy), pnpm treated every call — including the informational `pnpm --version` probe Etherpad runs at startup — as a request to download the pinned build. Behind a firewall that download failed (`Failed to get pnpm version: … Command exited with code 1`), breaking startup. The Dockerfile now sets `pnpm_config_pm_on_fail=ignore`, and the startup probe plus the updater's pnpm-on-PATH checks run with the same flag, so pnpm uses the installed version instead of reaching for the network (without changing which pnpm runs the build-time install). A backend spec fails CI if that guard is dropped while a version gap exists.
- **Firefox authorship colours — tag early keystrokes with the right author (#7910).** The inner editor's `thisAuthor` starts empty and is only populated when collab_client's queued `setProperty('userAuthor', userId)` reaches the iframe (applied asynchronously via `pendingInit`). Under Firefox timing the first keystrokes could beat it, so freshly typed text — and early line-attribute changes (lists, headings, alignment) — were tagged `author=''`, which canonicalises to an unattributed insert that the server's pad-corruption guard rejects, dropping the whole change and losing authorship (the intermittent `clear_authorship_color` flake, where undo couldn't restore the author colour). A `getLocalAuthor()` helper now falls back to `clientVars.userId` (the same id, available synchronously) whenever `thisAuthor` is still empty, applied at the text-insert sites and to seed `documentAttributeManager.author`; the intentional clear-authorship path and the server-side guard are unchanged.
- **Dark mode — fix the white address bar and the light-flash on load (#7909, issue #7606).** Dark-mode users still saw a white mobile address bar above the dark toolbar, and the whole page flashed light before going dark. Both came from rendering the light state server-side and switching to dark only after the JS bundle ran: iOS Safari reads `theme-color` at parse time and doesn't reliably repaint on a later JS mutation, and the page painted light before the bundle applied the dark skin classes. The server now emits a `prefers-color-scheme`-scoped `theme-color` pair so the address bar is correct at first paint, plus a small blocking `<head>` script that applies the dark skin classes before the stylesheet paints. Both are gated on `enableDarkMode` (default on) and the colibris skin; `pad.ts` still runs on init to wire up the `#options-darkmode` toggle (which now updates every `theme-color` meta) and theme the editor iframes. Applies to the pad and timeslider views.
### Internal / contributor-facing
- **Root-caused and fixed the Windows backend-test "silent ELIFECYCLE" flake (#7866).** The ~22% Windows flake — rotating across random spec files, no mocha summary, no JS trace — was diagnosed from a full-memory dump as two distinct causes. (1) A timing-fragile test abandoned by mocha keeps running and later throws an *orphan* unhandled rejection; `server.ts`'s process-global `uncaughtException`/`unhandledRejection` handlers (correct for a real Etherpad process) escalated that into a clean `process.exit`. They are now gated behind `require.main === module`, and the backend-test bootstraps (`common.ts`, `diagnostics.ts`) log orphan rejections instead of rethrowing. (2) A stack-buffer overrun in Node 24.x's bundled libuv Windows TCP-connect path (`uv__tcp_connect`) corrupts memory under the suite's localhost-connection churn; CI pins the Windows backend job to Node **24.16.0** (libuv 1.52.1, the bisected fix), referencing upstream `nodejs/node#63620`. Linux stays on Node 24 LTS.
- **Removed the now-unneeded ELIFECYCLE diagnostic scaffolding (#7846 / #7838 / #7842 / #7868).** The OS-level sidecar watcher, the diagnostics heartbeat/running-test pointer, and the mid-test snapshot — added to chase the flake above — are removed now that the cause is known.
- **Docs — document the Docker `settings.json` writable-layer and env-var-vs-file semantics (#7819 / #7827).** Two operator-facing gaps surfaced by #7819: that the on-disk `settings.json` is a template (env substitution happens in memory at load time), and that the default compose puts `settings.json` in the container's writable layer with no host mount, so admin edits are lost on `down`/`pull`/watchtower but survive a plain `restart`. Adds prose + a recreate-vs-restart table to `doc/docker.md` and a commented-out opt-in bind mount to the compose files.
- **Docs refresh for 3.2.0 (#7888)**, **dropped three redundant top-level files (#7839)**, **dropped a fragile viewport assertion in the enter test (#7845)**, and a backend-test fix-up.
### Dependencies
- Two major bumps: `redis` 5.12.1 → 6.0.0 (#7869) and `ejs` 5.0.2 → 6.0.1 (#7860).
- `ueberdb2` 6.1.2 → 6.1.8, `mssql` 12.5.3 → 12.5.5, `nodemailer` 8.0.7 → 8.0.10, `mysql2` 3.22.3 → 3.22.5 (#7915), `undici` 8.3.0 → 8.4.1 (#7914), `pdfkit` 0.18.0 → 0.19.0 (#7916), `oidc-provider` 9.8.3 → 9.8.4, `@elastic/elasticsearch` 9.4.1 → 9.4.2, `lru-cache` 11.5.0 → 11.5.1, `rate-limiter-flexible` 11.1.0 → 11.1.1, `semver` 7.8.1 → 7.8.2, `js-cookie` 3.0.7 → 3.0.8, `tsx` 4.22.3 → 4.22.4, `@radix-ui/react-switch` 1.2.6 → 1.3.0 (#7913), `@tanstack/react-query` 5.100.11 → 5.101.0 (+ devtools), plus `i18next`, `react-router-dom`, and several dev-dependency group bumps (#7912).
### Localisation
- Multiple updates from translatewiki.net.
# 3.2.0
3.2 adds first-class reverse-proxy / ingress support — `X-Forwarded-Prefix` and `X-Ingress-Path` are now honoured under `trustProxy`, so Etherpad can live under a subpath (Traefik, Nginx, Kubernetes Ingress) without breaking the PWA manifest, social-meta URLs, or any of the bootstrap asset links. The admin settings page learns to show *resolved* runtime values next to `${VAR:default}` placeholders, the v3.1.0 admin pad-list filter chips now apply server-side (so "show empty pads" no longer returns 012 of hundreds), and the v3.1.0 redesigned outdated-version gritter actually fires in production now (the session-based author lookup it shipped with always returned null for pad visitors).

View file

@ -5,6 +5,8 @@
## Pull requests
* PRs MUST include a non-empty description explaining what the change does and why
* PRs without a description should be flagged as incomplete
* the commit series in the PR should be _linear_ (it **should not contain merge commits**). This is necessary because we want to be able to [bisect](https://en.wikipedia.org/wiki/Bisection_(software_engineering)) bugs easily. Rewrite history/perform a rebase if necessary
* PRs should be issued against the **develop** branch: we never pull directly into **master**
* PRs **should not have conflicts** with develop. If there are, please resolve them rebasing and force-pushing
@ -123,7 +125,7 @@ Documentation should be kept up-to-date. This means, whenever you add a new API
You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet.
## Testing
Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
Front-end tests are found in the `src/tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
Back-end tests can be run from the `src` directory, via `npm test`.
You can use `npm test -- --inspect-brk` and navigate to `edge://inspect` or `chrome://inspect` to debug the tests.

View file

@ -7,6 +7,11 @@
# docker build --build-arg BUILD_ENV=copy .
ARG BUILD_ENV=git
# NOTE: this intentionally lags the "packageManager" pin in package.json. pnpm
# 11.1.x enforces the minimum-release-age supply-chain policy during install,
# which the frozen-lockfile Docker build can't satisfy, so the image stays on
# 11.0.x. The version gap is made harmless by pnpm_config_pm_on_fail=ignore in
# the build stage below — see ether/etherpad#7911.
ARG PnpmVersion=11.0.6
FROM node:24-alpine AS adminbuild
@ -28,6 +33,17 @@ RUN pnpm run build:ui
FROM node:24-alpine AS build
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad"
# The image's pnpm intentionally lags the "packageManager" pin (see the ARG
# note above). pnpm would otherwise try to self-provision the pinned version on
# invocation — including the informational `pnpm --version` probe Etherpad runs
# at startup — which fails closed with no network and breaks air-gapped boots
# (ether/etherpad#7911). pm_on_fail=ignore makes pnpm use the installed version
# instead. Inherited by the development and production runtime stages, so it
# also covers the updater's pnpm-on-PATH check and ad-hoc `pnpm` in an exec
# shell. It does not change which pnpm runs the build-time install (still the
# installed 11.0.x), so the frozen-lockfile build is unaffected.
ENV pnpm_config_pm_on_fail=ignore
# Set these arguments when building the image from behind a proxy
ARG http_proxy=
ARG https_proxy=

View file

@ -172,7 +172,7 @@ volumes:
### Docker container
Find [here](doc/docker.adoc) information on running Etherpad in a container.
Find [here](doc/docker.md) information on running Etherpad in a container.
## Plugins
@ -247,8 +247,8 @@ git -P tag --list "v*" --merged
```
4. Select the version
```sh
git checkout v2.2.5
git switch -c v2.2.5
git checkout v3.2.0
git switch -c v3.2.0
```
5. Upgrade Etherpad
```sh

View file

@ -1,7 +1,7 @@
{
"name": "admin",
"private": true,
"version": "3.2.0",
"version": "3.3.2",
"type": "module",
"scripts": {
"dev": "pnpm gen:api && vite",
@ -14,39 +14,40 @@
"test": "pnpm gen:api && tsx --test 'src/**/__tests__/*.test.ts' 'src/**/__tests__/*.test.tsx'"
},
"dependencies": {
"@radix-ui/react-switch": "^1.2.6",
"@tanstack/react-query": "^5.100.11",
"@tanstack/react-query-devtools": "^5.100.11",
"@radix-ui/react-switch": "^1.3.3",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-query-devtools": "^5.101.2",
"jsonc-parser": "^3.3.1",
"openapi-fetch": "^0.17.0",
"openapi-react-query": "^0.5.4"
},
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-toast": "^1.2.15",
"@types/react": "^19.2.15",
"@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-toast": "^1.2.19",
"@radix-ui/react-visually-hidden": "^1.2.7",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@vitejs/plugin-react": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"@vitejs/plugin-react": "^6.0.3",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^10.4.0",
"eslint": "^10.6.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"i18next": "^26.2.0",
"eslint-plugin-react-refresh": "^0.5.3",
"i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.16.0",
"lucide-react": "^1.23.0",
"openapi-typescript": "^7.13.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-hook-form": "^7.76.0",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.15.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-hook-form": "^7.81.0",
"react-i18next": "^17.0.9",
"react-router-dom": "^7.18.1",
"socket.io-client": "^4.8.3",
"tsx": "^4.22.3",
"tsx": "^4.23.0",
"typescript": "^6.0.3",
"vite": "^8.0.14",
"vite": "^8.1.4",
"vite-plugin-babel": "^1.7.3",
"zustand": "^5.0.13"
"zustand": "^5.0.14"
}
}

View file

@ -14,6 +14,8 @@
"cap-warning": "Showing the first 1000 authors. Narrow your search to see more.",
"feature-disabled-banner": "Author erasure is disabled. Set \"gdprAuthorErasure\": {\"enabled\": true} in settings.json to enable.",
"no-results": "No authors match this search.",
"confirm-dialog-title": "Confirm author erasure",
"confirm-dialog-description": "Review the impact of erasing this author and confirm or cancel.",
"confirm-preview-title": "Erase author {{name}}",
"confirm-preview-counters": "Will clear {{tokenMappings}} token mappings, {{externalMappings}} mapper bindings, and {{chatMessages}} chat messages across {{affectedPads}} pads.",
"confirm-irreversible": "This cannot be undone.",

View file

@ -34,6 +34,37 @@ textarea.settings:focus {
border-top: 1px solid #ddd;
}
/* --- env-var banner --- */
/* Shown only when settings.json contains ${VAR} placeholders. The
typical reader is a Docker/K8s operator who has just been surprised
by env-var substitution semantics, so the copy must explain rather
than warn visual weight matches a note, not an error. */
.settings-envvar-banner {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
margin-bottom: 16px;
background: #f5f7ff;
border: 1px solid #c7d2fe;
border-radius: 6px;
color: #1e293b;
}
.settings-envvar-banner svg {
flex-shrink: 0;
color: #4f46e5;
margin-top: 2px;
}
.settings-envvar-banner strong {
display: block;
margin-bottom: 4px;
}
.settings-envvar-banner p {
margin: 0;
font-size: 13px;
line-height: 1.5;
}
/* --- mode toggle --- */
.settings-mode-toggle {
display: inline-flex;

View file

@ -32,12 +32,19 @@ export const App = () => {
const settingSocket = connect(`${WS_URL}/settings`, {transports: ['websocket']});
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {transports: ['websocket']})
// When the server explicitly rejects us for not being admin, we must
// NOT reconnect on the subsequent `disconnect` event — otherwise the
// socket cycles forever (connect → admin_auth_error → server
// disconnect → SPA auto-reconnect → …). The flag is reset on each
// successful connect.
let authErrored = false;
pluginsSocket.on('connect', () => {
useStore.getState().setPluginsSocket(pluginsSocket);
});
settingSocket.on('connect', () => {
authErrored = false;
useStore.getState().setSettingsSocket(settingSocket);
useStore.getState().setShowLoading(false)
settingSocket.emit('load');
@ -46,7 +53,7 @@ export const App = () => {
settingSocket.on('disconnect', (reason) => {
useStore.getState().setShowLoading(true)
if (reason === 'io server disconnect') settingSocket.connect();
if (reason === 'io server disconnect' && !authErrored) settingSocket.connect();
});
settingSocket.on('settings', (settings: any) => {
@ -75,6 +82,22 @@ export const App = () => {
const detail = payload?.message ?? '';
setToastState({open: true, title: t('admin_settings.toast.save_failed') + (detail ? ` (${detail})` : ''), success: false});
}
});
// Backend emits this when the connecting socket does not have an
// admin session. Previously the server just dropped silently, so the
// SPA would sit on a loading screen with no clue. Surface it AND
// suppress the automatic reconnect — without this flag the SPA would
// immediately reconnect to a socket that will reject it again.
settingSocket.on('admin_auth_error', (payload?: {message?: string}) => {
authErrored = true;
const {setToastState} = useStore.getState();
setToastState({
open: true,
title: payload?.message || t('admin_settings.toast.auth_error'),
success: false,
});
useStore.getState().setShowLoading(false);
})
return () => {

View file

@ -1,13 +1,17 @@
import { Trans, useTranslation } from 'react-i18next';
export type Mode = 'form' | 'raw';
export type Mode = 'form' | 'raw' | 'effective';
type Props = {
mode: Mode;
onChange: (mode: Mode) => void;
// When false, the Effective tab is hidden. We hide it for installs that
// aren't using env-var substitution at all — there's no useful difference
// between the raw file and the effective in-memory config for them.
showEffective?: boolean;
};
export const ModeToggle = ({ mode, onChange }: Props) => {
export const ModeToggle = ({ mode, onChange, showEffective = false }: Props) => {
const { t } = useTranslation();
return (
<div className="settings-mode-toggle" role="tablist" aria-label={t('admin_settings.mode.aria_label')}>
@ -31,6 +35,19 @@ export const ModeToggle = ({ mode, onChange }: Props) => {
>
<Trans i18nKey="admin_settings.mode.raw" />
</button>
{showEffective && (
<button
type="button"
role="tab"
aria-selected={mode === 'effective'}
data-testid="mode-toggle-effective"
className={mode === 'effective' ? 'active' : ''}
onClick={() => onChange('effective')}
title={t('admin_settings.mode.effective_tooltip')}
>
<Trans i18nKey="admin_settings.mode.effective" />
</button>
)}
</div>
);
};

View file

@ -1,6 +1,7 @@
import {Trans, useTranslation} from "react-i18next";
import {useEffect, useMemo, useState} from "react";
import * as Dialog from "@radix-ui/react-dialog";
import {VisuallyHidden} from "@radix-ui/react-visually-hidden";
import {ChevronLeft, ChevronRight, Trash2} from "lucide-react";
import {useStore} from "../store/store.ts";
import {SearchField} from "../components/SearchField.tsx";
@ -153,16 +154,20 @@ export const AuthorPage = () => {
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<VisuallyHidden asChild>
<Dialog.Title>{t('ep_admin_authors:confirm-dialog-title')}</Dialog.Title>
</VisuallyHidden>
<VisuallyHidden asChild>
<Dialog.Description>{t('ep_admin_authors:confirm-dialog-description')}</Dialog.Description>
</VisuallyHidden>
{dialog.phase === 'loading-preview' && <div>
<Trans i18nKey="ep_admin_authors:loading-preview" ns="ep_admin_authors"/>
</div>}
{(dialog.phase === 'preview' || dialog.phase === 'committing') && (() => {
const p = dialog.preview;
return <div>
<Dialog.Title asChild>
<h3>{t('ep_admin_authors:confirm-preview-title',
{name: p.name || p.authorID})}</h3>
</Dialog.Title>
<h3>{t('ep_admin_authors:confirm-preview-title',
{name: p.name || p.authorID})}</h3>
<p>{t('ep_admin_authors:confirm-preview-counters', {
tokenMappings: p.removedTokenMappings,
externalMappings: p.removedExternalMappings,

View file

@ -4,6 +4,7 @@ import {useStore} from "../store/store.ts";
import {PadFilter, PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts";
import {useDebounce} from "../utils/useDebounce.ts";
import * as Dialog from "@radix-ui/react-dialog";
import {VisuallyHidden} from "@radix-ui/react-visually-hidden";
import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack, PlusIcon, Search, X, RefreshCw, History} from "lucide-react";
import {useForm} from "react-hook-form";
import type {TFunction} from "i18next";
@ -165,7 +166,10 @@ export const PadPage = () => {
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<div>{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}</div>
<VisuallyHidden asChild><Dialog.Title>{t('admin_pads.delete_pad_dialog_title')}</Dialog.Title></VisuallyHidden>
<Dialog.Description asChild>
<div>{t('ep_admin_pads:ep_adminpads2_confirm', {padID: padToDelete})}</div>
</Dialog.Description>
<div className="settings-button-bar">
<button onClick={() => setDeleteDialog(false)}><Trans i18nKey="admin_pads.cancel"/></button>
<button onClick={() => { deletePad(padToDelete); setDeleteDialog(false) }}>{t('admin_pads.confirm_button')}</button>
@ -178,7 +182,10 @@ export const PadPage = () => {
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<div>{t('admin_pads.error_prefix')}: {errorText}</div>
<VisuallyHidden asChild><Dialog.Title>{t('admin_pads.error_prefix')}</Dialog.Title></VisuallyHidden>
<Dialog.Description asChild>
<div>{t('admin_pads.error_prefix')}: {errorText}</div>
</Dialog.Description>
<div className="settings-button-bar">
<button onClick={() => setErrorText(null)}>{t('admin_pads.confirm_button')}</button>
</div>
@ -191,6 +198,7 @@ export const PadPage = () => {
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<Dialog.Title className="dialog-confirm-title"><Trans i18nKey="index.newPad"/></Dialog.Title>
<VisuallyHidden asChild><Dialog.Description>{t('admin_pads.create_pad_dialog_description')}</Dialog.Description></VisuallyHidden>
<form onSubmit={handleSubmit(onPadCreate)}>
<button className="dialog-close-button" type="button" onClick={() => setCreatePadDialogOpen(false)}>×</button>
<div style={{display: 'grid', gap: '10px', gridTemplateColumns: 'auto auto', marginBottom: '1rem'}}>
@ -410,7 +418,7 @@ export const PadPage = () => {
</button>
<button
className="pm-btn pm-btn-primary pm-btn--sm"
onClick={() => window.open(`../../p/${pad.padName}`, '_blank')}
onClick={() => window.open(`../../p/${encodeURIComponent(pad.padName)}`, '_blank', 'noopener,noreferrer')}
>
<Eye size={13}/> <Trans i18nKey="admin_pads.open"/>
</button>

View file

@ -1,22 +1,42 @@
import React, { useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useStore } from '../store/store';
import { isJSONClean, cleanComments } from '../utils/utils';
import { Trans, useTranslation } from 'react-i18next';
import { IconButton } from '../components/IconButton';
import { RotateCw, Save, AlignLeft, ShieldCheck } from 'lucide-react';
import { RotateCw, Save, AlignLeft, ShieldCheck, Info } from 'lucide-react';
import { FormView } from '../components/settings/FormView';
import { ModeToggle, type Mode } from '../components/settings/ModeToggle';
const TAB_INDENT = ' ';
// Heuristic: `${VAR}` or `${VAR:default}` in the file means the operator is
// running with env-var substitution (overwhelmingly Docker / Kubernetes).
// We use this to gate the Docker-aware UX (the explanatory banner and the
// Effective-config tab) so non-container installs see the existing UI
// unchanged. Conservative on purpose — false negatives just keep the old
// behaviour.
const ENV_VAR_PATTERN = /\$\{[A-Za-z_][A-Za-z0-9_]*(?::[^}]*)?\}/;
export const SettingsPage = () => {
const { t } = useTranslation();
const settingsSocket = useStore(state => state.settingsSocket);
const settings = useStore(state => state.settings) ?? '';
const resolved = useStore(state => state.resolved);
const usesEnvVars = useMemo(() => ENV_VAR_PATTERN.test(settings), [settings]);
const [mode, setMode] = useState<Mode>('form');
const [exposeExperimental] = useState(false);
// The Effective tab is only meaningful when there is a `resolved`
// payload AND the file uses substitution. Falling back to Raw on
// either condition keeps the toggle honest if the user opens this
// page against an older server.
const canShowEffective = usesEnvVars && resolved != null;
useEffect(() => {
if (mode === 'effective' && !canShowEffective) setMode('raw');
}, [mode, canShowEffective]);
// Tab in textarea inserts two spaces instead of moving focus; rAF restores caret position after React re-renders.
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== 'Tab') return;
@ -57,49 +77,80 @@ export const SettingsPage = () => {
settingsSocket.emit('saveSettings', settings);
};
const effectiveJson = useMemo(() => {
if (resolved == null) return '';
try { return JSON.stringify(resolved, null, 2); } catch { return ''; }
}, [resolved]);
return (
<div className="settings-page">
<h1><Trans i18nKey="admin_settings.current" /></h1>
<ModeToggle mode={mode} onChange={setMode} />
{usesEnvVars && (
<div
className="settings-envvar-banner"
role="note"
data-testid="settings-envvar-banner"
>
<Info size={18} aria-hidden="true" />
<div>
<strong><Trans i18nKey="admin_settings.envvar_banner.title" /></strong>
<p><Trans i18nKey="admin_settings.envvar_banner.body" /></p>
</div>
</div>
)}
{mode === 'form'
? <FormView onSwitchToRaw={() => setMode('raw')} />
: (
<textarea
value={settings}
className="settings"
data-testid="settings-raw-textarea"
spellCheck={false}
onKeyDown={handleKeyDown}
onChange={v => useStore.getState().setSettings(v.target.value)}
/>
)
}
<ModeToggle mode={mode} onChange={setMode} showEffective={canShowEffective} />
{mode === 'form' && <FormView onSwitchToRaw={() => setMode('raw')} />}
{mode === 'raw' && (
<textarea
value={settings}
className="settings"
data-testid="settings-raw-textarea"
spellCheck={false}
onKeyDown={handleKeyDown}
onChange={v => useStore.getState().setSettings(v.target.value)}
/>
)}
{mode === 'effective' && (
<textarea
value={effectiveJson}
className="settings"
data-testid="settings-effective-textarea"
spellCheck={false}
readOnly
aria-readonly="true"
/>
)}
<div className="settings-button-bar">
<IconButton
className="settingsButton"
data-testid="save-settings-button"
icon={<Save />}
title={<Trans i18nKey="admin_settings.current_save.value" />}
onClick={handleSave}
/>
<IconButton
className="settingsButton"
data-testid="test-settings-button"
icon={<ShieldCheck />}
title={<Trans i18nKey="admin_settings.current_test.value" />}
onClick={testJSON}
/>
{exposeExperimental && (
<IconButton
className="settingsButton"
data-testid="prettify-settings-button"
icon={<AlignLeft />}
title={<Trans i18nKey="admin_settings.current_prettify.value" />}
onClick={prettifyJSON}
/>
{mode !== 'effective' && (
<>
<IconButton
className="settingsButton"
data-testid="save-settings-button"
icon={<Save />}
title={<Trans i18nKey="admin_settings.current_save.value" />}
onClick={handleSave}
/>
<IconButton
className="settingsButton"
data-testid="test-settings-button"
icon={<ShieldCheck />}
title={<Trans i18nKey="admin_settings.current_test.value" />}
onClick={testJSON}
/>
{exposeExperimental && (
<IconButton
className="settingsButton"
data-testid="prettify-settings-button"
icon={<AlignLeft />}
title={<Trans i18nKey="admin_settings.current_prettify.value" />}
onClick={prettifyJSON}
/>
)}
</>
)}
<IconButton
className="settingsButton"

View file

@ -1,13 +1,18 @@
import {useStore} from "../store/store.ts";
import * as Dialog from '@radix-ui/react-dialog';
import {VisuallyHidden} from '@radix-ui/react-visually-hidden';
import {useTranslation} from 'react-i18next';
import brand from './brand.svg'
export const LoadingScreen = ()=>{
const showLoading = useStore(state => state.showLoading)
const {t} = useTranslation()
return <Dialog.Root open={showLoading}><Dialog.Portal>
<Dialog.Overlay className="loading-screen fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
<VisuallyHidden asChild><Dialog.Title>{t('admin.loading')}</Dialog.Title></VisuallyHidden>
<VisuallyHidden asChild><Dialog.Description>{t('admin.loading_description')}</Dialog.Description></VisuallyHidden>
<div className="flex flex-col items-center">
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
<div className="mt-4 text-[--fg-color]">

View file

@ -1,132 +0,0 @@
# Contributor Guidelines
(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad#get-in-touch))
**We have decided that LLM/Agent/AI contributions are fine as long as they are within the instructions set out by this document.**
## Pull requests
* PRs MUST include a non-empty description explaining what the change does and why
* PRs without a description should be flagged as incomplete
* the commit series in the PR should be _linear_ (it **should not contain merge commits**). This is necessary because we want to be able to [bisect](https://en.wikipedia.org/wiki/Bisection_(software_engineering)) bugs easily. Rewrite history/perform a rebase if necessary
* PRs should be issued against the **develop** branch: we never pull directly into **master**
* PRs **should not have conflicts** with develop. If there are, please resolve them rebasing and force-pushing
* when preparing your PR, please make sure that you have included the relevant **changes to the documentation** (preferably with usage examples)
* contain meaningful and detailed **commit messages** in the form:
```
submodule: description
longer description of the change you have made, eventually mentioning the
number of the issue that is being fixed, in the form: Fixes #someIssueNumber
```
* if the PR is a **bug fix**:
* The commit that fixes the bug should **include a regression test** that
would fail if the bug fix was reverted. Adding the regression test in the
same commit as the bug fix makes it easier for a reviewer to verify that the
test is appropriate for the bug fix.
* If there is a bug report, **the pull request description should include the
text "`Fixes #xxx`"** so that the bug report is auto-closed when the PR is
merged. It is less useful to say the same thing in a commit message because
GitHub will spam the bug report every time the commit is rebased, and
because a bug number alone becomes meaningless in forks. (A full URL would
be better, but ideally each commit is readable on its own without the need
to examine an external reference to understand motivation or context.)
* think about stability: code has to be backwards compatible as much as possible. Always **assume your code will be run with an older version of the DB/config file**
* if you want to remove a feature, **deprecate it instead**:
* write an issue with your deprecation plan
* output a `WARN` in the log informing that the feature is going to be removed
* remove the feature in the next version
* if you want to add a new feature, put it under a **feature flag**:
* once the new feature has reached a minimal level of stability, do a PR for it, so it can be integrated early
* expose a mechanism for enabling/disabling the feature
* the new feature should be **disabled** by default. With the feature disabled, the code path should be exactly the same as before your contribution. This is a __necessary condition__ for early integration
* think of the PR not as something that __you wrote__, but as something that __someone else is going to read__. The commit series in the PR should tell a novice developer the story of your thoughts when developing it
## How to write a bug report
* Please be polite, we all are humans and problems can occur.
* Please add as much information as possible, for example
* client os(s) and version(s)
* browser(s) and version(s), is the problem reproducible on different clients
* special environments like firewalls or antivirus
* host os and version
* npm and nodejs version
* Logfiles if available
* steps to reproduce
* what you expected to happen
* what actually happened
* Please format logfiles and code examples with markdown see github Markdown help below the issue textarea for more information.
If you send logfiles, please set the loglevel switch DEBUG in your settings.json file:
```
/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
"loglevel": "DEBUG",
```
The logfile location is defined in startup script or the log is directly shown in the commandline after you have started etherpad.
## General goals of Etherpad
To make sure everybody is going in the same direction:
* easy to install for admins and easy to use for people
* easy to integrate into other apps, but also usable as standalone
* lightweight and scalable
* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core.
Also, keep it maintainable. We don't wanna end up as the monster Etherpad was!
## How to work with git?
* Don't work in your master branch.
* Make a new branch for every feature you're working on. (This ensures that you can work you can do lots of small, independent pull requests instead of one big one with complete different features)
* Don't use the online edit function of github (this only creates ugly and not working commits!)
* Try to make clean commits that are easy readable (including descriptive commit messages!)
* Test before you push. Sounds easy, it isn't!
* Don't check in stuff that gets generated during build or runtime
* Make small pull requests that are easy to review but make sure they do add value by themselves / individually
## Coding style
* Do write comments. (You don't have to comment every line, but if you come up with something that's a bit complex/weird, just leave a comment. Bear in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless!)
* Never ever use tabs
* Indentation: 2 spaces
* Don't overengineer. Don't try to solve any possible problem in one step, but try to solve problems as easy as possible and improve the solution over time!
* Do generalize sooner or later! (if an old solution, quickly hacked together, poses more problems than it solves today, refactor it!)
* Keep it compatible. Do not introduce changes to the public API, db schema or configurations too lightly. Don't make incompatible changes without good reasons!
* If you do make changes, document them! (see below)
* Use protocol independent urls "//"
## Branching model / git workflow
see git flow http://nvie.com/posts/a-successful-git-branching-model/
### `master` branch
* the stable
* This is the branch everyone should use for production stuff
### `develop`branch
* everything that is READY to go into master at some point in time
* This stuff is tested and ready to go out
### release branches
* stuff that should go into master very soon
* only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why)
* we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle.
### hotfix branches
* fixes for bugs in master
### feature branches (in your own repos)
* these are the branches where you develop your features in
* If it's ready to go out, it will be merged into develop
Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop
## Documentation
The docs are in the `doc/` folder in the git repository, so people can easily find the suitable docs for the current git revision.
Documentation should be kept up-to-date. This means, whenever you add a new API method, add a new hook or change the database model, pack the relevant changes to the docs in the same pull request.
You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet.
## Testing
Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
Back-end tests can be run from the `src` directory, via `npm test`.
You can use `npm test -- --inspect-brk` and navigate to `edge://inspect` or `chrome://inspect` to debug the tests.

View file

@ -4,9 +4,9 @@
* Compact every pad on the instance to reclaim database space.
*
* Usage:
* node bin/compactAllPads.js # collapse all history on every pad
* node bin/compactAllPads.js --keep N # keep last N revisions per pad
* node bin/compactAllPads.js --dry-run # list pads + rev counts, no writes
* pnpm run --filter bin compactAllPads # collapse all history on every pad
* pnpm run --filter bin compactAllPads --keep N # keep last N revisions per pad
* pnpm run --filter bin compactAllPads --dry-run # list pads + rev counts, no writes
*
* Composes the existing `listAllPads` and `compactPad` HTTP APIs there is
* deliberately no instance-wide HTTP endpoint, because doing this over a
@ -170,9 +170,9 @@ export const parseArgs = (argv: string[]): CompactAllOpts | null => {
// so the test harness can use `runCompactAll` directly without network.
const usage = () => {
console.error('Usage:');
console.error(' node bin/compactAllPads.js');
console.error(' node bin/compactAllPads.js --keep <N>');
console.error(' node bin/compactAllPads.js --dry-run');
console.error(' pnpm run --filter bin compactAllPads');
console.error(' pnpm run --filter bin compactAllPads --keep <N>');
console.error(' pnpm run --filter bin compactAllPads --dry-run');
process.exit(2);
};

View file

@ -4,8 +4,8 @@
* Compact a pad's revision history to reclaim database space.
*
* Usage:
* node bin/compactPad.js <padID> # collapse all history
* node bin/compactPad.js <padID> --keep N # keep only the last N revisions
* pnpm run --filter bin compactPad <padID> # collapse all history
* pnpm run --filter bin compactPad <padID> --keep N # keep only the last N revisions
*
* Wraps the existing Cleanup helper (src/node/utils/Cleanup.ts) via the
* compactPad HTTP API so admins can trigger it from the CLI without
@ -41,8 +41,8 @@ const apiPost = async (p: string): Promise<any> => {
const usage = () => {
console.error('Usage:');
console.error(' node bin/compactPad.js <padID>');
console.error(' node bin/compactPad.js <padID> --keep <N>');
console.error(' pnpm run --filter bin compactPad <padID>');
console.error(' pnpm run --filter bin compactPad <padID> --keep <N>');
process.exit(2);
};

View file

@ -4,9 +4,9 @@
* Compact every pad on the instance that has not been edited recently.
*
* Usage:
* node bin/compactStalePads.js --older-than 90 # collapse history on pads not edited in 90 days
* node bin/compactStalePads.js --older-than 90 --keep 50 # keep last 50 revisions
* node bin/compactStalePads.js --older-than 90 --dry-run # list, don't write
* pnpm run --filter bin compactStalePads --older-than 90 # collapse history on pads not edited in 90 days
* pnpm run --filter bin compactStalePads --older-than 90 --keep 50 # keep last 50 revisions
* pnpm run --filter bin compactStalePads --older-than 90 --dry-run # list, don't write
*
* Composes `listAllPads` `getLastEdited` `compactPad`. Same shape as
* `bin/compactAllPads` (per-pad error tolerance, dry-run, tally), but
@ -255,9 +255,9 @@ export const parseArgs = (argv: string[]): CompactStaleOpts | null => {
const usage = () => {
console.error('Usage:');
console.error(' node bin/compactStalePads.js --older-than <days>');
console.error(' node bin/compactStalePads.js --older-than <days> --keep <N>');
console.error(' node bin/compactStalePads.js --older-than <days> --dry-run');
console.error(' pnpm run --filter bin compactStalePads --older-than <days>');
console.error(' pnpm run --filter bin compactStalePads --older-than <days> --keep <N>');
console.error(' pnpm run --filter bin compactStalePads --older-than <days> --dry-run');
process.exit(2);
};

View file

@ -1,203 +0,0 @@
#!/bin/bash
#
# WARNING: since Etherpad 1.7.0 (2018-08-17), this script is DEPRECATED, and
# will be removed/modified in a future version.
# It's left here just for documentation.
# The branching policies for releases have been changed.
#
# This script is used to publish a new release/version of etherpad on github
#
# Work that is done by this script:
# ETHER_REPO:
# - Add text to CHANGELOG.md
# - Replace version of etherpad in src/package.json
# - Create a release branch and push it to github
# - Merges this release branch into master branch
# - Creating the windows build and the docs
# ETHER_WEB_REPO:
# - Creating a new branch with the docs and the windows build
# - Replacing the version numbers in the index.html
# - Push this branch and merge it to master
# ETHER_REPO:
# - Create a new release on github
printf "WARNING: since Etherpad 1.7.0 this script is DEPRECATED, and will be removed/modified in a future version.\n\n"
while true; do
read -p "Do you want to continue? This is discouraged. [y/N]" yn
case $yn in
[Yy]* ) break;;
[Nn]* ) exit;;
* ) printf "Please answer yes or no.\n\n";;
esac
done
ETHER_REPO="https://github.com/ether/etherpad.git"
ETHER_WEB_REPO="https://github.com/ether/ether.github.com.git"
TMP_DIR="/tmp/"
echo "WARNING: You can only run this script if your github api token is allowed to create and merge branches on $ETHER_REPO and $ETHER_WEB_REPO."
echo "This script automatically changes the version number in package.json and adds a text to CHANGELOG.md."
echo "When you use this script you should be in the branch that you want to release (develop probably) on latest version. Any changes that are currently not committed will be committed."
echo "-----"
# Get the latest version
LATEST_GIT_TAG=$(git tag | tail -n 1)
# Current environment
echo "Current environment: "
echo "- branch: $(git branch | grep '* ')"
echo "- last commit date: $(git show --quiet --pretty=format:%ad)"
echo "- current version: $LATEST_GIT_TAG"
echo "- temp dir: $TMP_DIR"
# Get new version number
# format: x.x.x
echo -n "Enter new version (x.x.x): "
read VERSION
# Get the message for the changelogs
read -p "Enter new changelog entries (press enter): "
tmp=$(mktemp)
"${EDITOR:-vi}" $tmp
changelogText=$(<$tmp)
echo "$changelogText"
rm $tmp
if [ "$changelogText" != "" ]; then
changelogText="# $VERSION\n$changelogText"
fi
# get the token for the github api
echo -n "Enter your github api token: "
read API_TOKEN
function check_api_token {
echo "Checking if github api token is valid..."
CURL_RESPONSE=$(curl --silent -i https://api.github.com/user?access_token=$API_TOKEN | iconv -f utf8)
HTTP_STATUS=$(echo $CURL_RESPONSE | head -1 | sed -r 's/.* ([0-9]{3}) .*/\1/')
[[ $HTTP_STATUS != "200" ]] && echo "Aborting: Invalid github api token" && exit 1
}
function modify_files {
# Add changelog text to first line of CHANGELOG.md
msg=""
# source: https://unix.stackexchange.com/questions/9784/how-can-i-read-line-by-line-from-a-variable-in-bash#9789
while IFS= read -r line
do
# replace newlines with literal "\n" for using with sed
msg+="$line\n"
done < <(printf '%s\n' "${changelogText}")
sed -i "1s/^/${msg}\n/" CHANGELOG.md
[[ $? != 0 ]] && echo "Aborting: Error modifying CHANGELOG.md" && exit 1
# Replace version number of etherpad in package.json
sed -i -r "s/(\"version\"[ ]*: \").*(\")/\1$VERSION\2/" src/package.json
[[ $? != 0 ]] && echo "Aborting: Error modifying package.json" && exit 1
}
function create_release_branch {
echo "Creating new release branch..."
git rev-parse --verify release/$VERSION 2>/dev/null
if [ $? == 0 ]; then
echo "Aborting: Release branch already present"
exit 1
fi
git checkout -b release/$VERSION
[[ $? != 0 ]] && echo "Aborting: Error creating release branch" && exit 1
echo "Committing CHANGELOG.md and package.json"
git add CHANGELOG.md
git add src/package.json
git commit -m "Release version $VERSION"
echo "Pushing release branch to github..."
git push -u $ETHER_REPO release/$VERSION
[[ $? != 0 ]] && echo "Aborting: Error pushing release branch to github" && exit 1
}
function merge_release_branch {
echo "Merging release to master branch on github..."
API_JSON=$(printf '{"base": "master","head": "release/%s","commit_message": "Merge new release into master branch!"}' $VERSION)
CURL_RESPONSE=$(curl --silent -i -N --data "$API_JSON" https://api.github.com/repos/ether/etherpad-lite/merges?access_token=$API_TOKEN | iconv -f utf8)
echo $CURL_RESPONSE
HTTP_STATUS=$(echo $CURL_RESPONSE | head -1 | sed -r 's/.* ([0-9]{3}) .*/\1/')
[[ $HTTP_STATUS != "200" ]] && echo "Aborting: Error merging release branch on github" && exit 1
}
function create_builds {
echo "Cloning etherpad-lite repo and ether.github.com repo..."
cd $TMP_DIR
rm -rf etherpad-lite ether.github.com
git clone $ETHER_REPO --branch master
git clone $ETHER_WEB_REPO
echo "Creating windows build..."
cd etherpad-lite
bin/buildForWindows.sh
[[ $? != 0 ]] && echo "Aborting: Error creating build for windows" && exit 1
echo "Creating docs..."
make docs
[[ $? != 0 ]] && echo "Aborting: Error generating docs" && exit 1
}
function push_builds {
cd $TMP_DIR/etherpad-lite/
echo "Copying windows build and docs to website repo..."
GIT_SHA=$(git rev-parse HEAD | cut -c1-10)
mv etherpad-win.zip $TMP_DIR/ether.github.com/downloads/etherpad-win-$VERSION-$GIT_SHA.zip
mv out/doc $TMP_DIR/ether.github.com/doc/v$VERSION
cd $TMP_DIR/ether.github.com/
sed -i "s/etherpad-win.*\.zip/etherpad-win-$VERSION-$GIT_SHA.zip/" index.html
sed -i "s/$LATEST_GIT_TAG/$VERSION/g" index.html
git checkout -b release_$VERSION
[[ $? != 0 ]] && echo "Aborting: Error creating new release branch" && exit 1
git add doc/
git add downloads/
git commit -a -m "Release version $VERSION"
git push -u $ETHER_WEB_REPO release_$VERSION
[[ $? != 0 ]] && echo "Aborting: Error pushing release branch to github" && exit 1
}
function merge_web_branch {
echo "Merging release to master branch on github..."
API_JSON=$(printf '{"base": "master","head": "release_%s","commit_message": "Release version %s"}' $VERSION $VERSION)
CURL_RESPONSE=$(curl --silent -i -N --data "$API_JSON" https://api.github.com/repos/ether/ether.github.com/merges?access_token=$API_TOKEN | iconv -f utf8)
echo $CURL_RESPONSE
HTTP_STATUS=$(echo $CURL_RESPONSE | head -1 | sed -r 's/.* ([0-9]{3}) .*/\1/')
[[ $HTTP_STATUS != "200" ]] && echo "Aborting: Error merging release branch" && exit 1
}
function publish_release {
echo -n "Do you want to publish a new release on github (y/n)? "
read PUBLISH_RELEASE
if [ $PUBLISH_RELEASE = "y" ]; then
# create a new release on github
API_JSON=$(printf '{"tag_name": "%s","target_commitish": "master","name": "Release %s","body": "%s","draft": false,"prerelease": false}' $VERSION $VERSION $changelogText)
CURL_RESPONSE=$(curl --silent -i -N --data "$API_JSON" https://api.github.com/repos/ether/etherpad-lite/releases?access_token=$API_TOKEN | iconv -f utf8)
HTTP_STATUS=$(echo $CURL_RESPONSE | head -1 | sed -r 's/.* ([0-9]{3}) .*/\1/')
[[ $HTTP_STATUS != "201" ]] && echo "Aborting: Error publishing release on github" && exit 1
else
echo "No release published on github!"
fi
}
function todo_notification {
echo "Release procedure was successful, but you have to do some steps manually:"
echo "- Update the wiki at https://github.com/ether/etherpad/wiki"
echo "- Create a pull request on github to merge the master branch back to develop"
echo "- Announce the new release on the mailing list, blog.etherpad.org and Twitter"
}
# Call functions
check_api_token
modify_files
create_release_branch
merge_release_branch
create_builds
push_builds
merge_web_branch
publish_release
todo_notification

View file

@ -2,7 +2,6 @@
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
import util from "node:util";
import fs from 'node:fs';
import log4js from 'log4js';
import readline from 'readline';
@ -69,8 +68,7 @@ const unescape = (val: string) => {
if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE');
log('initializing db');
const initDb = await util.promisify(db.init.bind(db));
await initDb(null);
await db.init();
log('done');
log(`Opening ${sqlFile}...`);
@ -86,8 +84,7 @@ const unescape = (val: string) => {
value = value.substring(0, value.length - 2);
console.log(`key: ${key} val: ${value}`);
console.log(`unval: ${unescape(value)}`);
// @ts-ignore
db.set(key, unescape(value), null);
await db.set(key, unescape(value));
keyNo++;
if (keyNo % 1000 === 0) log(` ${keyNo}`);
}
@ -96,9 +93,7 @@ const unescape = (val: string) => {
process.stdout.write('done. waiting for db to finish transaction. ' +
'depended on dbms this may take some time..\n');
const closeDB = util.promisify(db.close.bind(db));
// @ts-ignore
await closeDB(null);
await db.close();
log(`finished, imported ${keyNo} keys.`);
process.exit(0)
})();

View file

@ -74,10 +74,20 @@ const handleSync = async ()=>{
}
}
handleSync().then(()=>{
handleSync().then(async ()=>{
// Closing flushes any buffered writes to the target DB and clears
// ueberdb2's keep-alive timer (added in 6.1.x). Without this the migrated
// data may not be fully persisted and the process would hang forever
// instead of exiting once the sync is done.
await ueberdb2.close()
await ueberdb1.close()
console.log("Done syncing dbs")
}).catch(e=>{
process.exit(0)
}).catch(async e=>{
console.log(`Error syncing db ${e}`)
await ueberdb2.close().catch(()=>{})
await ueberdb1.close().catch(()=>{})
process.exit(1)
})

View file

@ -35,26 +35,16 @@ process.on('unhandledRejection', (err) => { throw err; });
const keys = await dirty.findKeys('*', '')
console.log(`Found ${keys.length} records, processing now.`);
const p: Promise<void>[] = [];
let numWritten = 0;
for (const key of keys) {
let value = await dirty.get(key);
let bcb, wcb;
p.push(new Promise((resolve, reject) => {
bcb = (err:any) => { if (err != null) return reject(err); };
wcb = (err:any) => {
if (err != null) return reject(err);
if (++numWritten % 100 === 0) console.log(`Wrote record ${numWritten} of ${length}`);
resolve();
};
}));
db.set(key, value, bcb, wcb);
const value = await dirty.get(key);
await db.set(key, value);
if (++numWritten % 100 === 0) console.log(`Wrote record ${numWritten} of ${keys.length}`);
}
await Promise.all(p);
console.log(`Wrote all ${numWritten} records`);
await db.close(null);
await dirty.close(null);
await db.close();
await dirty.close();
console.log('Finished.');
process.exit(0)
})();

View file

@ -1,6 +1,6 @@
{
"name": "bin",
"version": "3.2.0",
"version": "3.3.2",
"description": "",
"main": "checkAllPads.js",
"directories": {
@ -9,12 +9,12 @@
"dependencies": {
"ep_etherpad-lite": "workspace:../src",
"log4js": "^6.9.1",
"semver": "^7.8.1",
"tsx": "^4.22.3",
"ueberdb2": "^6.1.2"
"semver": "^7.8.5",
"tsx": "^4.23.0",
"ueberdb2": "6.1.16"
},
"devDependencies": {
"@types/node": "^25.9.1",
"@types/node": "^26.1.1",
"@types/semver": "^7.7.1",
"typescript": "^6.0.3"
},
@ -24,6 +24,7 @@
"checkAllPads": "node --import tsx checkAllPads.ts",
"compactPad": "node --import tsx compactPad.ts",
"compactAllPads": "node --import tsx compactAllPads.ts",
"compactStalePads": "node --import tsx compactStalePads.ts",
"createUserSession": "node --import tsx createUserSession.ts",
"deletePad": "node --import tsx deletePad.ts",
"repairPad": "node --import tsx repairPad.ts",

View file

@ -26,13 +26,19 @@ export default defineConfig({
text: 'About',
items: [
{ text: 'Docker', link: '/docker.md' },
{ text: 'Configuration', link: '/configuration.md' },
{ text: 'Deployment', link: '/deployment.md' },
{ text: 'Database', link: '/database.md' },
{ text: 'Localization', link: '/localization.md' },
{ text: 'Cookies', link: '/cookies.md' },
{ text: 'Plugins', link: '/plugins.md' },
{ text: 'Stats', link: '/stats.md' },
{text: 'Skins', link: '/skins.md' },
{ text: 'Accessibility', link: '/accessibility.md' },
{text: 'Demo', link: '/demo.md' },
{text: 'CLI', link: '/cli.md'},
{ text: 'Development', link: '/development.md' },
{ text: 'FAQ', link: '/faq.md' },
]
},
{

91
doc/accessibility.md Normal file
View file

@ -0,0 +1,91 @@
# Accessibility
Etherpad aims to be usable by everyone, including people who rely on a
keyboard, a screen reader, or other assistive technology. The editor follows
common conventions so that selecting, formatting, and navigating text works the
way you would expect in other applications, and the toolbar can be reached and
operated without a mouse.
If you find a feature that is not accessible, please let us know by opening an
issue so it can be improved.
## Keyboard shortcuts
The following shortcuts are built into the editor. On macOS use the Command
(`Cmd`) key wherever `Ctrl` is listed.
::: tip
Most shortcuts can be individually enabled or disabled through the
`padShortcutEnabled` settings, so a deployment may have customised which of
these are active.
:::
### Editor
| Action | Shortcut |
| --- | --- |
| Bold | `Ctrl` + `B` |
| Italic | `Ctrl` + `I` |
| Underline | `Ctrl` + `U` |
| Strikethrough | `Ctrl` + `5` |
| Ordered (numbered) list | `Ctrl` + `Shift` + `N` or `Ctrl` + `Shift` + `1` |
| Unordered (bulleted) list | `Ctrl` + `Shift` + `L` |
| Indent line or selection | `Tab` |
| Outdent line or selection | `Shift` + `Tab` |
| Undo | `Ctrl` + `Z` |
| Redo | `Ctrl` + `Y` or `Ctrl` + `Shift` + `Z` |
| Save a named revision | `Ctrl` + `S` |
| Duplicate the current line(s) | `Ctrl` + `Shift` + `D` |
| Delete the current line(s) | `Ctrl` + `Shift` + `K` |
| Clear authorship colors on the pad or selection | `Ctrl` + `Shift` + `C` |
| Show the authors of the current line | `Ctrl` + `Shift` + `2` |
| Focus the toolbar (see below) | `Alt` + `F9` |
| Focus the chat input | `Alt` + `C` |
Text selection, cut (`Ctrl` + `X`), copy (`Ctrl` + `C`), paste
(`Ctrl` + `V`), and the arrow keys behave as they do in any standard text
editor.
### Timeslider
The timeslider (revision history) provides its own shortcuts:
| Action | Shortcut |
| --- | --- |
| Play / pause history playback | `Space` |
| Step back one revision | `Left Arrow` |
| Step forward one revision | `Right Arrow` |
| Jump back to the previous starred revision | `Shift` + `Left Arrow` |
| Jump forward to the next starred revision | `Shift` + `Right Arrow` |
## Toolbar navigation
The toolbar holds the formatting controls (bold, italic, lists, and so on) and
can be reached and operated entirely from the keyboard:
* Press `Alt` + `F9` from the editor to move focus to the first button in the
toolbar.
* Use the `Left Arrow` and `Right Arrow` keys to move between buttons. `Tab`
also moves to the next focusable control.
* Press `Enter` to activate the focused button.
* Press `Alt` + `F9` again, or `Escape`, to return focus to the pad.
Pressing `Escape` while a toolbar dropdown (such as the settings or color
picker) is open closes that dropdown first.
## Screen readers
Etherpad provides as much screen reader support as possible. Support quality
varies between platforms and browsers, so the following combinations are
recommended:
* On Windows, Firefox with [NVDA](https://www.nvaccess.org/) currently gives the
best experience.
To reduce verbose feedback while typing collaboratively in NVDA, open the
keyboard settings (`NVDA` + `Ctrl` + `K`) and turn off **Speak typed characters**
and **Speak typed words**.
Support in other screen readers and browsers (for example Orca on Linux, or
Chrome) is more limited. Contributions to improve coverage on these platforms
are very welcome.

View file

@ -29,13 +29,22 @@ In `settings.json`:
"requireSignature": false,
"trustedKeysPath": null
},
"adminEmail": null
"adminEmail": null,
// SMTP transport for the admin notification emails. host=null keeps
// log-only behaviour ("(would send email)"); set host+from to deliver.
"mail": {
"host": null,
"port": 587,
"secure": false,
"from": null,
"auth": null
}
}
```
| Setting | Default | Notes |
| --- | --- | --- |
| `updates.tier` | `"notify"` | One of `"off"`, `"notify"`, `"manual"`, `"auto"`, `"autonomous"`. Higher tiers are silently downgraded if the install method does not allow them. PR 1 only honors `"notify"` and `"off"`. |
| `updates.tier` | `"notify"` | One of `"off"`, `"notify"`, `"manual"`, `"auto"`, `"autonomous"`. All tiers are implemented. Higher tiers are silently downgraded if the install method does not allow them (only `"git"` installs can run the write tiers `manual` / `auto` / `autonomous`). |
| `updates.source` | `"github"` | Reserved for future alternative sources. Only `"github"` is implemented. |
| `updates.channel` | `"stable"` | Reserved. Stable releases only. |
| `updates.installMethod` | `"auto"` | One of `"auto"`, `"git"`, `"docker"`, `"npm"`, `"managed"`. Auto-detects via filesystem heuristics. Set explicitly to override. |
@ -49,6 +58,11 @@ In `settings.json`:
| `updates.requireSignature` | `false` | When `true`, refuse updates whose tag is not signed by a trusted key. Verification is done via `git verify-tag <tag>` against the user's GPG keyring. Default `false` because Etherpad's release process does not yet sign tags consistently — turning the check on by default would block every Tier 2 update. Set `true` if you run your own builds or have imported a fork's keys. |
| `updates.trustedKeysPath` | `null` | Override the keyring location passed to `git verify-tag` via the `$GNUPGHOME` env var. Useful when the trusted keys live in a dedicated keyring outside the Etherpad user's home. Only meaningful when `requireSignature: true`. |
| `adminEmail` | `null` | Top-level. Contact for admin notifications. Setting it enables the email nudges below. |
| `mail.host` | `null` | Top-level SMTP host. **`null` keeps log-only behaviour** — notifications are logged as `(would send email)` and never delivered. Set a host (and `mail.from`) to deliver over SMTP via nodemailer. The `nodemailer` dependency is lazy-loaded, so installs that leave `mail.host` unset pay no runtime cost. |
| `mail.port` | `587` | SMTP port. |
| `mail.secure` | `false` | `true` for an implicit-TLS connection (typically port 465); `false` uses STARTTLS upgrade when offered. |
| `mail.from` | `null` | Envelope/From address. **Required for delivery** — if `mail.from` is unset (even with a host) the updater falls back to log-only `(would send email)`. |
| `mail.auth` | `null` | SMTP credentials object `{ "user": "...", "pass": "..." }`, passed through to nodemailer. Leave `null` for unauthenticated relays. |
## What "outdated" means
@ -56,14 +70,26 @@ In `settings.json`:
## Email cadence (when `adminEmail` is set)
These are the "nudge" emails sent by the periodic checker when the instance is behind:
| Trigger | First send | Repeat |
| --- | --- | --- |
| Outdated (minor or more behind) detected | Immediate | Monthly while still outdated |
| Outdated (minor or more behind) detected | Immediate | Every 30 days while still outdated (`SEVERE_INTERVAL`) |
| Up to date | No email | — |
The write tiers also email about **apply outcomes** so admins learn about failures without watching the UI:
| Outcome | When | Dedupe |
| --- | --- | --- |
| `update-preflight-failed` | An auto/autonomous apply was blocked at preflight (e.g. `node-engine-mismatch`, dirty tree, low disk). Subject: *Auto-update to `<tag>` blocked at preflight*. | Deduped on `<outcome>:<targetTag>` — one email per outcome per target tag. |
| `update-rolled-back` | An apply failed mid-flow and Etherpad auto-recovered to the previous version. Subject: *Auto-update to `<tag>` rolled back*. | Deduped on `<outcome>:<targetTag>`. |
| `update-rollback-failed` | **Terminal.** The apply failed *and* the rollback failed — manual intervention required. Subject: *Auto-update FAILED and could not be rolled back — manual intervention required*. | **Always sends**, bypassing dedupe, because the admin must learn about it even if a transient failure shared the same key. |
A different outcome or a different target tag resets the dedupe key and fires a fresh email. Manual (Tier 2) failures surface in the admin UI banner; the outcome emails are tied to the auto/autonomous flows.
If `adminEmail` is unset, the updater never sends mail. The admin UI banner and the pad-side notice still work without it.
PR 1 ships the cadence machinery but does not yet wire a real SMTP transport — emails are logged with `(would send email)` until a future PR adds the transport. The dedupe state still advances correctly so admins are not bombarded once SMTP is wired.
SMTP delivery is wired via [nodemailer](https://nodemailer.com/) (lazy-loaded). When `mail.host` and `mail.from` are both set, emails are delivered over SMTP. When either is unset the updater falls back to logging each message as `(would send email)` — the dedupe state still advances correctly, so admins are not bombarded once SMTP is configured. An SMTP send failure is caught and logged (`email send failed: …`) and never disrupts the updater state machine.
## Pad-side notice
@ -76,7 +102,9 @@ The notice auto-fades after 8 seconds and can be dismissed immediately. The publ
## Disabling everything
Set `updates.tier` to `"off"`. No HTTP request will leave the instance and no banner or badge will render.
Set `updates.tier` to `"off"`. The self-updater goes silent — no request to the GitHub Releases API leaves the instance and no banner or badge renders. Note this does **not** cover the separate legacy version check in `UpdateCheck.ts`, which still fetches `${updateServer}/info.json` until you also set `privacy.updateCheck` to `false` (see [PRIVACY.md](https://github.com/ether/etherpad/blob/develop/PRIVACY.md)).
On Docker / air-gapped installs you can do both without editing `settings.json` inside the image by setting `UPDATES_TIER=off` **and** `PRIVACY_UPDATE_CHECK=false` (add `PRIVACY_PLUGIN_CATALOG=false` to also disable the admin plugin browser's catalogue fetch). See the [Updates & privacy](../docker.md#updates--privacy-offline--air-gapped) table in the Docker docs for the full set of environment variables.
## Privacy
@ -93,7 +121,7 @@ The version check sends no telemetry. Etherpad fetches the public GitHub Release
Set the value explicitly if the heuristics get it wrong (e.g., a docker container that bind-mounts a writable git checkout).
In PR 1 (notify only) the install method does not change behavior — every install method gets the banner. From PR 2 onward the install method gates whether the manual-click and automatic tiers can run; only `"git"` is initially supported for write tiers.
Every install method gets the Tier 1 banner. The install method gates whether the write tiers (manual click, auto, autonomous) can run: only `"git"` installs are supported for the write tiers — other methods are silently downgraded to notify.
## Tier 2 — manual click
@ -110,7 +138,7 @@ Etherpad applies an update by **exiting with code 75** so a process supervisor r
### What clicking "Apply update" does
1. **Lock acquire**`var/update.lock` (PID-based, stale locks reaped automatically).
2. **Pre-flight checks** — install method writable, working tree clean, free disk ≥ `diskSpaceMinMB`, `pnpm` on `PATH`, target tag exists at the configured remote, signature verifies (if `requireSignature: true`). On failure, state goes to `preflight-failed` with a typed reason; the admin sees a banner and clicks **Acknowledge** to clear it. No filesystem mutation has happened — nothing to roll back.
2. **Pre-flight checks** — install method writable, working tree clean, free disk ≥ `diskSpaceMinMB`, `pnpm` on `PATH`, no lock held, target tag exists at the configured remote, signature verifies (if `requireSignature: true`), and the target's Node engine matches the running Node. The Node-engine check runs *after* signature verification (so the `engines.node` range comes from a trusted tag): Etherpad reads `engines.node` from the target tag's `package.json` via `git show <tag>:package.json` and refuses the update via `semver.satisfies` if the running Node does not satisfy it. On failure, state goes to `preflight-failed` with a typed reason; the admin sees a banner and clicks **Acknowledge** to clear it. No filesystem mutation has happened — nothing to roll back.
3. **Drain**`drainSeconds` window during which T-60 / T-30 / T-10 announcements broadcast to every connected pad and new socket connections are refused. Click **Cancel** during this window to abort cleanly.
4. **Execute**`git fetch --tags origin`, `git checkout <tag>`, `pnpm install --frozen-lockfile`, `pnpm run build:ui`. Output streams to `var/log/update.log` (rotated 10 MB × 5).
5. **Exit 75** — the supervisor restarts on the new version.
@ -121,6 +149,7 @@ Etherpad applies an update by **exiting with code 75** so a process supervisor r
| What went wrong | Resulting state | Admin action |
| --- | --- | --- |
| Pre-flight check fails | `preflight-failed` | Click **Acknowledge** after fixing the underlying issue (free up disk, clean working tree, etc.). |
| Target tag requires a newer (or different) Node than the one running | `preflight-failed` (reason `node-engine-mismatch`) | Fails cleanly at preflight with a detail like *"target requires Node >=X, running Y"*. No drain, no `git checkout`, no restart, nothing to roll back — the install is untouched. Upgrade Node to a version that satisfies the target's `engines.node`, then **Acknowledge** and retry. |
| `git fetch` / `git checkout` fails mid-flow | `rolled-back` | Informational. The working tree is back where it started; click **Acknowledge** to clear. |
| `pnpm install` or `pnpm run build:ui` fails | `rolled-back` | Same as above. The lockfile and SHA are restored. |
| `/health` doesn't come up within `rollbackHealthCheckSeconds` | `rolled-back` | Same — RollbackHandler restores the previous SHA + lockfile and exits 75 again. |
@ -181,7 +210,7 @@ A single `grace-start` notification fires per scheduled tag:
> [Etherpad] Auto-update scheduled for 2.7.2
with the `scheduledFor` timestamp. Etherpad core does not yet wire SMTP; the message logs as `(would send email)` until a future PR adds a transport. Cadence and dedupe still update correctly.
with the `scheduledFor` timestamp. Delivery follows the same SMTP path as every other notification: when `mail.host` and `mail.from` are set the message is sent via nodemailer, otherwise it logs as `(would send email)`. Cadence and dedupe update correctly either way.
The right way to give docker admins an in-product Apply button is to delegate to the orchestrator rather than mutate the container. Two patterns to consider in a follow-up PR:

View file

@ -1,7 +1,7 @@
# Changeset Library
The [changeset
library](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/Changeset.ts)
library](https://github.com/ether/etherpad/blob/develop/src/static/js/Changeset.ts)
provides tools to create, read, and apply changesets.
## Changeset
@ -21,6 +21,42 @@ A transmitted changeset looks like this:
'Z:z>1|2=m=b*0|1+1$\n'
```
### Reading a changeset
`unpack()` splits a changeset string into its parts:
```javascript
const unpacked = Changeset.unpack('Z:z>1|2=m=b*0|1+1$\n');
// { oldLen: 35, newLen: 36, ops: '|2=m=b*0|1+1', charBank: '\n' }
```
`oldLen` is the document length before the change and `newLen` the length after.
`ops` is the list of operations, and `charBank` holds the characters inserted by
those operations.
Iterate the operations with `deserializeOps()`, which yields one `Op` at a time:
```javascript
for (const op of Changeset.deserializeOps(unpacked.ops)) {
console.log(op);
}
// Op { opcode: '=', chars: 22, lines: 2, attribs: '' }
// Op { opcode: '=', chars: 11, lines: 0, attribs: '' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '*0' }
```
There are three kinds of operation, each applied starting from the current
position in the text:
- `=` keeps text (it may still change the text's attributes, e.g. make it bold).
- `-` removes text.
- `+` inserts text (taking the characters from the changeset's `charBank`).
`opcode` is the operation type; `chars` and `lines` are how much text it covers;
and `attribs` are the attributes applied, written as `*` references into the
pad's attribute pool. In the example above the final op inserts one character
(the newline from `charBank`) carrying attribute `*0`.
## Attribute Pool
```javascript
@ -36,6 +72,59 @@ are used many times.
There is one attribute pool per pad, and it includes every current and
historical attribute used in the pad.
A pool can be serialized to and from a plain object with `toJsonable()` and
`fromJsonable()`:
```javascript
const pool = new AttributePool();
pool.fromJsonable({
numToAttrib: {
0: ['author', 'a.kVnWeomPADAT2pn9'],
1: ['bold', 'true'],
2: ['italic', 'true'],
},
nextNum: 3,
});
pool.getAttrib(1); // [ 'bold', 'true' ]
pool.getAttribKey(1); // 'bold'
pool.getAttribValue(1); // 'true'
```
Each attribute is a `[key, value]` pair — `['bold', 'true']`, or
`['author', '<authorId>']`. A character can carry several attributes (bold *and*
italic), but only one value per key (so it cannot belong to two authors).
## Attributed text (atext)
A pad's content is stored as *attributed text* (`atext`): the plain text plus an
attribute string describing which attributes apply to each span.
```javascript
const atext = {
text: 'bold text\nitalic text\nnormal text\n\n',
attribs: '*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2',
};
```
The attribute string is a sequence of `+` operations — the same encoding used by
changesets — which you can read with `deserializeOps()`:
```javascript
for (const op of Changeset.deserializeOps(atext.attribs)) {
console.log(op);
}
// Op { opcode: '+', chars: 9, lines: 0, attribs: '*0*1' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '*0' }
// Op { opcode: '+', chars: 11, lines: 0, attribs: '*0*1*2' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '' }
// Op { opcode: '+', chars: 11, lines: 0, attribs: '*0' }
// Op { opcode: '+', chars: 2, lines: 2, attribs: '' }
```
Read against the pool above, the first nine characters (`bold text`) carry
attributes `*0*1` (author + bold), the following newline carries `*0`, and so on.
## Further Reading
Detailed information about the changesets & Easysync protocol:

View file

@ -465,6 +465,20 @@ also use this to handle existing types.
`collab_client.js` has a pretty extensive list of message types, if you want to
take a look.
## handleClientTimesliderMessage_`name`
Called from: `src/static/js/broadcast.ts`
Things in context:
1. payload - the data that got sent with the message (use it for custom message
content)
This is the timeslider analog of `handleClientMessage_name`. It gets called
every time the timeslider receives a message of type `name`. Use it to handle
custom message types (or react to existing ones) while a user is viewing the
timeslider rather than editing the pad.
## aceStartLineAndCharForPoint-aceEndLineAndCharForPoint
Called from: src/static/js/ace2_inner.js
@ -496,6 +510,34 @@ Things in context:
This hook is provided to allow a plugin to handle key events.
The return value should be true if you have handled the event.
## acePaste
Called from: `src/static/js/ace2_inner.ts`
Things in context:
1. editorInfo - information about the user who is making the change
2. rep - information about where the change is being made
3. documentAttributeManager - information about attributes in the document
4. e - the fired paste event
This hook is called when content is pasted into the editor, before Etherpad
processes the pasted content. Use it to inspect or react to paste events.
## aceDrop
Called from: `src/static/js/ace2_inner.ts`
Things in context:
1. editorInfo - information about the user who is making the change
2. rep - information about where the change is being made
3. documentAttributeManager - information about attributes in the document
4. e - the fired drop event
This hook is called when content is dropped into the editor via drag-and-drop.
Use it to inspect or react to drop events.
## collectContentLineText
Called from: `src/static/js/contentcollector.js`

View file

@ -856,6 +856,39 @@ exports.exportHTMLAdditionalContent = async (hookName, {padId}) => {
};
```
## exportHTMLSend
Called from: `src/node/handler/ExportHandler.ts`
Things in context:
1. html - the full HTML body of the pad that is about to be sent to the client
as an HTML export.
This hook is invoked via `aCallFirst` for HTML exports, just before the HTML is
sent in the response. It lets a plugin rewrite or replace the exported HTML
body. Return the modified HTML string; if a non-empty value is returned, it
replaces the HTML that would otherwise be sent.
## exportConvert
Called from: `src/node/handler/ExportHandler.ts`
Things in context:
1. srcFile - the path to the source file (the intermediate file Etherpad has
produced) that needs to be converted.
2. destFile - the path to the destination file Etherpad expects the converted
output to be written to.
3. req - the express request object.
4. res - the express response object.
This hook is invoked via `aCallAll` for non-HTML export formats. It lets a
plugin handle the export format conversion itself instead of letting Etherpad
fall back to its bundled LibreOffice converter. If any plugin returns a value
(making the hook result non-empty), Etherpad assumes the conversion was handled
by the plugin and skips the built-in converter.
## stylesForExport
Called from: `src/node/utils/ExportHtml.js`
@ -1101,3 +1134,66 @@ Context properties:
value with the user's actual author ID before this hook runs).
* `padId`: The pad's real (not read-only) identifier.
* `pad`: The pad's Pad object.
## ccRegisterBlockElements
Called from: `src/node/handler/ImportHandler.ts`,
`src/node/utils/ImportEtherpad.ts`, and `src/static/js/contentcollector.ts`
Things in context: None
This is the **server-side companion** to the client-only
`aceRegisterBlockElements` hook (see the client-side hooks documentation), and
it is the half that plugin authors commonly forget to implement. Registering a
block element only on the client means imports and server-side content
collection do not treat your element as block-level.
The return value of this hook should be an array of tag names. Those tags are
added to the set of block-level elements that the content collector and the
import path (`.etherpad` and HTML/document imports) recognise, so the
collected/imported content is segmented into the correct lines. Plugins that
declare block elements via `aceRegisterBlockElements` should declare the same
elements here too.
```js
exports.ccRegisterBlockElements = () => ['pre', 'blockquote'];
```
## createServer
Called from: `src/node/server.ts`
Things in context: None
Invoked via `aCallAll` once during Etherpad startup, after the HTTP server has
been created. Use it to run any plugin initialisation that needs to happen when
the server comes up.
## restartServer
Called from: `src/node/hooks/express/adminsettings.ts` (and the plugin
installer)
Things in context: None
Invoked via `aCallAll` when the server is restarted from the admin interface
(for example after settings are saved or a plugin is installed/uninstalled). Use
it to re-initialise plugin state that needs to survive an admin-triggered
restart.
## clientReady
> **Deprecated:** use the `userJoin` hook instead. This hook is fired with an
> awkward context (the raw `CLIENT_READY` message rather than a structured set
> of context properties) and is kept only for backwards compatibility.
Called from: `src/node/handler/PadMessageHandler.ts`
Things in context:
1. message - the raw `CLIENT_READY` message sent by the client as it joins a
pad.
Invoked via `aCallAll` while a client is joining a pad, before the author and
session are fully set up. Because the context is just the raw client message,
new plugins should use `userJoin` instead.

View file

@ -306,6 +306,18 @@ Returns the Author Name of the author
-> can't be deleted cause this would involve scanning all the pads where this author was
#### anonymizeAuthor(authorID)
* API >= 1.3.1
Erases an author's identity across all pads they contributed to (GDPR Article 17, the "right to erasure"). The author's name and external/token mappings are removed and their chat messages are cleared, so the author can no longer be re-identified from pad data.
This endpoint is **disabled by default** and is gated on `gdprAuthorErasure.enabled = true` in `settings.json`. If GDPR author erasure is not enabled, the call returns an error and performs no changes. See [doc/privacy.md](../privacy.md) for the privacy/data-retention context.
*Example returns:*
* `{code: 0, message:"ok", data: {affectedPads: 3, removedTokenMappings: 1, removedExternalMappings: 1, clearedChatMessages: 7}}`
* `{code: 1, message:"anonymizeAuthor is disabled — set gdprAuthorErasure.enabled = true in settings.json to enable GDPR Art. 17 erasure", data: null}`
* `{code: 1, message:"authorID is required", data: null}`
### Session
Sessions can be created between a group and an author. This allows an author to access more than one group. The sessionID will be set as a cookie to the client and is valid until a certain date. The session cookie can also contain multiple comma-separated sessionIDs, allowing a user to edit pads in different groups at the same time. Only users with a valid session for this group, can access group pads. You can create a session after you authenticated the user at your web application, to give them access to the pads. You should save the sessionID of this session and delete it after the user logged out.
@ -609,7 +621,7 @@ token is ignored.
* `{code: 1, message:"invalid deletionToken", data: null}`
#### copyPad(sourceID, destinationID[, force=false])
* API >= 1.2.8
* API >= 1.2.9
copies a pad with full history and chat. If force is true and the destination pad exists, it will be overwritten.
@ -629,7 +641,7 @@ Note that all the revisions will be lost! In most of the cases one should use `c
* `{code: 1, message:"padID does not exist", data: null}`
#### movePad(sourceID, destinationID[, force=false])
* API >= 1.2.8
* API >= 1.2.9
moves a pad. If force is true and the destination pad exists, it will be overwritten.
@ -646,7 +658,7 @@ collapses the pad's revision history to reclaim database space (issue #6194). Wr
When `keepRevisions` is omitted (or null), all history is collapsed into a single base revision that reproduces the current pad text — equivalent to a freshly-imported pad. When set to a positive integer N, the pad keeps only its last N revisions.
Pad text and chat are preserved in both modes. Saved-revision bookmarks are cleared. **This operation is destructive — export the pad first via `getEtherpad` if you need a backup.**
Pad text and chat are preserved in both modes. Saved-revision bookmarks are cleared. **This operation is destructive — export the pad first (for example via the `/p/:padID/export/etherpad` export endpoint) if you need a full-history backup.**
*Example returns:*
* `{code: 0, message:"ok", data: {ok: true, mode: "all"}}`
@ -664,10 +676,10 @@ returns the read only link of a pad
* `{code: 0, message:"ok", data: {readOnlyID: "r.s8oes9dhwrvt0zif"}}`
* `{code: 1, message:"padID does not exist", data: null}`
#### getPadID(readOnlyID)
#### getPadID(roID)
* API >= 1.2.10
returns the id of a pad which is assigned to the readOnlyID
returns the id of the pad mapped to the given read-only id. The query parameter is named `roID` (the read-only id returned by `getReadOnlyID`).
*Example returns:*
* `{code: 0, message:"ok", data: {padID: "p.s8oes9dhwrvt0zif"}}`

View file

@ -1,68 +1,151 @@
# CLI
You can find different tools for migrating things, checking your Etherpad health in the bin directory.
One of these is the migrateDB command. It takes two settings.json files and copies data from one source to another one.
In this example we migrate from the old dirty db to the new rustydb engine. So we copy these files to the root of the etherpad-directory.
Etherpad ships a set of operator tools in the `bin/` directory for migrating
data, reclaiming database space, repairing damaged pads, managing sessions and
managing plugins. They are TypeScript scripts run through `tsx`, and each one
is registered as a pnpm script in `bin/package.json`. Invoke them from the
Etherpad root with:
```
pnpm run --filter bin <script> [args]
```
For example `pnpm run --filter bin checkPad my-pad`. Running the `.ts` files
directly with `node bin/foo.js` will **not** work — there are no compiled `.js`
files and the scripts need the `tsx` loader.
## Running vs. stopped
Some tools talk to a **running** Etherpad over its HTTP API (they read the API
key from `APIKEY.txt` and connect to `ip`/`port` from `settings.json`). Others
open the database **directly** and must be run while Etherpad is **stopped**,
otherwise you risk a database lock or a corrupt write. Each tool below is
labelled accordingly.
## Database migration (`migrateDB`)
`migrateDB` copies every record from one database to another. It takes two
settings files — `--file1` is the **source**, `--file2` is the **target**
each describing a database with a `dbType` and `dbSettings`. Both paths are
resolved relative to the Etherpad root.
In this example we migrate from the old `dirty` db to the new `rustydb` engine.
Create a source descriptor `source.json` in the Etherpad root:
````json
{
"dbType": "dirty",
"dbSettings": {
"filename": "./var/dirty.db"
}
}
````
and a target descriptor `target.json`:
````json
{
"dbType": "rustydb",
"dbSettings": {
"filename": "./var/rusty.db"
}
}
````
Then run:
```
pnpm run --filter bin migrateDB --file1 source.json --file2 target.json
```
````json
{
"dbType": "rustydb",
"dbSettings": {
"filename": "./var/rusty2.db"
}
}
````
After that we need to move the data from dirty to rustydb.
Therefore, we call `pnpm run --filter bin migrateDB --file1 test1.json --file2 test2.json` with these two files in our root directories. After some time the data should be copied over to the new database.
After some time the data is copied over to the new database. Run this with
Etherpad **stopped**.
## Pad compaction
Long-lived pads with heavy edit history accumulate revisions in the database. Three CLIs reclaim that space, in increasing scope:
Long-lived pads with heavy edit history accumulate revisions in the database.
Three CLIs reclaim that space, in increasing scope. All of them drive a
**running** Etherpad over the `compactPad` HTTP API.
| Tool | Targets | When to use |
| --- | --- | --- |
| `bin/compactPad.js <padID>` | one pad | you know which pad is fat |
| `bin/compactAllPads.js` | every pad | bulk reclaim across the whole instance |
| `bin/compactStalePads.js --older-than N` | pads not edited in N days | reclaim the cold tail without touching pads still in active use |
| `pnpm run --filter bin compactPad <padID>` | one pad | you know which pad is fat |
| `pnpm run --filter bin compactAllPads` | every pad | bulk reclaim across the whole instance |
| `pnpm run --filter bin compactStalePads --older-than N` | pads not edited in N days | reclaim the cold tail without touching pads still in active use |
All three are gated on `cleanup.enabled = true` in `settings.json` and are **destructive**: history is collapsed (or trimmed). Export anything you can't afford to lose with `getEtherpad` first.
All three are gated on `cleanup.enabled = true` in `settings.json` and are
**destructive**: history is collapsed (or trimmed). Export anything you can't
afford to lose with the `getEtherpad` API first.
Common flags:
- `--keep N` — retain the last N revisions instead of collapsing all history.
- `--dry-run` — list pads and revision counts without writing.
- `--dry-run` — list pads and revision counts without writing (`compactAllPads`
and `compactStalePads` only).
- `--older-than N` — (`compactStalePads` only, **required**) only consider pads
not edited in the last N days.
### Examples
````
# Compact a specific pad, collapsing all history.
node bin/compactPad.js my-pad
pnpm run --filter bin compactPad my-pad
# Keep only the last 50 revisions of one pad.
node bin/compactPad.js my-pad --keep 50
pnpm run --filter bin compactPad my-pad --keep 50
# Compact every pad on the instance (per-pad failures don't stop the run).
node bin/compactAllPads.js
node bin/compactAllPads.js --dry-run
pnpm run --filter bin compactAllPads
pnpm run --filter bin compactAllPads --dry-run
# Compact only pads not edited in the last 90 days, keeping the last 50 revisions.
node bin/compactStalePads.js --older-than 90 --keep 50
node bin/compactStalePads.js --older-than 90 --dry-run
pnpm run --filter bin compactStalePads --older-than 90 --keep 50
pnpm run --filter bin compactStalePads --older-than 90 --dry-run
````
`bin/compactStalePads.js` is the right tool for periodic operator runs on long-lived instances — hot pads that users are still navigating in timeslider stay untouched, and only the cold tail is rewritten. Per-pad failures (including a `getLastEdited` fault) are counted but do not abort the bulk run; the exit code reflects whether anything failed.
`compactStalePads` is the right tool for periodic operator runs on long-lived
instances — hot pads that users are still navigating in timeslider stay
untouched (staleness is even re-checked right before each compaction), and only
the cold tail is rewritten. Per-pad failures (including a `getLastEdited` fault)
are counted but do not abort the bulk run; the exit code reflects whether
anything failed.
See the `compactPad` HTTP API in `doc/api/http_api.md` for the same primitive over the wire (issues #6194, #7642).
See the `compactPad` HTTP API in `doc/api/http_api.md` for the same primitive
over the wire (issues #6194, #7642).
## Pad maintenance and debugging
| Tool | Purpose | Args | Etherpad |
| --- | --- | --- | --- |
| `pnpm run --filter bin checkPad <padID>` | Check one pad's revisions for data corruption. | `<padID>` | stopped |
| `pnpm run --filter bin checkAllPads` | Check every pad on the instance for data corruption. | none | stopped |
| `pnpm run --filter bin repairPad <padID>` | Repair a pad by extracting all of its data, deleting it and re-inserting it. | `<padID>` | stopped (the script refuses to be useful otherwise) |
| `pnpm run --filter bin rebuildPad <padID> <rev> [newPadID]` | Rebuild a damaged pad into a new pad at a known-good revision. The new pad defaults to `<padID>-rebuilt` and must not already exist. | `<padID> <rev> [newPadID]` | stopped |
| `node --import tsx extractPadData.ts <padID>` | Export one pad's data to a `<padID>.db` dirtyDB file so a bug can be reproduced in a dev environment. (No pnpm alias — run with the `tsx` loader directly from `bin/`.) | `<padID>` | stopped |
`checkPad`/`checkAllPads` report corruption but do not modify anything;
`repairPad` and `rebuildPad` write. As always, back up first.
## Database tools
| Tool | Purpose | Args | Etherpad |
| --- | --- | --- | --- |
| `pnpm run --filter bin migrateDB --file1 <src.json> --file2 <dst.json>` | Copy all records from a source database to a target database (see above). | `--file1 <src.json> --file2 <dst.json>` | stopped |
| `pnpm run --filter bin migrateDirtyDBtoRealDB` | One-shot migration of `var/dirty.db` into the real database configured in `settings.json`. Back up `dirty.db` first; may need more memory (e.g. `node --max-old-space-size=4096`). | none (reads target db from `settings.json`) | stopped |
| `pnpm run --filter bin importSqlFile <sqlFile>` | Import a SQL dump (rows of `REPLACE INTO store VALUES (...)`) into the configured database. | `<sqlFile>` | stopped |
## Session and pad management
| Tool | Purpose | Args | Etherpad |
| --- | --- | --- | --- |
| `pnpm run --filter bin deletePad <padID>` | Delete a single pad. | `<padID>` | running |
| `pnpm run --filter bin deleteAllGroupSessions` | Delete all group sessions (useful when a misconfiguration has wedged group access). | none | running |
| `pnpm run --filter bin createUserSession` | Create a throwaway group, pad, author and session, printing a `sessionID` you can set as a cookie — handy for debugging session-based configs. | none | running |
## Plugin tools
| Tool | Purpose | Args | Etherpad |
| --- | --- | --- | --- |
| `pnpm run --filter bin plugins <action> [names…]` | Manage installed plugins. Actions: `i`/`install`, `rm`/`remove`, `ls`/`list`, `up`/`update`. Install also accepts `--path <dir>` and `--github <repo>` sources. | `<action> [names…]` | stopped |
| `pnpm run --filter bin checkPlugin <ep_name> [autofix\|autocommit\|autopush]` | Lint a plugin checkout (a sibling `../ep_name` directory) against the plugin conventions; optional modes auto-fix, commit, or commit+push+publish (the last is dangerous). | `<ep_name> [mode]` | n/a |
| `pnpm run --filter bin stalePlugins` | List plugins in the registry not updated in over two years, with maintainer email. Requires `privacy.pluginCatalog` enabled. | none | n/a |

146
doc/configuration.md Normal file
View file

@ -0,0 +1,146 @@
# Configuration
This page explains how Etherpad is configured and documents the
reverse-proxy and subpath behaviour in detail. It is **not** an exhaustive list
of every setting — for that, see the fully-commented
[`settings.json.template`](https://github.com/ether/etherpad/blob/develop/settings.json.template),
which is the authoritative reference.
## Where settings live
Etherpad reads its configuration from `settings.json` in the installation root.
A new install copies `settings.json.template` to `settings.json` on first run.
* **Override the file location** by passing the `-s` / `--settings` flag to
the launcher, e.g. `bin/run.sh -s /etc/etherpad/settings.json`. This lets
you run multiple instances from one installation. (`bin/run.sh` forwards the
flag to `pnpm run prod`, which is the supported entrypoint — there is no
`server.js`; the runtime is `src/node/server.ts`, loaded via `tsx`.)
* **Environment-variable substitution** — any string value may reference an
environment variable using the syntax `"${ENV_VAR}"` or
`"${ENV_VAR:default}"`. The variable name **must** be quoted, even when the
resolved value is a number or boolean. A few rules worth remembering:
* `"${PORT:9001}"` → the value of `PORT`, or `9001` if unset.
* `"${MINIFY:true}"` → the boolean `true`/`false`, not the string.
* `"${UNSET_VAR:null}"``null`; `"${UNSET_VAR:}"` → the empty string.
* Substitution happens at load time, in memory only — env vars never
overwrite `settings.json` on disk.
When running in Docker, almost every setting is wired to an environment
variable in the shipped `settings.json.docker`. See the
[Docker page](/docker.md) for the full env-var list.
## Trusting a reverse proxy
If Etherpad runs behind NGINX, Traefik, HAProxy, a Kubernetes ingress, or any
other reverse proxy, set:
```json
"trustProxy": true
```
This makes Etherpad trust the standard `X-Forwarded-*` headers, so it:
* uses the real client IP (from `X-Forwarded-For`) in logs and rate limits
instead of the proxy's IP;
* respects the forwarded protocol and host, so the `secure` flag is set on
cookies when the proxy terminates TLS (required for `SameSite=None`).
Leave it at the default `false` when Etherpad is reachable directly on a public
IP — otherwise any client could forge these headers.
## Running under a subpath / ingress
Etherpad can be served under a URL-path prefix (for example
`https://example.com/etherpad/`) without recompiling anything. The prefix is
discovered per-request from upstream headers, so the same Etherpad process works
whether it is mounted at the root or under a path.
Three headers are checked, **in this order**; the first non-empty value (after
sanitization) wins:
| Order | Header | Origin | Requires `trustProxy: true`? |
| ----- | ------ | ------ | ---------------------------- |
| 1 | `x-proxy-path` | Etherpad's own convention | No — always honoured |
| 2 | `X-Forwarded-Prefix` | HAProxy / Traefik / Spring | Yes |
| 3 | `X-Ingress-Path` | Kubernetes / Home Assistant ingress | Yes |
`x-proxy-path` is always honoured because an operator must deliberately
configure their proxy to send Etherpad's custom header. The two standard
headers (`X-Forwarded-Prefix`, `X-Ingress-Path`) are honoured **only when
`trustProxy` is `true`**, because otherwise a client on a public IP could forge
them.
Once detected, the prefix is woven into the responses that would otherwise
break under a subpath:
* `manifest.json` (PWA install metadata);
* the social-media meta tags (`og:url` / `og:image`), unless an explicit
`publicURL` is configured;
* the bootstrap script entrypoint and the asset / reconnect links in the pad,
index, and timeslider pages.
### Sanitization
The header value is treated as untrusted input even when read from a trusted
header, because it ends up inside HTML, JS, CSS, and HTTP `Location` headers.
The sanitizer (`src/node/utils/sanitizeProxyPath.ts`):
* strips every character outside `[A-Za-z0-9_./-]`;
* collapses a leading `//+` to a single `/`, so the value can never be read as
a protocol-relative URL;
* prepends `/` if the result doesn't already start with one;
* **rejects** any value containing a `..` path segment (returns empty).
The output is therefore always either empty, or a string that starts with
exactly one `/` and contains only `[A-Za-z0-9_./-]`.
### Example: Traefik
```yaml
http:
middlewares:
etherpad-prefix:
stripPrefix:
prefixes:
- "/etherpad"
etherpad-headers:
headers:
customRequestHeaders:
X-Forwarded-Prefix: "/etherpad"
```
Apply both middlewares to the router and set `trustProxy: true` in
`settings.json`.
### Example: NGINX
```nginx
location /etherpad/ {
proxy_pass http://127.0.0.1:9001/;
proxy_set_header X-Proxy-Path /etherpad;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
Here `X-Proxy-Path` is used, which works regardless of `trustProxy`. Use
`X-Forwarded-Prefix` instead if you prefer the standard header (and set
`trustProxy: true`).
## Self-update, email, database, and metrics
These areas have their own pages:
* **Self-update** and **outbound email** (`adminEmail`, `mail.*` SMTP) —
see [Updates](/admin/updates.md). The corresponding Docker env vars
(`MAIL_HOST`, `MAIL_FROM`, …) are listed on the [Docker page](/docker.md).
* **Database** — choose a backend with `dbType` / `dbSettings`. The
supported drivers and example settings are documented in
[`settings.json.template`](https://github.com/ether/etherpad-lite/blob/develop/settings.json.template),
and the Docker equivalents (`DB_TYPE`, `DB_HOST`, …) are listed on the
[Docker page](/docker.md). The on-disk keyspace layout is described in
[`doc/database.adoc`](https://github.com/ether/etherpad-lite/blob/develop/doc/database.adoc).
* **Metrics** — Etherpad exposes Prometheus-compatible metrics; see
[Stats](/stats.md).

204
doc/database.md Normal file
View file

@ -0,0 +1,204 @@
# Database structure
## Keys and their values
### groups
A list of all existing groups (a JSON object with groupIDs as keys and `1` as values).
### pad:$PADID
Contains all information about pads
- **atext** - the latest attributed text
- **pool** - the attribute pool
- **head** - the number of the latest revision
- **chatHead** - the number of the latest chat entry
- **public** - flag that disables security for this pad
- **passwordHash** - string that contains a salted sha512 sum of this pad's password
### pad:$PADID:revs:$REVNUM
Saves a revision $REVNUM of pad $PADID
- **meta**
- **author** - the autorID of this revision
- **timestamp** - the timestamp of when this revision was created
- **changeset** - the changeset of this revision
### pad:$PADID:chat:$CHATNUM
Saves a chat entry with num $CHATNUM of pad $PADID
- **text** - the text of this chat entry
- **userId** - the authorID of this chat entry
- **time** - the timestamp of this chat entry
### pad2readonly:$PADID
Translates a padID to a readonlyID
### readonly2pad:$READONLYID
Translates a readonlyID to a padID
### token2author:$TOKENID
Translates a token to an authorID
### globalAuthor:$AUTHORID
Information about an author
- **name** - the name of this author as shown in the pad
- **colorID** - the colorID of this author as shown in the pad
### mapper2group:$MAPPER
Maps an external application identifier to an internal group
### mapper2author:$MAPPER
Maps an external application identifier to an internal author
### group:$GROUPID
a group of pads
- **pads** - object with pad names in it, values are 1
### session:$SESSIONID
a session between an author and a group
- **groupID** - the groupID the session belongs too
- **authorID** - the authorID the session belongs too
- **validUntil** - the timestamp until this session is valid
### author2sessions:$AUTHORID
saves the sessions of an author
- **sessionsIDs** - object with sessionIDs in it, values are 1
### group2sessions:$GROUPID
- **sessionsIDs** - object with sessionIDs in it, values are 1
# Connecting to a database backend
Etherpad stores everything in a single key/value table through
[ueberDB](https://www.npmjs.com/package/ueberdb2), so the same data model works
across many backends. The backend is selected with `dbType` in `settings.json`,
and backend-specific connection options go in `dbSettings`.
The default `dirty` backend writes to a local file (`var/dirty.db`) and needs no
setup, which is convenient for development but not recommended for production.
For a production instance, point Etherpad at a real database such as MySQL/MariaDB,
PostgreSQL or Redis. Etherpad creates its own table on first run; you only need
to provision an empty database and a user with access to it.
## MySQL / MariaDB
Create the database and a user, then grant access:
```sql
CREATE DATABASE `etherpad` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE USER 'etherpad'@'localhost' IDENTIFIED BY 'a-secure-password';
GRANT CREATE,ALTER,SELECT,INSERT,UPDATE,DELETE ON `etherpad`.* TO 'etherpad'@'localhost';
```
Then configure `settings.json`:
```json
"dbType": "mysql",
"dbSettings": {
"user": "etherpad",
"host": "localhost",
"port": 3306,
"password": "a-secure-password",
"database": "etherpad",
"charset": "utf8mb4"
}
```
Setting `charset` to `utf8mb4` is strongly recommended so that the full range of
Unicode (including emoji) is stored correctly. To connect over a local socket
instead of TCP, replace `host`/`port` with `"socketPath": "/var/run/mysqld/mysqld.sock"`.
## PostgreSQL
Create the user and a database owned by it:
```sql
CREATE USER etherpad WITH PASSWORD 'a-secure-password';
CREATE DATABASE etherpad OWNER etherpad;
```
Then configure `settings.json`:
```json
"dbType": "postgres",
"dbSettings": {
"user": "etherpad",
"host": "localhost",
"port": 5432,
"password": "a-secure-password",
"database": "etherpad"
}
```
The `dbSettings` object is passed straight to the `node-postgres` connection
pool, so any option it accepts (including a single `"connectionString"`) works.
On Debian/Ubuntu you can use peer authentication over the local socket by
setting `"host": "/var/run/postgresql"` and an empty password, provided the
operating-system user that runs Etherpad matches the PostgreSQL role.
## Redis
Install Redis and make sure it persists data to disk. Configure `settings.json`
with either discrete fields or a single connection URL:
```json
"dbType": "redis",
"dbSettings": {
"host": "localhost",
"port": 6379,
"password": "a-secure-redis-password"
}
```
```json
"dbType": "redis",
"dbSettings": {
"url": "redis://:a-secure-redis-password@localhost:6379"
}
```
## Migrating from MySQL to PostgreSQL
[pgloader](https://pgloader.io/) can copy an existing Etherpad database from
MySQL to PostgreSQL. Stop Etherpad first so the source database is quiescent.
```bash
sudo apt-get install postgresql pgloader
# Create the target role and database
sudo -u postgres createuser etherpad
sudo -u postgres createdb -O etherpad etherpad
# Describe and run the migration
cat > pgloader.load <<'EOF'
LOAD DATABASE
FROM mysql://etherpad:MYSQL_PASSWORD@127.0.0.1/etherpad
INTO postgresql:///etherpad
WITH preserve index names, prefetch rows = 100
ALTER SCHEMA 'etherpad' RENAME TO 'public';
EOF
pgloader --verbose pgloader.load
```
Afterwards set the PostgreSQL user's password and make sure it can read and
write the migrated table:
```sql
ALTER USER etherpad WITH PASSWORD 'a-secure-password';
GRANT pg_read_all_data TO etherpad;
GRANT pg_write_all_data TO etherpad;
```
Then point `settings.json` at PostgreSQL as shown above and start Etherpad.
::: tip
To move data between *any* two backends supported by ueberDB, you can also
use the `migrateDB` CLI tool, which reads every record from a source database
descriptor and writes it to a target one. See the [CLI chapter](./cli.md).
:::

359
doc/deployment.md Normal file
View file

@ -0,0 +1,359 @@
# Deployment
This page collects working configurations for deploying Etherpad in production:
running it behind a reverse proxy, hosting it under a subdirectory, terminating
HTTPS natively, running it as a system service, and deploying it on Kubernetes.
Etherpad listens on port `9001` by default. Throughout this page the upstream
Etherpad server is assumed to be reachable at `http://127.0.0.1:9001`.
## Running behind a reverse proxy
The recommended production setup is to run Etherpad on `127.0.0.1:9001` and put a
reverse proxy in front of it to terminate TLS, serve a virtual host, and forward
requests.
Etherpad uses WebSockets (via socket.io). The load-bearing part of every proxy
config below is the WebSocket upgrade: the proxy **must** forward the `Upgrade`
and `Connection` headers, or real-time editing will silently fail back to slow
long-polling (or break entirely).
When Etherpad runs behind a proxy you should also set `trustProxy: true` in your
settings so that Etherpad honours the `X-Forwarded-*` headers (correct client IP,
secure-cookie flag, etc.). See the `trustProxy` section in the [Configuration documentation](./configuration.md) for the full details of which headers are trusted.
### Nginx
```nginx
# Map the Upgrade header so WebSockets work. Place this in the http context.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name pad.example.com;
ssl_certificate /etc/nginx/ssl/etherpad.crt;
ssl_certificate_key /etc/nginx/ssl/etherpad.key;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_pass_header Server;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
# Redirect plain HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name pad.example.com;
return 301 https://$host$request_uri;
}
```
### Apache
Enable `mod_proxy`, `mod_proxy_http`, `mod_proxy_wstunnel` and `mod_headers`.
The `mod_proxy_wstunnel` `upgrade=websocket` syntax requires Apache 2.4.47 or
newer.
```apache
<VirtualHost *:443>
ServerName pad.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/etherpad/etherpad.crt
SSLCertificateKeyFile /etc/ssl/etherpad/etherpad.key
ProxyVia On
ProxyRequests Off
ProxyPreserveHost On
# WebSocket traffic (socket.io) must be matched first.
<Location "/socket.io">
ProxyPass "ws://127.0.0.1:9001/socket.io" upgrade=websocket timeout=30
ProxyPassReverse "ws://127.0.0.1:9001/socket.io"
</Location>
<Location "/">
ProxyPass "http://127.0.0.1:9001/" retry=0 timeout=30
ProxyPassReverse "http://127.0.0.1:9001/"
</Location>
</VirtualHost>
```
### Caddy
Caddy v2 proxies WebSocket connections automatically and obtains/renews a
certificate for you, so the configuration is minimal:
```caddy
pad.example.com {
reverse_proxy 127.0.0.1:9001
}
```
### Traefik
Traefik v2 also proxies WebSockets transparently. For a Docker deployment, attach
these labels to the Etherpad container:
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.etherpad.rule=Host(`pad.example.com`)"
- "traefik.http.routers.etherpad.entrypoints=websecure"
- "traefik.http.routers.etherpad.tls.certresolver=myresolver"
- "traefik.http.services.etherpad.loadbalancer.server.port=9001"
- "traefik.http.services.etherpad.loadbalancer.passhostheader=true"
```
### HAProxy
HAProxy detects the `Connection: Upgrade` exchange automatically and switches to
tunnel mode once the WebSocket is established. The important value is
`timeout tunnel`, which governs the lifetime of the upgraded connection.
```haproxy
frontend http
mode http
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/etherpad.pem alpn h2,http/1.1
http-request redirect scheme https code 301 unless { ssl_fc }
http-request add-header X-Forwarded-Proto https if { ssl_fc }
default_backend etherpad
backend etherpad
mode http
option forwardfor
timeout client 25s
timeout server 25s
timeout tunnel 3600s
server pad 127.0.0.1:9001
```
## Hosting under a subdirectory
To serve Etherpad from a path such as `https://example.com/pad` rather than from
the root of a domain, the proxy must send the `X-Proxy-Path` header so that
Etherpad rewrites its own asset and API URLs to include the prefix. This header
is honoured regardless of the `trustProxy` setting — see the [Configuration documentation](./configuration.md).
```nginx
location /pad/ {
rewrite ^/pad/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Proxy-Path /pad;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
```
## Native HTTPS without a proxy
Etherpad can terminate TLS itself using Node's native HTTPS server, with no
reverse proxy required. Configure the `ssl` block in `settings.json`:
```json
"ssl": {
"key": "/path-to-your/etherpad-server.key",
"cert": "/path-to-your/etherpad-server.crt",
"ca": ["/path-to-your/intermediate-cert1.crt", "/path-to-your/intermediate-cert2.crt"]
}
```
* `key` — path to the private key file.
* `cert` — path to the certificate file.
* `ca` — an (optional) array of intermediate/chain certificate paths.
Restart Etherpad after editing the settings. It will now serve HTTPS on its
configured port.
For local testing you can generate a self-signed certificate with a single
command:
```bash
openssl req -x509 -newkey rsa:4096 -nodes -days 365 \
-keyout etherpad-server.key -out etherpad-server.crt \
-subj "/CN=localhost"
```
Make sure the files are readable only by the user that runs Etherpad:
```bash
chmod 400 etherpad-server.key etherpad-server.crt
chown etherpad etherpad-server.key etherpad-server.crt
```
::: tip
Self-signed certificates trigger browser warnings and are only suitable for
testing. For production, obtain a free, trusted certificate from
[Let's Encrypt](https://letsencrypt.org/), or terminate TLS at a reverse proxy
(see above) and let it manage certificate issuance and renewal.
:::
## Running as a service (systemd)
On a modern Linux distribution, run Etherpad as a `systemd` service so it starts
on boot and restarts automatically on failure.
Create a dedicated unprivileged user and install Etherpad into its home
directory (for example `/opt/etherpad`), owned by that user. Etherpad refuses to
start as root.
Create `/etc/systemd/system/etherpad.service`:
```ini
[Unit]
Description=Etherpad collaborative editor
After=network.target
[Service]
Type=simple
User=etherpad
Group=etherpad
WorkingDirectory=/opt/etherpad
Environment=NODE_ENV=production
ExecStart=/usr/bin/pnpm run prod
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
Adjust `WorkingDirectory` to your install path and the `ExecStart` path to
wherever `pnpm` lives (`which pnpm`). Then enable and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now etherpad.service
# check status and follow logs
sudo systemctl status etherpad.service
sudo journalctl -u etherpad.service -f
```
## Kubernetes (Istio)
The following manifest deploys Etherpad behind an Istio ingress gateway. It
defines three resources: a `Gateway` (TLS + hostname), a `VirtualService`
(routing with WebSocket-friendly timeouts), and a `DestinationRule` (sticky
sessions via the socket.io `io` cookie).
It assumes:
* Istio >= 1.18
* A `Service` named `etherpad` in the `etherpad` namespace, on port `9001`
* A TLS secret `etherpad-tls` provisioned in the gateway namespace
* You replace `<your-host>` with your own hostname
::: warning
Sticky sessions are necessary but **not** sufficient for a multi-replica
Etherpad deployment. Multi-replica also needs the socket.io Redis adapter so
that pad state is shared across pods. Without it, two clients editing the same
pad but routed to different pods will see divergent state.
Recommendation: start with `replicas: 1` plus good failover, and only go
multi-replica once the Redis adapter is wired up.
:::
```yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: etherpad
namespace: etherpad
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: etherpad-tls
hosts:
- <your-host>
- port:
number: 80
name: http
protocol: HTTP
hosts:
- <your-host>
tls:
httpsRedirect: true
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: etherpad
namespace: etherpad
spec:
hosts:
- <your-host>
gateways:
- etherpad
http:
- match:
- uri:
prefix: /
route:
- destination:
host: etherpad
port:
number: 9001
# No per-request timeout — websockets and long-polling sit on the
# connection indefinitely. The default of 15s kills WS upgrades.
timeout: 0s
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: etherpad
namespace: etherpad
spec:
host: etherpad
trafficPolicy:
loadBalancer:
# Sticky sessions on the socket.io session cookie. Required so that
# long-polling fallback requests land on the same pod that owns the
# session state.
consistentHash:
httpCookie:
name: io
ttl: 0s # session cookie, expires with the browser tab
connectionPool:
tcp:
maxConnections: 10000
http:
# Must comfortably exceed socket.io's pingInterval (25s) +
# pingTimeout (20s). 1h is conservative.
idleTimeout: 3600s
h2UpgradePolicy: UPGRADE
http1MaxPendingRequests: 1000
```

240
doc/development.md Normal file
View file

@ -0,0 +1,240 @@
# Development
This page is a contributor-oriented tour of the Etherpad source tree and of a
few internals that plugin authors and core contributors commonly need to
understand: how the source is laid out, how pads are converted to and from
other formats, and how to access the database from server-side code.
The Etherpad server is written in TypeScript (`.ts`). Most server code lives
under `src/node/` and most client code under `src/static/js/`.
## Source tree overview
The repository root contains, among others, the following directories:
```
etherpad/
|- bin/ # maintenance and build scripts (run.sh, pad tools, docs, release)
|- doc/ # this manual, in AsciiDoc and Markdown
|- src/ # the Etherpad source code
|- packaging/ # OS/distribution packaging helpers
|- var/ # runtime data (e.g. the dirty.db database file)
```
`bin/` contains scripts for running and maintaining Etherpad. For example
`bin/run.sh` starts the server, and there are TypeScript utilities such as
`bin/checkPad.ts`, `bin/deletePad.ts`, `bin/repairPad.ts`,
`bin/rebuildPad.ts`, `bin/migrateDB.ts` and `bin/make_docs.ts`.
The HTML manual is built from the AsciiDoc sources in `doc/` by
`bin/make_docs.ts` (exposed as the `makeDocs` script), which shells out to
`asciidoctor` and writes the result to `out/doc/`. From the repository root you
can run it with `pnpm run makeDocs`. (`asciidoctor` must be installed.)
The `src/` directory looks like this:
```
src/
|- locales/ # translations, managed via https://translatewiki.net
|- node/ # server-side code
|- static/ # client-side code, CSS and fonts
|- templates/ # server-rendered page templates
|- ep.json # core plugin/hook registration
|- package.json # package name: ep_etherpad-lite
```
### src/node/ (server side)
```
src/node/
|- db/ # database access and pad/author/group/session state
|- eejs/ # server-side embedded-JS templating
|- handler/ # import/export and collaboration message handling
|- hooks/ # express route registration and i18n
|- security/ # crypto, OAuth2/OIDC, secret rotation
|- types/ # shared TypeScript types
|- updater/ # in-place self-update machinery
|- utils/ # settings, import/export format helpers, toolbar, minification
|- server.ts # entry point
```
`db/` contains the modules that read and write pad state. `Pad.ts` manages an
individual pad; `PadManager.ts`, `AuthorManager.ts`, `GroupManager.ts`,
`SessionManager.ts` and `ReadOnlyManager.ts` manage the corresponding records;
`DB.ts` exposes the low-level key/value store (see
[Accessing the database from server code / plugins](#accessing-the-database-from-server-code-plugins)); and `API.ts` implements
the public HTTP API.
`handler/` contains the request and message handlers. `PadMessageHandler.ts`
drives real-time collaboration, while `ImportHandler.ts` and `ExportHandler.ts`
handle import and export.
`hooks/` contains mostly Express-related code. `i18n.ts` builds the translation
files and registers routes to serve them, and `hooks/express/` registers the
routes that serve pads, the timeslider, static assets and the admin pages.
`utils/` contains the import/export format converters (`ImportHtml.ts`,
`ExportHtml.ts`, `ExportTxt.ts`, `ExportEtherpad.ts`, `ImportEtherpad.ts`,
`ExportHelper.ts`, and native converters such as `ExportPdfNative.ts` and
`ImportDocxNative.ts`), the settings parser (`Settings.ts`), the toolbar builder
(`toolbar.ts`) and the asset minifier (`Minify.ts`).
### src/static/ (client side)
```
src/static/
|- css/ # stylesheets, including css/pad/icons.css
|- font/ # web fonts, including the fontawesome-etherpad icon font
|- img/
|- js/ # client-side TypeScript
|- skins/ # bundled UI skins
|- vendor/
```
`js/` contains the client-side editor code. Notable modules include
`ace2_inner.ts` and `ace2_common.ts` (the editor core), `contentcollector.ts`,
`linestylefilter.ts` and `domline.ts` (content/attribute processing, shared
with the server import/export pipeline), `Changeset.ts` and `AttributePool.ts`
(the changeset and attribute model), and `collab_client.ts` (the
client side of real-time collaboration).
### src/templates/
`templates/` contains the server-rendered page templates for the index, the
pad, the timeslider and the admin pages, plus the bootstrap scripts that load
the client bundles. The templates expose named `eejs` blocks that plugins can
hook into to inject custom HTML.
## How Etherpad converts pads to and from other formats
Internally a pad is not stored as HTML. A pad is a sequence of lines, and each
line carries **attributes** (for example `heading1`, `bullet` or a list number).
The set of attributes that a pad can use is stored in its **attribute pool**; the
pool only records which attributes exist, not where they are applied. The
pool grows over the history of the pad.
Where an attribute is applied to a line is recorded in an **attribute string**,
and a line that carries a line-level attribute is prefixed with a **line marker**
(`lmkr`). Attribute strings and changesets are defined by
`src/static/js/Changeset.ts` and `src/static/js/AttributePool.ts`.
### Collecting content
`src/static/js/contentcollector.ts` is the shared starting point for both the
client (when content is typed or pasted) and the server (when content is
imported). It walks the incoming DOM/HTML, decides which attributes apply to
each line, adds the discovered attributes to the attribute pool, and emits the
resulting attribute strings. On import, `src/node/utils/ImportHtml.ts` calls
`contentcollector.makeContentCollector(...)` to do exactly this, and the HTML
import path in `src/node/handler/ImportHandler.ts` ultimately drives it.
### From attributes to HTML/text (export)
On export the flow is, conceptually:
```
contentcollector.ts
-> linestylefilter.ts
-> ExportHtml.ts / ExportTxt.ts (helped by ExportHelper.ts)
-> ExportHandler.ts
-> the HTTP API / /export/* route
```
- `src/static/js/linestylefilter.ts` walks each line, reads its attributes,
and turns them into the classes/markup the line should render with.
- `src/node/utils/ExportHelper.ts` adds export-only logic that does not belong
in the live editor. The clearest example is lists: in the editor each list
item is rendered as its own line-level block, but a clean export needs the
items collapsed into a single properly nested list. The helper performs that
reshaping for export only.
- `src/node/utils/ExportHtml.ts` and `src/node/utils/ExportTxt.ts` (and
`ExportEtherpad.ts` for the native `.etherpad` format) turn the attributed
text (`atext`) into the final HTML or plain text.
- `src/node/handler/ExportHandler.ts` receives the export request and dispatches
on the requested format — for instance, office formats such as `.docx` and
`.pdf` are routed through the native converters / LibreOffice rather than
through the plain HTML/text path.
On the client side, edits are turned into changesets by the editor, attributes
are translated into CSS classes (so `heading2` becomes
`class="heading2"`), and `src/static/js/domline.ts` (`createDomLine`) renders
the final DOM for each line.
## Accessing the database from server code / plugins
Etherpad stores everything in a single key/value store backed by
[ueberDB](https://www.npmjs.com/package/ueberdb2), which abstracts over the
configured database (dirtyDB, MySQL/MariaDB, PostgreSQL, SQLite, MongoDB, Redis,
and others). Server-side code and plugins access it through
`src/node/db/DB.ts`.
The package name of the core module is, for historical reasons, still
`ep_etherpad-lite`, so plugins import the database module like this:
```javascript
const db = require('ep_etherpad-lite/node/db/DB');
```
The exposed methods are asynchronous and return promises (use `await`), not the
old callback style. The available methods are `get`, `set`, `remove`, `getSub`,
`setSub`, `findKeys` and `findKeysPaged`:
```javascript
// Read a record (returns undefined/null if it does not exist)
const value = await db.get('record_key');
// Create or replace a record
await db.set('record_key', data);
// Read or write a nested value inside a record
const colorId = await db.getSub('author_key', ['colorId']);
await db.setSub('author_key', ['email'], 'tutti@frutti.org');
// Delete a record
await db.remove('record_key');
```
For example, given the author record:
```json
{"colorId":"#79d9d9","name":"tutti","timestamp":1364832712430,"padIDs":{"mypad":1}}
```
calling `await db.setSub('author_key', ['email'], 'tutti@frutti.org')` yields:
```json
{"colorId":"#79d9d9","name":"tutti","timestamp":1364832712430,"padIDs":{"mypad":1},"email":"tutti@frutti.org"}
```
::: warning
Keys are namespaced (for example `pad:<padId>`,
`pad:<padId>:revs:<rev>`, `globalAuthor:<authorId>`). Prefer the high-level
managers (`Pad.ts`, `AuthorManager.ts`, etc.) over direct `DB` access where one
exists; reach for `DB` directly only for data your plugin owns, and use a key
prefix unique to your plugin to avoid collisions.
:::
## Adding a toolbar icon
Etherpad's toolbar icons come from the bundled `fontawesome-etherpad` icon
font in `src/static/font/`. Toolbar buttons reference an icon by a
`buttonicon-<name>` CSS class (see `src/node/utils/toolbar.ts`, which builds
each button's class as `buttonicon buttonicon-<name>`), and those classes are
defined in `src/static/css/pad/icons.css`. The font itself is generated with
[Fontello](http://fontello.com) from `src/static/font/config.json` (whose
`css_prefix_text` is `buttonicon-`).
To add a new icon:
1. Go to [Fontello](http://fontello.com) and import the existing
`src/static/font/config.json` (Fontello's "import" loads the current icon
set and pre-selects the icons it contains).
2. Select the additional icon(s) you want, then click **Download webfont**.
3. From the unzipped download, copy `config.json` and the
`font/fontawesome-etherpad.*` files over the ones in `src/static/font/`.
4. From the unzipped `css/fontawesome-etherpad.css`, copy the new
`.buttonicon-<name>:before { content: '\\eXXX'; }` rules into
`src/static/css/pad/icons.css`, replacing the existing block of icon rules.
The icon is then available wherever a `buttonicon-<name>` class can be used,
including toolbar button definitions.

View file

@ -29,6 +29,37 @@ Edit `<BASEDIR>/settings.json.docker` at your will. When rebuilding the image, t
**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`.
### How `settings.json` and environment variables interact
This trips people up often enough that it's worth calling out explicitly (see [#7819](https://github.com/ether/etherpad/issues/7819)):
* `settings.json` inside the container is a **template** containing `${VAR:default}` placeholders.
* Environment variable substitution happens at **load time, in memory only** — env vars never overwrite `settings.json` on disk.
* `docker exec <container> cat /opt/etherpad-lite/settings.json` will therefore always show the *templated* file (e.g. `"port": "${PORT:9001}"`), regardless of what `PORT` is set to in your environment. The resolved value is what Etherpad uses at runtime; the file is unchanged.
* The admin /settings page also reads this file directly, so the raw view shows placeholders too. The page now surfaces a banner and an "Effective" tab that displays the in-memory resolved values when placeholders are present.
### Persisting admin /settings edits across container recreates
`settings.json` lives in the container's writable layer by default. That means:
| Operation | Effect on `settings.json` |
|------------------------------------------|------------------------------------------|
| `docker restart` | Preserved (writable layer is reused) |
| `docker compose restart` | Preserved |
| `docker compose down && docker compose up` | **Reset** to the image template |
| `docker compose pull && docker compose up` | **Reset** to the new image template |
| Watchtower / image auto-update | **Reset** to the new image template |
| `docker rm` + `docker run` | **Reset** to the image template |
If you intend to edit `settings.json` through the admin UI (rather than relying solely on env vars), mount the file from the host so edits survive container recreate:
```yaml
volumes:
- ./settings.json:/opt/etherpad-lite/settings.json
```
(Bootstrap by copying `settings.json.docker` to `./settings.json` on the host before the first `up`.) The default compose example below ships this line commented out — uncomment it if you need persistent on-disk edits.
### Rebuilding including some plugins
If you want to install some plugins in your container, it is sufficient to list them in the ETHERPAD_PLUGINS build variable.
The variable value has to be a space separated, double quoted list of plugin names (see examples).
@ -81,10 +112,31 @@ The `settings.json.docker` available by default allows to control almost every s
| `DEFAULT_PAD_TEXT` | The default text of a pad | `Welcome to Etherpad! This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents! Get involved with Etherpad at https://etherpad.org` |
| `IP` | IP which etherpad should bind at. Change to `::` for IPv6 | `0.0.0.0` |
| `PORT` | port which etherpad should bind at | `9001` |
| `PUBLIC_URL` | Canonical public origin of this instance, e.g. `https://pad.example.com` (no trailing slash, must include scheme). Used to build absolute URLs in link-preview meta tags. When `null`, falls back to the incoming request's protocol+Host. | `null` |
| `ENABLE_DARK_MODE` | Respect the end user's browser dark-mode preference. When enabled this overrides the admin-configured skin variants and skin name for that user. | `true` |
| `ADMIN_PASSWORD` | the password for the `admin` user (leave unspecified if you do not want to create it) | |
| `USER_PASSWORD` | the password for the first user `user` (leave unspecified if you do not want to create it) | |
### Updates & privacy (offline / air-gapped)
Etherpad makes a small number of outbound calls (a periodic version check and the admin plugin catalogue). In an air-gapped or firewalled deployment these can be disabled entirely without editing `settings.json` inside the image — set the variables below. See [PRIVACY.md](https://github.com/ether/etherpad/blob/develop/PRIVACY.md) and [doc/admin/updates.md](admin/updates.md) for what each call sends.
| Variable | Description | Default |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `PRIVACY_UPDATE_CHECK` | Set to `false` to disable the hourly version check (`UpdateCheck.ts`). | `true` |
| `PRIVACY_PLUGIN_CATALOG` | Set to `false` to disable the admin plugin browser (manual install-by-name via CLI still works). | `true` |
| `UPDATES_TIER` | Self-updater tier: `off` \| `notify` \| `manual` \| `auto` \| `autonomous`. Set to `off` to suppress the GitHub Releases check entirely. | `notify` |
| `UPDATES_SOURCE` | Where update metadata is fetched from. | `github` |
| `UPDATES_CHANNEL` | Release channel to track. | `stable` |
| `UPDATES_CHECK_INTERVAL_HOURS` | How often (hours) the updater polls when not `off`. | `6` |
| `UPDATES_GITHUB_REPO` | Repository the updater checks for releases. | `ether/etherpad` |
| `UPDATES_REQUIRE_ADMIN_FOR_STATUS`| Lock `/admin/update/status` to authenticated admins. | `false` |
| `UPDATE_SERVER` | Endpoint backing the version check. Point elsewhere (or disable the check above) for offline installs. | `https://etherpad.org/ep_infos` |
> **Fully offline:** set `UPDATES_TIER=off`, `PRIVACY_UPDATE_CHECK=false`, and `PRIVACY_PLUGIN_CATALOG=false`. The version check is fire-and-forget and already fails closed (a blocked endpoint is caught and logged, it does not prevent startup), but disabling it removes the outbound attempt and the log noise.
### Database
| Variable | Description | Default |
@ -179,6 +231,31 @@ For the editor container, you can also make it full width by adding `full-width-
| `DISABLE_IP_LOGGING` | Privacy: disable IP logging | `false` |
### Email (SMTP)
SMTP transport used by the self-updater and admin notifications. When `MAIL_HOST` is `null` (the default) Etherpad keeps log-only behaviour and sends no real mail; set `MAIL_HOST` and `MAIL_FROM` to send via nodemailer.
| Variable | Description | Default |
| ------------- | ------------------------------------------------------------------------------------------- | ------- |
| `MAIL_HOST` | SMTP server hostname. Leave `null` to keep log-only behaviour (no outbound mail). | `null` |
| `MAIL_PORT` | SMTP server port. | `587` |
| `MAIL_SECURE` | Use a secure (TLS) connection to the SMTP server. | `false` |
| `MAIL_FROM` | The `From` address used on outbound mail. Required (together with `MAIL_HOST`) to send mail. | `null` |
### Privacy banner
Optional privacy banner shown to users. See `settings.json.template` for full field docs.
| Variable | Description | Default |
| ------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `PRIVACY_BANNER_ENABLED` | Show the privacy banner. | `false` |
| `PRIVACY_BANNER_TITLE` | Banner title. | `Privacy notice` |
| `PRIVACY_BANNER_BODY` | Banner body text. | `This instance processes pad content on our servers. See the linked policy for retention and how to request erasure.` |
| `PRIVACY_BANNER_LEARN_MORE_URL` | Optional URL for a "learn more" link in the banner. | `null` |
| `PRIVACY_BANNER_DISMISSAL` | Banner dismissal behaviour, e.g. `dismissible`. | `dismissible` |
### Advanced
| Variable | Description | Default |
@ -187,6 +264,10 @@ For the editor container, you can also make it full width by adding `full-width-
| `COOKIE_SESSION_LIFETIME` | How long (ms) a user can be away before they must log in again. | `864000000` (10 days) |
| `COOKIE_SESSION_REFRESH_INTERVAL` | How often (ms) to write the latest cookie expiration time. | `86400000` (1 day) |
| `SHOW_SETTINGS_IN_ADMIN_PAGE` | hide/show the settings.json in admin page | `true` |
| `AUTHENTICATION_METHOD` | Authentication method used by the server. Use `sso` for the built-in OpenID Connect provider, or `apikey` for the legacy API-key authentication system. | `sso` |
| `ENABLE_METRICS` | Enable the Prometheus metrics endpoint used by monitoring plugins to collect metrics about Etherpad. Disable if you do not use any monitoring plugins. | `true` |
| `ENABLE_PAD_WIDE_SETTINGS` | Enable creator-owned pad-wide settings and new-pad default seeding from My View. The pad creator gets a "Pad-wide Settings" section to set/enforce defaults; other users see only their own view options. Set to `false` for the legacy single-settings behavior. | `true` |
| `GDPR_AUTHOR_ERASURE_ENABLED` | Enable the GDPR Art. 17 author anonymize/erasure REST endpoint and admin UI. Enable only when an operator process exists to authorise erasure requests. | `false` |
| `TRUST_PROXY` | set to `true` if you are using a reverse proxy in front of Etherpad (for example: Traefik for SSL termination via Let's Encrypt). This will affect security and correctness of the logs if not done | `false` |
| `IMPORT_MAX_FILE_SIZE` | maximum allowed file size when importing a pad, in bytes. | `52428800` (50 MB) |
| `IMPORT_EXPORT_MAX_REQ_PER_IP` | maximum number of import/export calls per IP. | `10` |
@ -208,7 +289,7 @@ For the editor container, you can also make it full width by adding `full-width-
| `FOCUS_LINE_PERCENTAGE_ARROW_UP` | Percentage of viewport height to be additionally scrolled when user presses arrow up in the line of the top of the viewport. Set to 0 to let the scroll to be handled as default by Etherpad | `0` |
| `FOCUS_LINE_DURATION` | Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation | `0` |
| `FOCUS_LINE_CARET_SCROLL` | Flag to control if it should scroll when user places the caret in the last line of the viewport | `false` |
| `SOCKETIO_MAX_HTTP_BUFFER_SIZE` | The maximum size (in bytes) of a single message accepted via Socket.IO. If a client sends a larger message, its connection gets closed to prevent DoS (memory exhaustion) attacks. | `50000` |
| `SOCKETIO_MAX_HTTP_BUFFER_SIZE` | The maximum size (in bytes) of a single message accepted via Socket.IO. If a client sends a larger message, its connection gets closed to prevent DoS (memory exhaustion) attacks. Larger values allow bigger pastes. | `1000000` (1 MB) |
| `LOAD_TEST` | Allow Load Testing tools to hit the Etherpad Instance. WARNING: this will disable security on the instance. | `false` |
| `DUMP_ON_UNCLEAN_EXIT` | Enable dumping objects preventing a clean exit of Node.js. WARNING: this has a significant performance impact. | `false` |
| `EXPOSE_VERSION` | Expose Etherpad version in the web interface and in the Server http header. Do not enable on production machines. | `false` |
@ -282,6 +363,13 @@ services:
volumes:
- plugins:/opt/etherpad-lite/src/plugin_packages
- etherpad-var:/opt/etherpad-lite/var
# OPTIONAL: persist admin /settings edits across container recreates.
# Without this mount, settings.json lives in the image's writable
# layer — `docker compose restart` preserves it, but `docker compose
# down && up`, `pull`, or watchtower reverts it to the image
# template. Uncomment if you intend to edit settings.json through
# the /admin UI. See https://github.com/ether/etherpad/issues/7819.
# - ./settings.json:/opt/etherpad-lite/settings.json
depends_on:
- postgres
environment:

204
doc/faq.md Normal file
View file

@ -0,0 +1,204 @@
# FAQ
This page answers common operational questions about running and maintaining
an Etherpad instance. It collects material previously kept on the project wiki.
## How do I install Etherpad?
There are several supported ways to install Etherpad. Pick whichever suits your
environment.
### Docker
The official image is published to Docker Hub (`etherpad/etherpad`) and to the
GitHub Container Registry (`ghcr.io/ether/etherpad`) with identical tags.
```bash
docker pull etherpad/etherpad
docker run -p 9001:9001 etherpad/etherpad
```
See the [Docker chapter](./docker.md) for building personalized images, enabling plugins, and
configuring office-format import/export.
### One-line installer (macOS / Linux / WSL)
```bash
curl -fsSL https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.sh | sh
```
On Windows (PowerShell):
```powershell
irm https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.ps1 | iex
```
The installer clones Etherpad, installs dependencies and builds the frontend.
Set `ETHERPAD_RUN=1` to also start it once the install finishes.
### apt repository (Debian / Ubuntu)
Etherpad publishes a signed APT repository (`stable` channel). Import the signing
key, add the repository and install:
```bash
curl -fsSL https://etherpad.org/key.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/etherpad.gpg
echo "deb [signed-by=/usr/share/keyrings/etherpad.gpg] https://etherpad.org/apt stable main" \
| sudo tee /etc/apt/sources.list.d/etherpad.list
sudo apt-get update
sudo apt-get install etherpad
```
The repository provides `amd64` and `arm64` builds. Etherpad depends on
Node.js >= 24, so on older distributions you may also need NodeSource's apt
repository to satisfy that dependency.
### From source
Etherpad requires [Node.js](https://nodejs.org/) >= 24 and `pnpm`.
```bash
git clone -b master https://github.com/ether/etherpad
cd etherpad
pnpm i
pnpm run build:etherpad
pnpm run prod
```
Then open `http://localhost:9001`.
## What URL paths does Etherpad serve?
| Path | Description |
|------|-------------|
| `/admin` | Administration dashboard (requires admin login). |
| `/admin/plugins` | Install, update and remove plugins from the web UI. |
| `/admin/settings` | Edit `settings.json` from the web UI. |
| `/p/:padID` | Open (or create) the pad with the given `padID`, e.g. `/p/foo`. |
| `/p/:padID/timeslider` | Open the pad's history/timeslider view. Append `#N` to jump to a specific revision, e.g. `/p/foo/timeslider#5`. |
| `/p/:padID/export/:type` | Export the pad in the given format, e.g. `/p/foo/export/html`. Append `?revs=N` to export a specific revision. |
Supported export types:
- **Native (no extra dependencies):** `txt`, `html`, `etherpad`, `docx`, `pdf`.
- **Via LibreOffice:** `odt`, `doc`, `rtf` — these require the `soffice` setting
to point at a LibreOffice executable. See the office-format notes in the
[Docker chapter](./docker.md).
## How do I list all pads?
The recommended way is the HTTP API method `listAllPads`, combined with `jq`:
```bash
ETHERPAD_HOST='https://pad.example.com'
ETHERPAD_API_KEY='...' # the APIKEY.txt file in the Etherpad root
ETHERPAD_API_VERSION='...' # see https://pad.example.com/api
curl -s "${ETHERPAD_HOST}/api/${ETHERPAD_API_VERSION}/listAllPads?apikey=${ETHERPAD_API_KEY}" \
| jq -r '.data.padIDs[]'
```
For an interactive list with management actions, install the `ep_adminpads2`
plugin and browse to `/admin/pads`.
As a last resort you can query the database directly. The exact query depends on
your configured backend; pad records use keys of the form `pad:<padID>` and
`pad:<padID>:revs:<n>`. For example, with SQLite:
```bash
sqlite3 ./var/sqlite.db "select key from store where key like 'pad:%'" \
| grep -Eo '^pad:[^:]+' \
| sed -e 's/pad://' \
| sort -u
```
Prefer the API or admin plugin over direct SQL: the schema is an implementation
detail and may change.
## How do I delete or manage pads?
Use the HTTP API `deletePad` method:
```bash
curl -s "${ETHERPAD_HOST}/api/${ETHERPAD_API_VERSION}/deletePad?apikey=${ETHERPAD_API_KEY}&padID=foo"
```
The API also offers `copyPad`, `movePad`, `getRevisionsCount` and more — see the
[HTTP API chapter](./api/http_api.md).
For a web UI, install the `ep_adminpads2` plugin and manage pads from
`/admin/pads`, where you can search, view and delete pads.
The `deletePad` CLI tool is also available for operators:
```bash
pnpm run --filter bin deletePad <padID>
```
## How do I back up and restore pads?
### Back up the whole instance
All pad data lives in the configured database. Back it up using the tool
appropriate to your backend (for example `mysqldump` for MySQL/MariaDB,
`pg_dump` for PostgreSQL, or a file copy of `var/*.db` for the file-based
`dirty`/`rusty` engines while Etherpad is stopped). A regular, automated dump of
the database is the canonical backup for a production instance.
### Back up a single pad
Export the pad over HTTP by appending `/export/<type>` to its URL. Plain text,
HTML and the round-trippable `etherpad` format are most useful for backups:
```bash
curl -o mypad.txt https://pad.example.com/p/foo/export/txt
curl -o mypad.html https://pad.example.com/p/foo/export/html
curl -o mypad.etherpad https://pad.example.com/p/foo/export/etherpad
```
The `etherpad` export preserves the pad's full history and can be re-imported,
making it the best choice for migrating or archiving an individual pad.
### Restore or inspect an old revision
Every state the pad has been in is stored in the database, so you can retrieve
an earlier revision without a separate backup:
- Open `/p/:padID/timeslider` to browse the history and find the revision
number you want.
- Export a specific revision directly with the `?revs=N` query parameter, e.g.
`https://pad.example.com/p/foo/export/html?revs=1000`.
### Repairing a damaged pad
If a pad is corrupt, use the CLI repair tools (`checkPad`, `repairPad`,
`rebuildPad`) documented in the [CLI chapter](./cli.md). Always back up the database before
running write operations.
## How do I limit history or prune revisions?
Etherpad keeps the full revision history of every pad, so the database grows
over time. To reclaim space, use the pad-compaction CLI tools, which collapse or
trim revision history for one pad, every pad, or only stale pads:
```bash
# Collapse all history of one pad
pnpm run --filter bin compactPad <padID>
# Keep only the last 50 revisions of one pad
pnpm run --filter bin compactPad <padID> --keep 50
# Compact every pad on the instance
pnpm run --filter bin compactAllPads
# Compact only pads not edited in the last 90 days, keeping the last 50 revisions
pnpm run --filter bin compactStalePads --older-than 90 --keep 50
```
These tools require `cleanup.enabled = true` in `settings.json` and are
**destructive** — history is collapsed or trimmed. Export anything you can't
afford to lose via the pad's `/export/etherpad` route first. The same primitive
is available over the wire as the `compactPad` HTTP API method. See the [CLI chapter](./cli.md) for full details.

View file

@ -13,6 +13,9 @@ hero:
- theme: brand
text: Install
link: /docker
- theme: alt
text: Configuration
link: /configuration
- theme: alt
text: API documentation
link: /api/

View file

@ -58,8 +58,22 @@ alert ('Chat');
```
to:
```js
alert(window._('pad.chat'));
alert(window.html10n.get('pad.chat'));
```
> **Note:** Etherpad core sets up a `window._` gettext-like shortcut as
> `window._ = html10n.get`. Because this assignment does **not** bind `this`,
> calling the shortcut bare as `window._('pad.chat')` loses its `this` context
> and returns `undefined` (the `get` implementation reads `this.translations`).
> Prefer one of the patterns that actually works:
>
> * Call the method on the object directly: `window.html10n.get('pad.chat')`.
> * Bind it once, then reuse: `const _ = window.html10n.get.bind(window.html10n);`
> followed by `_('pad.chat')`.
>
> Wherever possible, prefer marking up your templates with `data-l10n-id`
> attributes (see above) instead of translating strings in JavaScript at all —
> html10n applies those automatically.
### 2. Create translate files in the locales directory of your plugin
* The name of the file must be the language code of the language it contains translations for (see [supported lang codes](https://joker-x.github.com/languages4translatewiki/test/); e.g. en ? English, es ? Spanish...)

View file

@ -1,7 +1,7 @@
{
"devDependencies": {
"oxc-minify": "^0.132.0",
"vitepress": "^2.0.0-alpha.17"
"oxc-minify": "^0.139.0",
"vitepress": "^2.0.0-alpha.18"
},
"scripts": {
"docs:dev": "vitepress dev",

View file

@ -267,15 +267,16 @@ below.
### Runtime flag
The passthrough is gated by `settings.enablePluginPadOptions`, default
`false`. Operators must opt in via `settings.json`:
`true`. Operators who want to lock plugins out of pad-wide state can flip
it in `settings.json`:
```json
{
"enablePluginPadOptions": true
"enablePluginPadOptions": false
}
```
When enabled, the server reflects the value to every client via
When enabled (the default), the server reflects the value to every client via
`clientVars.enablePluginPadOptions` so plugins can detect both *capable*
(static) and *active* (per-pad request) at the same point.

View file

@ -20,7 +20,7 @@ and the URL to use if you embed the timeslider in your own page.
You can choose a skin changing the parameter `skinName` in `settings.json`.
Since Etherpad **1.7.5**, two skins are included:
Two skins are included:
* `no-skin`: an empty skin, leaving the default Etherpad appearance unchanged, that you can use as guidance to develop your own.
* `colibris`: a new, experimental skin, that will become the default in Etherpad 2.0.
* `colibris`: the current default skin, used by Etherpad out of the box. This is what you see in a standard installation.
* `no-skin`: an unstyled base skin that leaves the default Etherpad appearance unchanged. Use it as a starting point and guidance to develop your own skin.

View file

@ -1,18 +1,147 @@
# Statistics
Etherpad keeps track of the goings-on inside the edit machinery. If you'd like to have a look at this, just point your browser to `/stats`.
# Statistics and metrics
We currently measure:
Etherpad tracks runtime statistics about the edit machinery, the database
layer, and the Node.js process, and can expose them over HTTP for monitoring.
- totalUsers (counter)
- connects (meter)
- disconnects (meter)
- pendingEdits (counter)
- edits (timer)
- failedChangesets (meter)
- httpRequests (timer)
- http500 (meter)
- memoryUsage (gauge)
There are two endpoints:
Under the hood, we are happy to rely on [measured](https://github.com/felixge/node-measured) for all our metrics needs.
- `GET /stats` — a JSON dump of the internal `measured-core` collection.
- `GET /stats/prometheus` — the same kind of data (plus process/runtime
metrics) in the [Prometheus](https://prometheus.io/) text exposition format.
To modify or simply access our stats in your plugin, simply `require('ep_etherpad-lite/stats')` which is a [`measured.Collection`](https://yaorg.github.io/node-measured/packages/measured-core/Collection.html).
## Enabling the endpoints
Both endpoints are gated behind the `enableMetrics` setting, which defaults to
`true`:
```json
{
"enableMetrics": true
}
```
When `enableMetrics` is `false` the routes are **not registered at all** — a
request to `/stats` or `/stats/prometheus` returns the normal 404 handling, not
an empty response. The admin-panel statistics view is unaffected by this
setting.
## `GET /stats` (JSON)
Returns the current snapshot of the `measured-core` collection as JSON. The
following metrics are collected:
| Metric | Type | Meaning |
| --- | --- | --- |
| `totalUsers` | gauge | Number of users currently connected across all pads. |
| `activePads` | gauge | Number of pads with at least one connected user. |
| `connects` | meter | Rate of new client connections. |
| `disconnects` | meter | Rate of client disconnections. |
| `rateLimited` | meter | Rate of messages dropped by the per-connection rate limiter. |
| `pendingEdits` | counter | Edits received but not yet fully processed. |
| `edits` | timer | Time taken to process an incoming `USER_CHANGES` edit (full handler span). |
| `failedChangesets` | meter | Rate of changesets that failed to apply. |
| `httpRequests` | timer | Duration of HTTP requests served by Express. |
| `http500` | meter | Rate of HTTP 500 responses. |
| `memoryUsage` | gauge | Process resident set size (`process.memoryUsage().rss`). |
| `memoryUsageHeap` | gauge | Process heap usage (`process.memoryUsage().heapUsed`). |
| `lastDisconnect` | gauge | Timestamp (ms) of the most recent socket disconnect. |
| `ueberdb_*` | gauge | One gauge per [ueberDB](https://github.com/ether/ueberDB) database statistic, e.g. read/write counts and timings (`ueberdb_reads`, `ueberdb_writes`, …). The exact set depends on the configured database driver. |
Under the hood these are provided by
[`measured-core`](https://github.com/yaorg/node-measured/tree/master/packages/measured-core).
To read or extend them from a plugin, require the shared collection:
```js
const stats = require('ep_etherpad-lite/node/stats');
// stats is a measured-core Collection
stats.counter('my_plugin_events').inc();
console.log(stats.toJSON());
```
## `GET /stats/prometheus` (Prometheus exposition format)
Served from a dedicated [`prom-client`](https://github.com/siimon/prom-client)
registry. This is the endpoint you point a Prometheus scraper at.
### Metrics exposed by default
| Metric | Type | Labels | Meaning |
| --- | --- | --- | --- |
| `etherpad_total_users` | gauge | — | Total number of connected users. |
| `etherpad_active_pads` | gauge | — | Total number of active pads. |
| `ueberdb_stats` | gauge | `type` | ueberDB statistics, one series per numeric ueberDB metric (the metric name is carried in the `type` label). |
In addition, the registry calls `prom-client`'s `collectDefaultMetrics()`, so
the standard Node.js / process metrics are also exposed, including (names as
emitted by `prom-client`):
- `process_cpu_user_seconds_total`, `process_cpu_system_seconds_total`,
`process_cpu_seconds_total`
- `process_resident_memory_bytes`, `process_heap_bytes`,
`process_virtual_memory_bytes`
- `process_open_fds`, `process_max_fds`
- `process_start_time_seconds`
- `nodejs_eventloop_lag_seconds` and the `nodejs_eventloop_lag_*` family
- `nodejs_active_handles`, `nodejs_active_requests`, `nodejs_active_resources`
(and their `_total` variants)
- `nodejs_heap_size_total_bytes`, `nodejs_heap_size_used_bytes`,
`nodejs_external_memory_bytes`, `nodejs_heap_space_size_*_bytes`
- `nodejs_gc_duration_seconds`
- `nodejs_version_info`
The exact default-metric set is determined by `prom-client` and the Node.js
version, not by Etherpad.
### Opt-in scaling-dive metrics (`scalingDiveMetrics`)
A second, more detailed instrument set was added for the scaling investigation
(PR #7756). It is gated behind the `scalingDiveMetrics` setting, which defaults
to `false`:
```json
{
"scalingDiveMetrics": false
}
```
When the flag is off, these metrics are never registered and their recording
helpers short-circuit to no-ops, so production deployments pay nothing for the
instrumentation. When enabled, the following are added to the
`/stats/prometheus` output:
| Metric | Type | Labels | Meaning |
| --- | --- | --- | --- |
| `etherpad_changeset_apply_duration_seconds` | histogram | — | Time spent applying an incoming `USER_CHANGES` message on the server (apply path only; excludes fan-out to other clients). Buckets: 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 5 seconds. |
| `etherpad_socket_emits_total` | counter | `type` | Number of socket.io broadcast emits, bucketed by message type. |
| `etherpad_pad_users` | gauge | `padId` | Active users connected to each pad, keyed by pad id. |
**Cardinality caution.** The scaling-dive metrics carry high-cardinality
labels:
- `etherpad_pad_users` adds one time series **per active pad** (`padId`
label). On instances with many pads this can produce a large number of
series; stale labels are reset on each scrape so drained pads drop out.
- `etherpad_socket_emits_total` uses the `type` label. To keep cardinality
bounded, only a fixed allowlist of known message types is reported; any
other (or missing) type is rolled into a single `other` bucket, so a
misbehaving plugin or API caller cannot explode the label space.
Enable `scalingDiveMetrics` for targeted load-testing or capacity
investigations, not as a permanent production default.
## Scraping with Prometheus
Add a scrape job pointing at the `/stats/prometheus` endpoint, for example:
```yaml
scrape_configs:
- job_name: etherpad
metrics_path: /stats/prometheus
static_configs:
- targets: ['localhost:9001']
```
Make sure `enableMetrics` is `true` (the default) so the endpoint exists. If
your instance is reachable from untrusted networks, restrict access to
`/stats` and `/stats/prometheus` at your reverse proxy, since they expose
operational details about the deployment.

View file

@ -7,15 +7,25 @@ services:
volumes:
- plugins:/opt/etherpad-lite/src/plugin_packages
- etherpad-var:/opt/etherpad-lite/var
# OPTIONAL: persist admin /settings edits across container recreates.
# Without this mount, settings.json lives in the image's writable
# layer — `docker compose restart` preserves it, but `docker compose
# down && up`, `pull`, or watchtower reverts it to the image
# template. Uncomment if you intend to edit settings.json through
# the /admin UI instead of (or in addition to) env vars. See
# https://github.com/ether/etherpad/issues/7819.
# - ./settings.json:/opt/etherpad-lite/settings.json
depends_on:
- postgres
environment:
NODE_ENV: production
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
# Required — set a strong value (e.g. in .env). No fallback, so misconfig
# surfaces at `docker compose up` rather than at runtime.
ADMIN_PASSWORD: "${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:?Set DOCKER_COMPOSE_APP_ADMIN_PASSWORD to a strong value}"
DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PASS: "${DOCKER_COMPOSE_POSTGRES_PASSWORD:?Set DOCKER_COMPOSE_POSTGRES_PASSWORD to a strong value}"
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
@ -23,7 +33,9 @@ services:
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
# Default off: only enable when actually behind a trusted reverse proxy
# that sets the X-Forwarded-* headers.
TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-false}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
@ -32,7 +44,7 @@ services:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PASSWORD: "${DOCKER_COMPOSE_POSTGRES_PASSWORD:?Set DOCKER_COMPOSE_POSTGRES_PASSWORD to a strong value}"
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata

View file

@ -0,0 +1,633 @@
# Downstream Client Compatibility Tests — Phase 1 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a core-side compatibility gate (golden-vector + socket-sequence + HTTP-API-shape contract tests, plus a downstream-smoke workflow scaffold) so a PR against `develop` detects changes that would break the separate downstream clients.
**Architecture:** Layer A — hermetic contract tests run inside core's existing mocha backend suite, anchored to a committed `wire-vectors.json` fixture generated from core's own `Changeset`/`AttributePool`. Layer B — a `downstream-smoke.yml` workflow that boots a real Etherpad on :9003, proves the boot→healthcheck→teardown cycle with a self-check, and is ready to matrix over a `clients.json` manifest as clients register in Phase 2.
**Tech Stack:** TypeScript, mocha (`--import=tsx`), `assert/strict`, supertest + socket.io-client via `common.ts` helpers, GitHub Actions.
**Spec:** `docs/superpowers/specs/2026-06-09-downstream-client-compat-tests-design.md`
**Scope note:** This plan is **Phase 1 only** (all changes in `ether/etherpad`). Phase 2 (wiring each client repo's `test:vectors` + smoke and registering it in the manifest) is one separate plan/PR per client repo and is out of scope here.
---
## File Structure
- Create `src/tests/backend/specs/downstream/generate-vectors.ts` — pure module exporting `generateVectors(): WireVector[]`; the single source of truth for the canonical wire fixtures. Also runnable as a CLI to (re)write the fixture.
- Create `src/tests/fixtures/wire-vectors.json` — committed canonical fixture (generated, never hand-edited).
- Create `src/tests/backend/specs/downstream/wire-vectors.ts` — mocha spec asserting the committed fixture is stable and self-consistent.
- Create `src/tests/backend/specs/downstream/wire-socket-sequence.ts` — mocha spec asserting the socket.io handshake + USER_CHANGES→ACCEPT_COMMIT sequence/shapes.
- Create `src/tests/backend/specs/downstream/wire-http-api.ts` — mocha spec snapshotting client-facing HTTP API response shapes.
- Create `src/tests/downstream/clients.json` — manifest of downstream clients (data; entries `enabled:false` until their Phase-2 smoke lands).
- Create `.github/workflows/downstream-smoke.yml` — boot/healthcheck/self-check/teardown + manifest matrix scaffold.
- Modify `src/package.json` — add `vectors:gen` script.
All backend specs live under `specs/downstream/` so the existing `mocha ... --recursive tests/backend/specs` glob picks them up with zero config change.
---
## Task 1: Golden-vector generator module
**Files:**
- Create: `src/tests/backend/specs/downstream/generate-vectors.ts`
- Test: `src/tests/backend/specs/downstream/wire-vectors.ts` (created in Task 3; this task is tested via Task 2's run)
- [ ] **Step 1: Write the generator module**
Create `src/tests/backend/specs/downstream/generate-vectors.ts`:
```typescript
'use strict';
/**
* Single source of truth for the downstream wire-compatibility fixtures.
*
* Each vector is a self-contained changeset application: given `initialAText`
* and `pool`, applying `changeset` yields `resultAText`. Downstream clients
* (which reimplement Etherpad's changeset/attribpool decoders) consume the
* exact same JSON and must reproduce `resultAText`. See the Phase 1 plan.
*
* Runnable as a CLI to (re)write src/tests/fixtures/wire-vectors.json:
* pnpm run vectors:gen
*/
import Changeset from '../../../../static/js/Changeset';
import AttributePool from '../../../../static/js/AttributePool';
export type WireVector = {
name: string;
initialText: string;
changeset: string;
pool: ReturnType<AttributePool['toJsonable']>;
resultText: string;
};
const vector = (
name: string,
initialText: string,
build: (pool: AttributePool) => string,
): WireVector => {
const pool = new AttributePool();
const changeset = build(pool);
Changeset.checkRep(changeset);
return {
name,
initialText,
changeset,
pool: pool.toJsonable(),
resultText: Changeset.applyToText(changeset, initialText),
};
};
export const generateVectors = (): WireVector[] => [
vector('plain-insert', 'abc\n', () =>
Changeset.makeSplice('abc\n', 3, 0, 'XYZ')),
vector('plain-delete', 'abcdef\n', () =>
Changeset.makeSplice('abcdef\n', 1, 3, '')),
vector('formatted-insert', 'abc\n', (pool) =>
Changeset.makeSplice('abc\n', 3, 0, 'bold', [['bold', 'true']], pool)),
vector('multiline-insert', 'abc\n', () =>
Changeset.makeSplice('abc\n', 3, 0, 'one\ntwo\n')),
vector('attrib-reuse', 'abc\n', (pool) => {
// Two formatted inserts sharing one pool entry exercises pool index reuse.
const cs1 = Changeset.makeSplice('abc\n', 0, 0, 'A', [['bold', 'true']], pool);
const mid = Changeset.applyToText(cs1, 'abc\n');
const cs2 = Changeset.makeSplice(mid, mid.length - 1, 0, 'B', [['bold', 'true']], pool);
return Changeset.compose(cs1, cs2, pool);
}),
];
// CLI entry: write the canonical fixture to disk.
if (require.main === module) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
const out = path.join(__dirname, '../../../fixtures/wire-vectors.json');
fs.writeFileSync(out, `${JSON.stringify(generateVectors(), null, 2)}\n`);
// eslint-disable-next-line no-console
console.log(`wrote ${out}`);
}
```
- [ ] **Step 2: Add the `vectors:gen` script**
In `src/package.json`, add to `"scripts"` (alongside the existing `test` entry):
```json
"vectors:gen": "tsx tests/backend/specs/downstream/generate-vectors.ts",
```
- [ ] **Step 3: Sanity-run the generator (no fixture committed yet)**
Run from `src/`:
```bash
cd src && pnpm run vectors:gen
```
Expected: prints `wrote .../src/tests/fixtures/wire-vectors.json` and the file exists with 5 vectors. Verify each has non-empty `changeset` and a `resultText` that differs from `initialText`.
- [ ] **Step 4: Commit**
```bash
git add src/tests/backend/specs/downstream/generate-vectors.ts src/package.json
git commit -m "test(downstream): add golden wire-vector generator"
```
---
## Task 2: Commit the generated fixture
**Files:**
- Create: `src/tests/fixtures/wire-vectors.json`
- [ ] **Step 1: Generate the fixture**
Run from `src/`:
```bash
cd src && pnpm run vectors:gen
```
Expected: `src/tests/fixtures/wire-vectors.json` written.
- [ ] **Step 2: Eyeball the fixture**
Open `src/tests/fixtures/wire-vectors.json`. Confirm it is a JSON array of 5 objects, each with keys `name, initialText, changeset, pool, resultText`. The `pool` for `plain-insert`/`plain-delete`/`multiline-insert` has empty `numToAttrib`; `formatted-insert` and `attrib-reuse` contain a `bold,true` entry.
- [ ] **Step 3: Commit**
```bash
git add src/tests/fixtures/wire-vectors.json
git commit -m "test(downstream): add committed golden wire-vectors fixture"
```
---
## Task 3: Fixture stability + self-consistency spec
**Files:**
- Create: `src/tests/backend/specs/downstream/wire-vectors.ts`
- [ ] **Step 1: Write the failing test**
Create `src/tests/backend/specs/downstream/wire-vectors.ts`:
```typescript
'use strict';
/**
* Guards the downstream wire-format contract:
* - the committed fixture exactly matches a fresh regeneration (any drift is a
* deliberate wire change and must be re-generated + reviewed in the same PR), and
* - every vector is internally consistent under core's own Changeset engine.
*/
const assert = require('assert').strict;
import fs from 'fs';
import path from 'path';
import Changeset from '../../../../static/js/Changeset';
import {generateVectors} from './generate-vectors';
const fixturePath = path.join(__dirname, '../../../fixtures/wire-vectors.json');
describe(__filename, function () {
it('committed fixture matches a fresh regeneration', function () {
const committed = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
const fresh = generateVectors();
assert.deepEqual(committed, fresh,
'wire-vectors.json is stale — run `pnpm run vectors:gen` and commit the result');
});
it('every vector applies to its result under core Changeset', function () {
for (const v of generateVectors()) {
Changeset.checkRep(v.changeset);
assert.equal(Changeset.applyToText(v.changeset, v.initialText), v.resultText,
`vector ${v.name} result mismatch`);
}
});
});
```
- [ ] **Step 2: Run test to verify it passes (fixture already committed)**
Run from `src/`:
```bash
cd src && pnpm exec mocha --import=tsx --timeout 120000 --extension ts tests/backend/specs/downstream/wire-vectors.ts
```
Expected: 2 passing.
- [ ] **Step 3: Prove the guard bites (temporary edit)**
Hand-edit one `resultText` in `src/tests/fixtures/wire-vectors.json`, re-run the command above.
Expected: the "committed fixture matches a fresh regeneration" test FAILS. Then `git checkout src/tests/fixtures/wire-vectors.json` to restore.
- [ ] **Step 4: Commit**
```bash
git add src/tests/backend/specs/downstream/wire-vectors.ts
git commit -m "test(downstream): assert wire-vectors fixture stability + consistency"
```
---
## Task 4: Socket message-sequence spec
**Files:**
- Create: `src/tests/backend/specs/downstream/wire-socket-sequence.ts`
Reference for helpers: `src/tests/backend/specs/messages.ts` (`common.connect`, `common.handshake`) and `src/tests/backend/common.ts`.
- [ ] **Step 1: Write the failing test**
Create `src/tests/backend/specs/downstream/wire-socket-sequence.ts`:
```typescript
'use strict';
/**
* Pins the socket.io message sequence + shapes that every realtime client
* depends on: handshake -> CLIENT_VARS, then USER_CHANGES -> ACCEPT_COMMIT.
* A change here is a wire-protocol change that will break downstream clients.
*/
const assert = require('assert').strict;
const common = require('../../common');
const padManager = require('../../../node/db/PadManager');
import AttributePool from '../../../../static/js/AttributePool';
import Changeset from '../../../../static/js/Changeset';
describe(__filename, function () {
let agent: any;
let socket: any;
let padId: string;
before(async function () { agent = await common.init(); });
beforeEach(async function () {
padId = common.randomString();
const pad = await padManager.getPad(padId, 'init\n');
await pad.setText('init\n');
const res = await agent.get(`/p/${padId}`).expect(200);
socket = await common.connect(res);
});
afterEach(async function () {
if (socket != null) socket.close();
socket = null;
});
it('handshake returns CLIENT_VARS with the client-facing shape', async function () {
const {type, data} = await common.handshake(socket, padId);
assert.equal(type, 'CLIENT_VARS');
assert.ok(data.userId, 'CLIENT_VARS.userId missing');
assert.ok(data.collab_client_vars, 'collab_client_vars missing');
assert.equal(typeof data.collab_client_vars.rev, 'number');
assert.ok(data.collab_client_vars.initialAttributedText, 'initialAttributedText missing');
});
it('USER_CHANGES is acknowledged with ACCEPT_COMMIT and a bumped rev', async function () {
const {data: clientVars} = await common.handshake(socket, padId);
const rev = clientVars.collab_client_vars.rev;
const pool = new AttributePool();
const cs = Changeset.makeSplice('init\n', 4, 0, '-typed', [], pool);
const accepted = common.waitForSocketEvent(socket, 'message');
socket.emit('message', {
type: 'COLLABROOM',
component: 'pad',
data: {type: 'USER_CHANGES', baseRev: rev, changeset: cs, apool: pool.toJsonable()},
});
const msg: any = await accepted;
assert.equal(msg.type, 'COLLABROOM');
assert.equal(msg.data.type, 'ACCEPT_COMMIT');
assert.equal(msg.data.newRev, rev + 1);
});
});
```
- [ ] **Step 2: Run test to verify it passes**
Run from `src/`:
```bash
cd src && pnpm exec mocha --import=tsx --timeout 120000 --extension ts tests/backend/specs/downstream/wire-socket-sequence.ts
```
Expected: 2 passing. If `waitForSocketEvent`'s default 1s timeout is too tight on the ACCEPT_COMMIT, pass a larger `timeoutMs` (its 3rd arg) — e.g. `common.waitForSocketEvent(socket, 'message', 5000)`.
- [ ] **Step 3: Commit**
```bash
git add src/tests/backend/specs/downstream/wire-socket-sequence.ts
git commit -m "test(downstream): pin socket.io handshake + USER_CHANGES sequence"
```
---
## Task 5: HTTP API shape spec
**Files:**
- Create: `src/tests/backend/specs/downstream/wire-http-api.ts`
Reference: `src/tests/backend/specs/api/api.ts` for the `common.init()` agent + API-version pattern.
- [ ] **Step 1: Write the failing test**
Create `src/tests/backend/specs/downstream/wire-http-api.ts`:
```typescript
'use strict';
/**
* Snapshots the *shapes* (keys/types, not volatile values) of the HTTP API
* endpoints downstream clients call to create pads and round-trip text.
*/
const assert = require('assert').strict;
const common = require('../../common');
describe(__filename, function () {
let agent: any;
let apiVersion = 1;
const apiKey = common.apiKey;
const padId = common.randomString();
const ep = (point: string, qs: string) =>
`/api/${apiVersion}/${point}?apikey=${apiKey}&${qs}`;
before(async function () {
agent = await common.init();
const res = await agent.get('/api/').expect(200);
apiVersion = res.body.currentVersion;
});
it('createPad returns the standard {code,message,data} envelope', async function () {
const res = await agent.get(ep('createPad', `padID=${padId}&text=hello%0A`)).expect(200);
assert.deepEqual(Object.keys(res.body).sort(), ['code', 'data', 'message']);
assert.equal(res.body.code, 0);
});
it('setText + getText round-trips text through the documented shape', async function () {
await agent.get(ep('setText', `padID=${padId}&text=world%0A`)).expect(200);
const res = await agent.get(ep('getText', `padID=${padId}`)).expect(200);
assert.equal(res.body.code, 0);
assert.equal(typeof res.body.data.text, 'string');
assert.equal(res.body.data.text, 'world\n');
});
it('getRevisionsCount exposes a numeric revisions field', async function () {
const res = await agent.get(ep('getRevisionsCount', `padID=${padId}`)).expect(200);
assert.equal(res.body.code, 0);
assert.equal(typeof res.body.data.revisions, 'number');
});
});
```
- [ ] **Step 2: Confirm the apiKey helper name**
Run from `src/`:
```bash
cd src && grep -n "apiKey\|apikey" tests/backend/common.ts | head
```
Expected: a `common.apiKey` export (or similar). If the export is named differently, adjust the `apiKey` reference in the spec to match before running.
- [ ] **Step 3: Run test to verify it passes**
Run from `src/`:
```bash
cd src && pnpm exec mocha --import=tsx --timeout 120000 --extension ts tests/backend/specs/downstream/wire-http-api.ts
```
Expected: 3 passing.
- [ ] **Step 4: Commit**
```bash
git add src/tests/backend/specs/downstream/wire-http-api.ts
git commit -m "test(downstream): snapshot client-facing HTTP API shapes"
```
---
## Task 6: Client manifest
**Files:**
- Create: `src/tests/downstream/clients.json`
- [ ] **Step 1: Write the manifest**
Create `src/tests/downstream/clients.json` (SHAs are current `main` HEADs at authoring; `enabled:false` until each client's Phase-2 smoke lands):
```json
[
{
"name": "etherpad-pad",
"repo": "https://github.com/ether/pad.git",
"ref": "31176d64ce746d45349e58ee6c0bb043052c6e66",
"kind": "rust",
"enabled": false,
"vectorTest": "cargo test --test vectors",
"smokeCmd": "cargo test --test smoke -- --ignored"
},
{
"name": "etherpad-cli-client",
"repo": "https://github.com/ether/etherpad-cli-client.git",
"ref": "edbe0bb70971e54514ebea672e4ad9b51fc55bff",
"kind": "node",
"enabled": false,
"vectorTest": "pnpm run test:vectors",
"smokeCmd": "pnpm run test:smoke"
},
{
"name": "etherpad-desktop",
"repo": "https://github.com/ether/etherpad-desktop.git",
"ref": "ad273c119f1926a8390c9908fc91f62fa2cf740f",
"kind": "desktop",
"enabled": false,
"vectorTest": "pnpm run test:vectors",
"smokeCmd": "pnpm run test:smoke"
}
]
```
- [ ] **Step 2: Validate it is well-formed JSON**
Run:
```bash
node -e "const c=require('./src/tests/downstream/clients.json'); console.log(c.length, c.map(x=>x.name).join(','))"
```
Expected: `3 etherpad-pad,etherpad-cli-client,etherpad-desktop`.
- [ ] **Step 3: Commit**
```bash
git add src/tests/downstream/clients.json
git commit -m "test(downstream): add client manifest (entries disabled pending Phase 2)"
```
---
## Task 7: Downstream-smoke workflow
**Files:**
- Create: `.github/workflows/downstream-smoke.yml`
Reference an existing workflow (`.github/workflows/backend-tests.yml`) for the checkout/pnpm/node setup block this repo uses, and copy that setup verbatim into the job below.
- [ ] **Step 1: Write the workflow**
Create `.github/workflows/downstream-smoke.yml`:
```yaml
name: Downstream smoke
on:
pull_request:
schedule:
- cron: '0 4 * * *' # nightly against develop
permissions:
contents: read
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout core (PR)
uses: actions/checkout@v4
# --- Reuse core's standard node+pnpm setup (copy from backend-tests.yml) ---
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Boot Etherpad on :9003
env:
APIKEY: downstream-smoke-key
run: |
mkdir -p var
echo -n "$APIKEY" > APIKEY.txt
PORT=9003 pnpm run prod &
echo $! > /tmp/ep.pid
- name: Wait for healthcheck
run: |
for i in $(seq 1 60); do
if curl -fsS http://localhost:9003/api/ >/dev/null; then
echo "up"; exit 0
fi
sleep 2
done
echo "server did not come up"; exit 1
- name: Self-check (boot + API roundtrip proves the harness)
run: |
K=downstream-smoke-key
curl -fsS "http://localhost:9003/api/1/createPad?apikey=$K&padID=smoke&text=hi%0A"
curl -fsS "http://localhost:9003/api/1/getText?apikey=$K&padID=smoke" | grep -q '"text":"hi'
- name: Generate canonical wire-vectors
run: cd src && pnpm run vectors:gen
- name: Run enabled downstream clients
run: |
node -e '
const fs=require("fs");
const clients=require("./src/tests/downstream/clients.json").filter(c=>c.enabled);
if(!clients.length){console.log("No clients enabled yet (Phase 1).");process.exit(0);}
fs.writeFileSync("/tmp/clients.json",JSON.stringify(clients));
'
# Phase 2 wires per-kind clone + toolchain + vector injection + smoke here,
# iterating /tmp/clients.json. Until a client is enabled this is a no-op.
- name: Teardown (by PID, never pkill)
if: always()
run: |
if [ -f /tmp/ep.pid ]; then kill "$(cat /tmp/ep.pid)" 2>/dev/null || true; fi
```
- [ ] **Step 2: Confirm the boot command + port env**
Run:
```bash
cd /home/jose/etherpad/etherpad-core-fresh && grep -nE '"prod"|"dev"|"start"' src/package.json
grep -rn "process.env.PORT\|settings.port" src/node/utils/Settings.ts | head
```
Expected: confirm the script that starts a production server and that `PORT`/`APIKEY` are honored (Settings reads `process.env.PORT`). If the runnable script is named differently (e.g. `prod` vs `dev`), update the "Boot Etherpad" step to match. If APIKEY is read from a file rather than env, the `echo ... > APIKEY.txt` line already covers it.
- [ ] **Step 3: Lint the workflow YAML**
Run:
```bash
node -e "require('js-yaml')" 2>/dev/null && npx --yes js-yaml .github/workflows/downstream-smoke.yml >/dev/null && echo "valid yaml" || python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/downstream-smoke.yml')); print('valid yaml')"
```
Expected: `valid yaml`.
- [ ] **Step 4: Commit**
```bash
git add .github/workflows/downstream-smoke.yml
git commit -m "ci(downstream): add downstream-smoke workflow (boot/self-check/teardown + manifest scaffold)"
```
---
## Task 8: Full backend-suite run + push
**Files:** none (verification + integration)
- [ ] **Step 1: Run the whole downstream spec group**
Per the "always run backend tests" rule, run the new specs through the real mocha invocation the suite uses, from `src/`:
```bash
cd src && cross-env NODE_ENV=production pnpm exec mocha --import=tsx --timeout 120000 --extension ts --recursive tests/backend/specs/downstream
```
Expected: all specs in `tests/backend/specs/downstream/` pass (7 tests total across 3 files).
- [ ] **Step 2: Confirm no regression in the fixture guard**
Run from `src/`:
```bash
cd src && pnpm run vectors:gen && git diff --exit-code src/tests/fixtures/wire-vectors.json
```
Expected: exit 0 (regeneration is byte-identical to the committed fixture).
- [ ] **Step 3: Push the branch and open the PR**
```bash
git push -u origin feat/downstream-client-compat-tests
gh pr create --base develop \
--title "test: downstream client compatibility gate (Phase 1)" \
--body "Adds core-side contract tests (golden wire-vectors, socket-sequence, HTTP API shapes) and a downstream-smoke workflow scaffold so PRs detect changes that would break the separate CLI / terminal / desktop clients. Phase 2 wires each client repo's vector+smoke tests and flips its manifest entry to enabled. Spec + plan under docs/superpowers/. Closes nothing; tracks the downstream-compat initiative."
```
- [ ] **Step 4: Watch CI**
Per the "check CI after PRs" rule, wait ~20s then:
```bash
gh pr checks --watch
```
Expected: backend tests green (now including the downstream specs); `Downstream smoke` green (self-check passes, no clients enabled yet). Fix any red before moving on.
---
## Self-Review
**Spec coverage:**
- Layer A golden vectors → Tasks 13. ✅
- Layer A socket sequence → Task 4. ✅
- Layer A HTTP API shapes → Task 5. ✅
- Layer B manifest (pinned SHAs, `enabled` gate) → Task 6. ✅
- Layer B workflow (boot :9003, healthcheck, vector generation, PID teardown, nightly+PR triggers) → Task 7. ✅
- Flakiness mitigations: healthcheck-poll (Task 7 step), PID teardown (Task 7), :9003 (Task 7), no external clients on the gate yet so no flake surface (Task 6 `enabled:false`). ✅
- Phasing: Phase 1 self-contained and green; Phase 2 explicitly out of scope. ✅
**Verification-required tasks** (Task 5 step 2, Task 7 step 2) ask the engineer to confirm the exact `common.apiKey` export name and the production boot script/port handling against the live repo before running — these are real lookups, not placeholders, because those names are repo-version-specific.
**Type consistency:** `WireVector` fields (`name/initialText/changeset/pool/resultText`) are defined in Task 1 and used identically in Tasks 23. `generateVectors()` signature is stable across Tasks 1/3. Manifest keys (`enabled`, `vectorTest`, `smokeCmd`) in Task 6 match the workflow's `.filter(c=>c.enabled)` consumer in Task 7.

View file

@ -197,7 +197,7 @@ GET /admin/openapi.json (CORS: *)
The route is gated by `settings.adminOpenAPI.enabled`, **default `false`**,
per the project's "new features behind a flag, off by default" policy
(CONTRIBUTING.md, AGENTS.MD, best_practices.md). When the flag is off,
(CONTRIBUTING.md, AGENTS.MD). When the flag is off,
`expressPreSession` returns early and the route is dormant.
When enabled, the route registers in `expressPreSession`, which runs before

View file

@ -0,0 +1,154 @@
# Downstream Client Compatibility Tests — Design
**Date:** 2026-06-09
**Status:** Approved (brainstorming complete)
**Target repo:** `ether/etherpad` (core), branch `develop`
**Downstream repos in scope:** `etherpad-pad` (Rust terminal editor + `etherpad-client` crate), `etherpad-cli-client` (Node/TS CLI), `etherpad-desktop` (Electron desktop + Capacitor mobile)
## Problem
The three downstream clients live in **separate repos** and consume core's wire
protocols rather than importing core as a library:
- **etherpad-cli-client** ships its *own copies* of `Changeset.ts` + `AttributePool.ts`
and talks the socket.io `message` protocol. No test script today.
- **etherpad-pad** (Rust) hand-rolls engine.io v4 / socket.io v4 + changeset
decoding in its `etherpad-client` crate. Has CI + a `mock-socket` test feature.
- **etherpad-desktop** wraps / points at a core server URL. Has vitest + Playwright e2e + CI.
A core PR can change the **HTTP API**, the **socket.io handshake / `message`
sequence**, or the **changeset / attribpool wire format** and break these clients
**silently**, because their CI never runs against the new core. The goal: a PR
against core `develop` (and friends) must detect downstream breakage before merge.
## Decisions (locked during brainstorming)
| Decision | Choice |
|---|---|
| Detection strategy | **Hybrid**: fast contract tests in core (every PR) + downstream smoke (every PR) |
| Smoke cadence | **Every PR** (with strong flakiness mitigations) |
| How CI gets clients | **Git clone at pinned refs**, recorded in a manifest in core |
| Contract depth | **Shared golden vectors** — core generates canonical fixtures, each client decodes the same fixtures with its own decoder |
| Sequencing | **Phase by layer** — Phase 1 all core-side; Phase 2 one client repo at a time |
## Architecture
Two-layer compatibility gate on every core PR:
```
core PR ──┬─► Layer A: Contract tests (hermetic, fast, no network/clients)
│ • golden-vector assertions (changeset/attribpool roundtrip)
│ • socket.io message-sequence test (CLIENT_READY → CLIENT_VARS,
│ USER_CHANGES → ACCEPT_COMMIT / NEW_CHANGES)
│ • HTTP API shape snapshots
└─► Layer B: Downstream smoke (boots a real server, runs real clients)
• build + boot Etherpad from the PR on :9003 with a known API key
• healthcheck-poll until ready
• matrix over manifest: clone client @ pinned ref, set up toolchain,
inject core's freshly-generated vectors, run client `test:vectors`,
run client smoke: connect → create/open pad → write text →
read back via HTTP API → assert equality
• tear down server by PID
```
## Layer A — Contract tests (Phase 1, core)
### Golden vectors
- Generator script: `src/tests/downstream/generate-vectors.ts` (run via a package
script, e.g. `pnpm run vectors:gen`).
- Output fixture: `src/tests/fixtures/wire-vectors.json`. Each record:
`{ name, initialAText, changeset, pool, resultAText }` covering the operation
classes clients must decode: plain insert, delete, format/attrib op,
multi-line insert (char_bank ending in `\n`), attribpool reuse across ops.
- Core test `src/tests/backend/specs/wire-vectors.ts`: regenerate in-memory and
assert it matches the committed fixture exactly (drift requires a deliberate
commit, which is the signal a wire change happened).
### Socket message-sequence test
- `src/tests/backend/specs/wire-socket-sequence.ts`: drive a socket.io client
against the in-process server, assert the handshake message sequence and the
shape of `CLIENT_VARS`, `USER_CHANGES``ACCEPT_COMMIT`, and broadcast
`NEW_CHANGES`. Reuses the existing backend socket test helpers.
### HTTP API shape snapshots
- `src/tests/backend/specs/wire-http-api.ts`: snapshot the response *shapes*
(keys / types, not volatile values) of the API endpoints clients call
(`createPad`, `setText`, `getText`, `getRevisionsCount`, session/auth as needed).
These join the existing `backend-tests.yml` run — no new per-PR job, no new infra.
## Layer B — Downstream smoke (Phase 1 scaffold, Phase 2 per client)
### Manifest
`src/tests/downstream/clients.json`:
```json
[
{ "name": "etherpad-pad", "repo": "https://github.com/ether/pad.git", "ref": "<sha on main>", "kind": "rust", "smokeCmd": "..." },
{ "name": "etherpad-cli-client", "repo": "https://github.com/ether/etherpad-cli-client.git", "ref": "<sha on main>", "kind": "node", "smokeCmd": "..." },
{ "name": "etherpad-desktop", "repo": "https://github.com/ether/etherpad-desktop.git", "ref": "<sha on main>", "kind": "desktop", "smokeCmd": "..." }
]
```
Refs are pinned to a specific commit SHA (not `main`) so a client's own pushes
cannot redden core CI; bumping a ref is a deliberate PR. Current `main` HEADs at
authoring time: pad `31176d6`, cli-client `edbe0bb`, desktop `ad273c1`.
Pinned refs mean a client's *own* breakage never randomly reddens core; picking up
a client fix is a deliberate ref-bump PR. Clients are added to the manifest as
their Phase-2 smoke lands — the workflow only runs what's registered.
### Workflow
`.github/workflows/downstream-smoke.yml` (triggers: `pull_request` + nightly
`schedule` against `develop`):
1. Build core from the PR, install deps.
2. Boot Etherpad on **:9003** with a known `APIKEY` in the background; record PID.
3. Healthcheck-poll the server (bounded timeout) before proceeding.
4. Matrix over manifest entries: clone @ pinned ref → set up toolchain
(node+pnpm / rust / electron+xvfb) → copy core's freshly-generated
`wire-vectors.json` into the client → run `test:vectors` → run smoke.
5. Tear down: kill the recorded **PID** (never `pkill -f`).
### Per-client smoke (Phase 2)
Minimal roundtrip exercising the real protocol end-to-end:
- **etherpad-pad** (`rust`): integration test gated by `ETHERPAD_SMOKE_URL`, using
the real tungstenite socket — connect, open pad, send a changeset, read back
via HTTP `getText`, assert. Plus `cargo test` vector consumer reading the
injected fixture.
- **etherpad-cli-client** (`node`): add a minimal test runner (none today —
`node:test` or vitest), a `test:vectors` decoding the fixture, and a smoke
using the client lib: connect → write → verify via HTTP `getText`.
- **etherpad-desktop** (`desktop`): **headless-light** vitest smoke that points
the shell/webview at the booted URL and roundtrips. The full Electron e2e stays
in desktop's own CI — it is **not** in the core gate. If Electron is
unavoidable here, run under `xvfb-run`.
## Flakiness mitigations (because smoke runs on every PR)
- Healthcheck-poll-with-timeout before any client runs.
- Bounded timeout + 1 retry per client smoke.
- Desktop kept headless-light; heavy Electron e2e excluded from the gate.
- PID-based teardown, never `pkill -f` (would kill the developer's other servers).
- Pinned manifest refs isolate core CI from clients' own breakage.
- Tests that bind a port use **:9003** (9001 is reserved for ad-hoc local use).
## Phasing
- **Phase 1 (core only, lands first, immediately useful):** vector generator +
`wire-vectors.json` + three contract specs + `downstream-smoke.yml` +
`clients.json` manifest. Harness proven with **one** reference client wired in
(the Rust `etherpad-pad`, which already has test infra).
- **Phase 2 (one client repo at a time):** order `etherpad-pad`
`etherpad-cli-client``etherpad-desktop`. Each PR adds that client's
`test:vectors` + smoke and registers it in the core manifest.
## Out of scope
- Replacing clients' existing full e2e suites (they stay in their own repos).
- ep_kaput (excluded from all sweeps per standing instruction).
- Changing the wire protocol itself — this work only *observes* it.
## Success criteria
- A core PR that alters changeset serialization, the socket message sequence, or a
client-facing API shape fails Layer A (contract) and/or Layer B (smoke) before merge.
- Phase 1 lands green on core `develop` with the Rust client wired into the smoke matrix.
- Bumping a client's pinned ref is the only way a client's own changes affect core CI.

View file

@ -45,13 +45,13 @@
"node": ">=24.0.0",
"pnpm": ">=11.1.2"
},
"packageManager": "pnpm@11.1.2",
"packageManager": "pnpm@11.10.0",
"repository": {
"type": "git",
"url": "https://github.com/ether/etherpad.git"
},
"engineStrict": true,
"version": "3.2.0",
"version": "3.3.2",
"license": "Apache-2.0",
"pnpm": {
"onlyBuiltDependencies": [

4276
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -21,20 +21,38 @@ strictDepBuilds: false
# As of pnpm 11, overrides must live here (root package.json's pnpm.overrides
# is no longer read). Force-bump transitive deps with known CVEs.
overrides:
basic-ftp: '>=5.3.0'
'@babel/core@<7.29.6': '>=7.29.6'
'@opentelemetry/core@<2.8.0': '>=2.8.0'
basic-ftp@<5.3.1: '>=5.3.1 <6.0.0'
brace-expansion@>=2.0.0 <2.0.3: '>=2.0.3'
diff@>=6.0.0 <8.0.3: '>=8.0.3'
esbuild@<0.28.1: '>=0.28.1'
flatted: '>=3.4.2'
follow-redirects: '>=1.16.0'
form-data@>=4.0.0 <4.0.6: '>=4.0.6'
glob@>=10.2.0 <10.5.0: '>=10.5.0'
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
ip-address@<10.1.1: '>=10.1.1'
js-yaml@>=4.0.0 <4.2.0: '>=4.2.0'
lodash: '>=4.18.0'
minimatch@>=9.0.0 <9.0.7: '>=9.0.7'
path-to-regexp@>=8.0.0 <8.4.0: '>=8.4.0'
picomatch@>=4.0.0 <4.0.4: '>=4.0.4'
qs@>=6.7.0 <6.14.2: '>=6.14.2'
qs@>=6.7.0 <6.15.2: '>=6.15.2'
serialize-javascript@<7.0.5: '>=7.0.5'
socket.io-parser@>=4.0.0 <4.2.6: '>=4.2.6'
tar@<7.5.11: '>=7.5.11'
tar@<7.5.16: '>=7.5.16'
uuid@<14.0.0: '>=14.0.0'
vite@>=7.0.0 <7.3.2: '>=7.3.2'
ws@>=8.0.0 <8.21.0: '>=8.21.0'
minimumReleaseAgeExclude:
- '@radix-ui/primitive@1.1.5'
- '@radix-ui/react-collection@1.1.12'
- '@radix-ui/react-context@1.2.0'
- '@radix-ui/react-dialog@1.1.19'
- '@radix-ui/react-dismissable-layer@1.1.15'
- '@radix-ui/react-focus-scope@1.1.12'
- '@radix-ui/react-presence@1.1.7'
- '@radix-ui/react-toast@1.2.19'
- mysql2@3.22.6
- oidc-provider@9.9.0
- sql-escaper@1.4.0

View file

@ -210,15 +210,17 @@
* tier: "off" | "notify" | "manual" | "auto" | "autonomous"
* Default "notify" shows a banner when an update is available.
* Docker installs are read-only — tiers above "notify" are not applied even if requested.
* Air-gapped / offline deployments should set UPDATES_TIER=off to suppress the
* periodic check against the GitHub Releases API entirely.
*/
"updates": {
"tier": "notify",
"source": "github",
"channel": "stable",
"tier": "${UPDATES_TIER:notify}",
"source": "${UPDATES_SOURCE:github}",
"channel": "${UPDATES_CHANNEL:stable}",
"installMethod": "docker",
"checkIntervalHours": 6,
"githubRepo": "ether/etherpad",
"requireAdminForStatus": false,
"checkIntervalHours": "${UPDATES_CHECK_INTERVAL_HOURS:6}",
"githubRepo": "${UPDATES_GITHUB_REPO:ether/etherpad}",
"requireAdminForStatus": "${UPDATES_REQUIRE_ADMIN_FOR_STATUS:false}",
"preApplyGraceMinutes": 0,
"drainSeconds": 60,
"rollbackHealthCheckSeconds": 60,
@ -321,7 +323,19 @@
* https://etherpad.org/ep_infos
*/
"updateServer": "https://etherpad.org/ep_infos",
"updateServer": "${UPDATE_SERVER:https://etherpad.org/ep_infos}",
/*
* Outbound network calls. See PRIVACY.md for what each one sends.
* - PRIVACY_UPDATE_CHECK=false : disables the hourly version check (UpdateCheck.ts)
* - PRIVACY_PLUGIN_CATALOG=false : disables the admin plugin browser
* (manual install-by-name via CLI still works)
* Air-gapped / firewalled deployments should set both to false.
*/
"privacy": {
"updateCheck": "${PRIVACY_UPDATE_CHECK:true}",
"pluginCatalog": "${PRIVACY_PLUGIN_CATALOG:true}"
},
/*
* The type of the database.

View file

@ -216,7 +216,7 @@
* Default "notify" shows a banner when an update is available. "off" disables the version check.
*/
"updates": {
"tier": "notify",
"tier": "${UPDATES_TIER:notify}",
"source": "github",
"channel": "stable",
"installMethod": "auto",
@ -443,17 +443,19 @@
* https://etherpad.org/ep_infos
*/
"updateServer": "https://etherpad.org/ep_infos",
"updateServer": "${UPDATE_SERVER:https://etherpad.org/ep_infos}",
/*
* Outbound network calls. See PRIVACY.md for what each one sends.
* - updateCheck=false : disables hourly version check (UpdateCheck.ts)
* - pluginCatalog=false: disables admin plugin browser
* (manual install-by-name via CLI still works)
* Air-gapped / firewalled deployments should set both PRIVACY_UPDATE_CHECK and
* PRIVACY_PLUGIN_CATALOG to false (or UPDATES_TIER=off, which covers the version check).
*/
"privacy": {
"updateCheck": true,
"pluginCatalog": true
"updateCheck": "${PRIVACY_UPDATE_CHECK:true}",
"pluginCatalog": "${PRIVACY_PLUGIN_CATALOG:true}"
},
/*
@ -853,11 +855,11 @@
* accepting pad-wide values under plugin-namespaced keys matching
* /^ep_[a-z0-9_]+$/ (e.g. ep_table_of_contents). Values are validated
* (JSON-safe, 64 KB per key, 256 KB total) and broadcast to every
* connected client just like native pad-wide toggles. Disabled by
* default; flip to true once your plugins (e.g. ep_plugin_helpers'
* padToggle) require it. See doc/plugins.md.
* connected client just like native pad-wide toggles. Enabled by
* default; set to false to lock plugins out of pad-wide state. See
* doc/plugins.md.
**/
"enablePluginPadOptions": "${ENABLE_PLUGIN_PAD_OPTIONS:false}",
"enablePluginPadOptions": "${ENABLE_PLUGIN_PAD_OPTIONS:true}",
/*
* Optional privacy banner shown once the pad loads. Disabled by default.

View file

@ -7,6 +7,7 @@
"Haytham morsy",
"Meno25",
"Mido",
"Mohammed Qays",
"Shbib Al-Subaie",
"Tala Ali",
"Test Create account",
@ -57,7 +58,7 @@
"index.transferSessionNow": "نقل الجلسة الآن",
"index.copyLink": "2. نسخ الرابط",
"index.copyLinkDescription": "انقر على الزر أدناه لنسخ الرابط إلى الحافظة الخاصة بك.",
"index.copyLinkButton": سخ الرابط إلى الحافظة",
"index.copyLinkButton": ُسخت الوصلة إلى الحافظة",
"index.transferToSystem": "3. نسخ الجلسة إلى النظام الجديد",
"index.transferToSystemDescription": "افتح الرابط المنسوخ في المتصفح أو الجهاز المستهدف لنقل جلستك.",
"index.transferSessionDescription": "انقل جلستك الحالية إلى المتصفح أو الجهاز بالنقر على الزر أدناه. سيؤدي هذا إلى نسخ رابط لصفحة ستنقل جلستك عند فتحها في المتصفح أو الجهاز المستهدف.",
@ -123,7 +124,7 @@
"pad.importExport.exportword": "مايكروسوفت وورد",
"pad.importExport.exportpdf": "صيغة المستندات المحمولة",
"pad.importExport.exportopen": "ODF (نسق المستند المفتوح)",
"pad.importExport.noConverter.innerHTML": "لا يمكنك الاستيراد إلا من تنسيقات النصوص العادية أو تنسيقات HTML. لمزيد من ميزات الاستيراد المتقدمة، يرجى <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">تثبيت LibreOffice</a> .",
"pad.importExport.noConverter.innerHTML": "يمكنك استيراد النصوص العادية، وملفات HTML، وملفات Microsoft Word (.docx)، وملفات Etherpad مباشرةً. لاستيراد تنسيقات أخرى مثل PDF، وODT، وDOC، وRTF، يحتاج مسؤول الخادم إلى تثبيت LibreOffice - راجع <a href=\"https://docs.etherpad.org/\">وثائق Etherpad</a> .",
"pad.modals.connected": "متصل.",
"pad.modals.reconnecting": "إعادة الاتصال ببادك..",
"pad.modals.forcereconnect": "فرض إعادة الاتصال",

View file

@ -116,7 +116,7 @@
"pad.modals.looping.explanation": "Праблемы далучэньня да сэрвэра сынхранізацыі.",
"pad.modals.looping.cause": "Магчыма, вы падключыліся празь несумяшчальны брандмаўэр або проксі.",
"pad.modals.initsocketfail": "Сэрвэр недаступны.",
"pad.modals.initsocketfail.explanation": "Не атрымалася падлучыцца да сэрвэра сынхранізацыі.",
"pad.modals.initsocketfail.explanation": "Не ўдалося падлучыцца да сэрвэра сынхранізацыі.",
"pad.modals.initsocketfail.cause": "Імаверна, гэта зьвязана з праблемамі з вашым браўзэрам або інтэрнэт-злучэньнем.",
"pad.modals.slowcommit.explanation": "Сэрвэр не адказвае.",
"pad.modals.slowcommit.cause": "Гэта можа быць выклікана праблемамі зь сеткавым падлучэньнем.",
@ -178,9 +178,9 @@
"pad.impexp.importbutton": "Імпартаваць зараз",
"pad.impexp.importing": "Імпартаваньне…",
"pad.impexp.confirmimport": "Імпарт файла перазапіша цяперашні тэкст дакумэнту. Вы ўпэўненыя, што хочаце працягваць?",
"pad.impexp.convertFailed": "Не атрымалася імпартаваць гэты файл. Калі ласка, выкарыстайце іншы фармат дакумэнту або скапіюйце ўручную.",
"pad.impexp.convertFailed": "Не ўдалося імпартаваць гэты файл. Калі ласка, выкарыстайце іншы фармат дакумэнту або скапіюйце рукамі.",
"pad.impexp.padHasData": "Мы не змаглі імпартаваць гэты файл, бо дакумэнт ужо мае зьмены, калі ласка, імпартуйце ў новы дакумэнт",
"pad.impexp.uploadFailed": "Загрузка не атрымалася, калі ласка, паспрабуйце яшчэ раз",
"pad.impexp.uploadFailed": "Загрузка не ўдалася, калі ласка, паспрабуйце яшчэ раз",
"pad.impexp.importfailed": "Памылка імпарту",
"pad.impexp.copypaste": "Калі ласка, скапіюйце і ўстаўце",
"pad.impexp.exportdisabled": "Экспарт у фармаце {{type}} адключаны. Калі ласка, зьвярніцеся да вашага сыстэмнага адміністратара па падрабязнасьці.",

View file

@ -16,13 +16,80 @@
]
},
"admin.page-title": "Ovládací panel Správce - Etherpad",
"admin.loading": "Načítám…",
"admin.loading_description": "Počkejte prosím, než se stránka načte.",
"admin.toggle_sidebar": "Přepnout postranní panel",
"admin.shout": "Komunikace",
"admin_shout.online_one": "Aktuálně je online {{count}} uživatelů",
"admin_shout.online_other": "Aktuálně je online {{count}} uživatelů",
"admin_shout.sticky_toggle": "Změnit připevněnou zprávu",
"admin_login.title": "Etherpad",
"admin_login.username": "Uživatelské jméno",
"admin_login.password": "Heslo",
"admin_login.submit": "Přihlásit se",
"admin_login.failed": "Přihlášení se nezdařilo",
"admin_pads.all_pads": "Všechny Pady",
"admin_pads.bulk.cleanup_history": "Vyčistit historii",
"admin_pads.bulk.clear_selection": "Vymazat výběr",
"admin_pads.bulk.delete": "Smazat",
"admin_pads.cancel": "Zrušit",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Revize",
"admin_pads.col.users": "Uživatelé",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Vyberte název pro nový Pad.",
"admin_pads.delete_pad_dialog_description": "Potvrdit nebo zrušit smazání Padu.",
"admin_pads.delete_pad_dialog_title": "Smazat pad",
"admin_pads.empty_never_edited": "prázdné · nikdy neupravené",
"admin_pads.error_dialog_description": "Došlo k chybě.",
"admin_pads.error_prefix": "Chyba",
"admin_pads.filter.active": "Aktivní",
"admin_pads.filter.all": "Vše",
"admin_pads.filter.empty": "Prázdné",
"admin_pads.filter.recent": "Tento týden",
"admin_pads.filter.stale": "Zastaralé (>1 rok)",
"admin_pads.open": "Otevřít",
"admin_pads.pagination.next": "Další",
"admin_pads.pagination.previous": "Předchozí",
"admin_pads.refresh": "Obnovit",
"admin_pads.relative.days": "před {{count}} dny",
"admin_pads.relative.hours": "před {{count}}h",
"admin_pads.relative.just_now": "právě teď",
"admin_pads.relative.minutes": "před {{count}} minutami",
"admin_pads.relative.months": "před {{count}} měsíci",
"admin_pads.relative.weeks": "před {{count}} týdny",
"admin_pads.relative.years": "před {{count}} lety",
"admin_pads.revisions_count": "{{count}} revizí",
"admin_pads.selected_count": "Vybráno {{count}}",
"admin_pads.show": "Zobrazit",
"admin_pads.sort.name": "Jméno (AZ)",
"admin_pads.sort.revision_number": "Revize",
"admin_pads.sort.user_count": "Uživatelé",
"admin_pads.stats.across_pads": "napříč všemi pady",
"admin_pads.stats.active_users": "Aktivní uživatelé",
"admin_pads.stats.empty_pads": "Prázdné pady",
"admin_pads.stats.last_activity": "Poslední aktivita:",
"admin_pads.stats.no_active_users": "Žádní aktivní uživatelé",
"admin_pads.stats.revisions_zero": "0 revizí",
"admin_pads.stats.total": "Celkový počet padů",
"admin_pads.stats.users_active": "{{count}} aktuálně aktivních",
"admin_pads.subtitle": "Přehled všech padů na této instanci Etherpadu. Vyhledat, vyčistit, otevřít.",
"admin_plugins": "Správce zásuvných moodulů",
"admin_plugins.available": "Dostupné zásuvné moduly",
"admin_plugins.available_not-found": "Nejsou žádné zásuvné moduly",
"admin_plugins.available_fetching": "Načítání...",
"admin_plugins.available_install.value": "Instalovat",
"admin_plugins.available_search.placeholder": "Vyhledat zásuvné moduly k instalaci",
"admin_plugins.check_updates": "Zkontrolovat aktualizace",
"admin_plugins.core_count": "{{count}} jádro",
"admin_plugins.catalog_disabled": "Katalog pluginů je zakázán vaším operátorem (privacy.pluginCatalog=false). Chcete-li plugin nainstalovat, spusťte příkaz `pnpm run plugins i ep_<name> ` ze serveru.",
"admin_plugins.crumbs": "Pluginy",
"admin_plugins.description": "Popis",
"admin_plugins.disables.label": "Vypnuto:",
"admin_plugins.disables.warning_title": "Tento plugin záměrně odstraňuje uvedené funkce Etherpadu.",
"admin_plugins.error_retrieving": "Chyba při načítání pluginů",
"admin_plugins.install_error": "Instalace pluginu {{plugin}} se nezdařila: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Nelze nainstalovat {{plugin}}: vyžaduje novější verzi Etherpadu. Prosím, aktualizujte Etherpad a zkuste to znovu.",
"admin_plugins.installed": "Nainstalované zásuvné moduly",
"admin_plugins.installed_fetching": "Načítání instalovaných zásuvných modulů...",
"admin_plugins.installed_nothing": "Dosud jste nenainstalovali žádné zásuvné moduly.",
@ -30,23 +97,61 @@
"admin_plugins.last-update": "Poslední aktualizace",
"admin_plugins.name": "Název",
"admin_plugins.page-title": "Správce zásuvných modulů - Etherpad",
"admin_plugins.reload_catalog": "Obnovit katalog",
"admin_plugins.search_npm": "Hledat na npm",
"admin_plugins.sort_ascending": "Seřadit vzestupně",
"admin_plugins.sort_descending": "Seřadit sestupně",
"admin_plugins.sort.last_updated": "Naposledy aktualizováno",
"admin_plugins.sort.name": "Jméno (AZ)",
"admin_plugins.sort.version": "Verze",
"admin_plugins.source": "Zdroj pluginu",
"admin_plugins.subtitle": "Instalace, aktualizace a odebrání pluginů Etherpad. Změny vyžadují restart serveru.",
"admin_plugins.tag_core": "Jádro",
"admin_plugins.update_tooltip": "Aktualizovat",
"admin_plugins.updates_available": "Dostupné aktualizace",
"admin_plugins.update_now": "Aktualizovat",
"admin_plugins.version": "Verze",
"admin_plugins_info": "Informace o řešení problému",
"admin_plugins_info.bindings_label": "{{count}} vazeb",
"admin_plugins_info.copy_diagnostics": "Diagnostika kopírování",
"admin_plugins_info.copy_value": "Kopírovat {{label}}",
"admin_plugins_info.git_sha": "SHA v Gitu",
"admin_plugins_info.hooks": "Instalované hooks",
"admin_plugins_info.hooks_client": "hooks na straně klienta",
"admin_plugins_info.hooks_server": "hooks na straně serveru",
"admin_plugins_info.parts": "Nainstalované součásti",
"admin_plugins_info.plugins": "Nainstalované zásuvné moduly",
"admin_plugins_info.page-title": "Informace o zásuvných modulech - Etherpad",
"admin_plugins_info.tab_client": "Klient",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aktuální",
"admin_plugins_info.update_available": "Aktualizace k dispozici: {{version}}",
"admin_plugins_info.version": "Verze Etherpad",
"admin_plugins_info.version_latest": "Poslední dostupná verze",
"admin_plugins_info.version_number": "Číslo verze",
"admin_settings": "Nastavení",
"admin_settings.create_pad": "Vytvořit pad",
"admin_settings.current": "Aktuální konfugurace",
"admin_settings.current_example-devel": "Příklad ukázkové vývojové šablony",
"admin_settings.current_example-prod": "Příklad šablony nastavení výroby",
"admin_settings.current_restart.value": "Restartovat Etherpad",
"admin_settings.current_save.value": "Uložit nastavení",
"admin_settings.invalid_json": "Neplatný JSON",
"admin_settings.current_test.value": "Ověření JSON",
"admin_settings.current_prettify.value": "Zkrášlit JSON",
"admin_settings.toast.saved": "Nastavení bylo úspěšně uloženo.",
"admin_settings.toast.save_failed": "Uložení se nezdařilo: soubor settings.json se nepodařilo zapsat.",
"admin_settings.toast.json_invalid": "Syntaktická chyba: zkontrolujte čárky, závorky a uvozovky.",
"admin_settings.toast.disconnected": "Nelze uložit: nejsem připojen k serveru.",
"admin_settings.toast.validation_ok": "JSON je platný.",
"admin_settings.toast.validation_failed": "JSON je neplatný: opravte syntaktické chyby.",
"admin_settings.toast.prettify_failed": "Nelze upravovat: nejprve opravte syntaktické chyby.",
"admin_settings.prettify_confirm": "Zkrášlováním odstraníte všechny komentáře. Pokračovat?",
"admin_settings.mode.form": "Formulář",
"admin_settings.mode.effective": "Efektivní",
"admin_settings.mode.effective_tooltip": "Zobrazení hodnot, které Etherpad aktuálně používá, pouze pro čtení, po substituci proměnných prostředí. Tajné kódy jsou redigovány.",
"admin_settings.mode.aria_label": "Režim editoru",
"admin_settings.envvar_banner.title": "Tento soubor je šablona, nikoli živá konfigurace.",
"admin_settings.page-title": "Nastavení - Etherpad",
"index.newPad": "Založ nový Pad",
"index.settings": "Nastavení",

View file

@ -10,6 +10,7 @@
"Metalhead64",
"Mklehr",
"Mukeber",
"Nbux",
"Nipsky",
"Predatorix",
"SamTV",
@ -22,13 +23,80 @@
]
},
"admin.page-title": "Admin Dashboard - Etherpad",
"admin.loading": "Lade …",
"admin.loading_description": "Bitte warten, die Seite wird aktualisiert ...",
"admin.toggle_sidebar": "Seitenleiste ein-/ausblenden",
"admin.shout": "Kommunikation",
"admin_shout.online_one": "Es ist derzeit {{count}} Benutzer online",
"admin_shout.online_other": "Es sind aktuell {{count}} Benutzer online",
"admin_shout.sticky_toggle": "Nachricht bleibt angeheftet",
"admin_login.title": "Etherpad",
"admin_login.username": "Benutzername",
"admin_login.password": "Passwort",
"admin_login.submit": "Login",
"admin_login.failed": "Login fehlgeschlagen.",
"admin_pads.all_pads": "Alle Pads",
"admin_pads.bulk.cleanup_history": "Verlauf löschen",
"admin_pads.bulk.clear_selection": "Auswahl löschen",
"admin_pads.bulk.delete": "Löschen",
"admin_pads.cancel": "Abbrechen",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Bearbeitungen",
"admin_pads.col.users": "Benutzer",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Wähle einen Namen für das neue Pad.",
"admin_pads.delete_pad_dialog_description": "Bestätigen oder stornieren Sie die Löschung des Pads.",
"admin_pads.delete_pad_dialog_title": "Pad löschen",
"admin_pads.empty_never_edited": "leer · nie bearbeitet",
"admin_pads.error_dialog_description": "Ein Fehler ist aufgetreten",
"admin_pads.error_prefix": "Fehler",
"admin_pads.filter.active": "Aktiv",
"admin_pads.filter.all": "Alle",
"admin_pads.filter.empty": "Leer",
"admin_pads.filter.recent": "Diese Woche",
"admin_pads.filter.stale": "Veraltet (> 1 Jahr)",
"admin_pads.open": "Offen",
"admin_pads.pagination.next": "Weiter",
"admin_pads.pagination.previous": "Zurück",
"admin_pads.refresh": "Aktualisieren",
"admin_pads.relative.days": "vor {{count}} Tag(en)",
"admin_pads.relative.hours": "vor {{count}} Stunde(n)",
"admin_pads.relative.just_now": "Soeben",
"admin_pads.relative.minutes": "vor {{count}} Minute(n)",
"admin_pads.relative.months": "vor {{count}} Monat(en)",
"admin_pads.relative.weeks": "vor {{count}} Woche(n)",
"admin_pads.relative.years": "vor {{count}} Jahr(en)",
"admin_pads.revisions_count": "{{count}} Bearbeitungen",
"admin_pads.selected_count": "{{count}} ausgewählt",
"admin_pads.show": "Anzeigen",
"admin_pads.sort.name": "Name (AZ)",
"admin_pads.sort.revision_number": "Bearbeitungen",
"admin_pads.sort.user_count": "Nutzer",
"admin_pads.stats.across_pads": "über alle Pads",
"admin_pads.stats.active_users": "Aktive Nutzer",
"admin_pads.stats.empty_pads": "Leere Pads",
"admin_pads.stats.last_activity": "Letzte Aktivität",
"admin_pads.stats.no_active_users": "Keine aktiven Benutzer",
"admin_pads.stats.revisions_zero": "0 Bearbeitungen",
"admin_pads.stats.total": "Gesamtanzahl Pads",
"admin_pads.stats.users_active": "{{count}} aktuell aktiv",
"admin_pads.subtitle": "Überblick über alle Pads auf dieser Etherpad-Instanz. Suchen, aufräumen, öffnen.",
"admin_plugins": "Pluginverwaltung",
"admin_plugins.available": "Verfügbare Plugins",
"admin_plugins.available_not-found": "Keine Plugins gefunden.",
"admin_plugins.available_fetching": "Wird abgerufen...",
"admin_plugins.available_install.value": "Installieren",
"admin_plugins.available_search.placeholder": "Suche nach Plugins zum Installieren",
"admin_plugins.check_updates": "Nach Updates suchen",
"admin_plugins.core_count": "{{count}} Kern(e)",
"admin_plugins.catalog_disabled": "Plugin-Katalog wurde vom Betreiber deaktiviert (privacy.pluginCatalog=false). Um ein Plugin zu installieren, führen Sie `pnpm run plugins i ep_<name>` auf dem Server aus.",
"admin_plugins.crumbs": "Plugins",
"admin_plugins.description": "Beschreibung",
"admin_plugins.disables.label": "Deaktiviert:",
"admin_plugins.disables.warning_title": "Dieses Plugin entfernt absichtlich die aufgeführten Etherpad-Funktionen.",
"admin_plugins.error_retrieving": "Fehler beim Abrufen von Plugins",
"admin_plugins.install_error": "Installation von {{plugin}} fehlgeschlagen: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Installation von {{plugin}} nicht möglich: Hierfür ist neuere Version von Etherpad notwendig. Bitte führe ein Upgrade von Etherpad durch und versuche es erneut.",
"admin_plugins.installed": "Installierte Plugins",
"admin_plugins.installed_fetching": "Rufe installierte Plugins ab...",
"admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.",
@ -36,23 +104,67 @@
"admin_plugins.last-update": "Letze Aktualisierung",
"admin_plugins.name": "Name",
"admin_plugins.page-title": "Plugin Manager - Etherpad",
"admin_plugins.reload_catalog": "Katalog neu laden",
"admin_plugins.search_npm": "Suche auf npm",
"admin_plugins.sort_ascending": "Aufsteigend sortieren",
"admin_plugins.sort_descending": "Absteigend sortieren",
"admin_plugins.sort.last_updated": "Zuletzt aktualisiert",
"admin_plugins.sort.name": "Name (AZ)",
"admin_plugins.sort.version": "Version",
"admin_plugins.source": "Plugin-Quelle",
"admin_plugins.subtitle": "Installieren, aktualisieren und entfernen Sie Etherpad-Plugins. Änderungen erfordern einen Server-Neustart.",
"admin_plugins.tag_core": "Kern",
"admin_plugins.update_tooltip": "Update",
"admin_plugins.updates_available": "Updates verfügbar",
"admin_plugins.update_now": "Update",
"admin_plugins.version": "Version",
"admin_plugins_info": "Hilfestellung",
"admin_plugins_info.copy_diagnostics": "Kopiere Diagnose-Daten",
"admin_plugins_info.copy_value": "Kopiere {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hook_bindings": "Hook-Bindungen",
"admin_plugins_info.hooks": "Installierte Hooks",
"admin_plugins_info.hooks_client": "Client-seitige Hooks",
"admin_plugins_info.hooks_server": "Server-seitige Hooks",
"admin_plugins_info.no_hooks": "Keine Hooks gefunden",
"admin_plugins_info.parts": "Installierte Teile",
"admin_plugins_info.plugins": "Installierte Plugins",
"admin_plugins_info.page-title": "Plugin Informationen - Etherpad",
"admin_plugins_info.search_placeholder": "Suche Hook oder Teil…",
"admin_plugins_info.subtitle": "Systemdiagnose: installierte Version, registrierte Teile und Hooks.",
"admin_plugins_info.tab_client": "Client",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aktuell",
"admin_plugins_info.update_available": "Update verfügbar: {{version}}",
"admin_plugins_info.version": "Etherpad Version",
"admin_plugins_info.version_latest": "Neueste verfügbare Version",
"admin_plugins_info.version_number": "Versionsnummer",
"admin_settings": "Einstellungen",
"admin_settings.create_pad": "Pad erstellen",
"admin_settings.current": "Derzeitige Konfiguration",
"admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates",
"admin_settings.current_example-prod": "Beispiel eines produktiven Templates",
"admin_settings.current_restart.value": "Etherpad neustarten",
"admin_settings.current_save.value": "Einstellungen speichern",
"admin_settings.invalid_json": "Ungültiges JSON",
"admin_settings.current_test.value": "Validiere JSON",
"admin_settings.current_prettify.value": "Prettify JSON",
"admin_settings.toast.saved": "Einstellungen erfolgreich gespeichert.",
"admin_settings.toast.save_failed": "Speichern fehlgeschlagen: settings.json konnte nicht geschrieben werden.",
"admin_settings.toast.json_invalid": "Syntaxfehler: Überprüfen Sie Kommas, Klammern und Anführungszeichen.",
"admin_settings.toast.disconnected": "Nicht gespeichert: Nicht mit dem Server verbunden.",
"admin_settings.toast.validation_ok": "JSON ist gültig.",
"admin_settings.toast.validation_failed": "JSON ist ungültig: Bitte beheben Sie Syntaxfehler.",
"admin_settings.toast.prettify_failed": "Prettify nicht möglich: Bitte beheben Sie zunächst Syntaxfehler.",
"admin_settings.prettify_confirm": "Prettify entfernt alle Kommentare. Weiter?",
"admin_settings.mode.form": "Formular",
"admin_settings.mode.effective_tooltip": "Read-only-Ansicht der Werte, die Etherpad tatsächlich verwendet, nach Umgebungsvariablen-Ersetzung. Secrets sind geschwärzt.",
"admin_settings.mode.aria_label": "Editor-Modus",
"admin_settings.envvar_banner.title": "Diese Datei ist eine Vorlage, nicht die Live-Konfiguration.",
"admin_settings.envvar_banner.body": "Platzhalter wie ${VAR:default} werden beim Start in den Speicher eingesetzt; sie werden nie in diese Datei zurückgeschrieben. Bearbeiten Sie env vars in Ihrer Umgebung (Docker compose, systemd, .env), um den aufgelösten Wert zu ändern, oder ersetzen Sie den Platzhalter hier durch ein Literal. Wechseln Sie auf die Registerkarte \"Effective\", um zu sehen, was Etherpad gerade verwendet.",
"admin_settings.toast.auth_error": "Sie sind nicht als Administrator authentifiziert. Bitte melden Sie sich erneut an.",
"admin_settings.section.general": "Allgemein",
"admin_settings.parse_error.title": "Parsen von settings.json nicht möglich",
"admin_settings.page-title": "Einstellungen - Etherpad",
"index.newPad": "Neues Pad",
"index.settings": "Einstellungen",
@ -75,7 +187,7 @@
"index.labelPad": "Padname (optional)",
"index.placeholderPadEnter": "Gib den Namen des Pads ein...",
"index.createAndShareDocuments": "Erstelle und teile Dokumente in Echtzeit",
"index.createAndShareDocumentsDescription": "Etherpad ermöglicht die gemeinsame Bearbeitung von Dokumenten in Echtzeit, ähnlich wie ein Live-Multiplayer-Editor, der in Ihrem Browser läuft.",
"index.createAndShareDocumentsDescription": "Etherpad ermöglicht die gemeinsame Bearbeitung von Dokumenten in Echtzeit, ähnlich wie ein Live-Multiplayer-Editor, der in Deinem Browser läuft.",
"pad.toolbar.bold.title": "Fett (Strg-B)",
"pad.toolbar.italic.title": "Kursiv (Strg-I)",
"pad.toolbar.underline.title": "Unterstrichen (Strg-U)",
@ -99,18 +211,31 @@
"pad.loading": "Laden …",
"pad.noCookie": "Das Cookie konnte nicht gefunden werden. Bitte erlaube Cookies in deinem Browser! Deine Sitzung und Einstellungen werden zwischen den Besuchen nicht gespeichert. Dies kann darauf zurückzuführen sein, dass Etherpad in einigen Browsern in einem iFrame enthalten ist. Bitte stelle sicher, dass sich Etherpad auf der gleichen Subdomain/Domain wie der übergeordnete iFrame befindet.",
"pad.permissionDenied": "Du hast keine Berechtigung, um auf dieses Pad zuzugreifen.",
"pad.settings.padSettings": "Pad-Einstellungen",
"pad.settings.title": "Einstellungen",
"pad.settings.padSettings": "Pad-weite Einstellungen",
"pad.settings.userSettings": "Nutzereinstellungen",
"pad.settings.myView": "Eigene Ansicht",
"pad.settings.disablechat": "Chat deaktivieren",
"pad.settings.darkMode": "Dunkler Modus",
"pad.settings.stickychat": "Chat immer anzeigen",
"pad.settings.chatandusers": "Chat und Benutzer anzeigen",
"pad.settings.colorcheck": "Autorenfarben anzeigen",
"pad.settings.fadeInactiveAuthorColors": "Farben inaktiver Autoren ausblenden",
"pad.settings.linenocheck": "Zeilennummern",
"pad.settings.rtlcheck": "Inhalt von rechts nach links lesen?",
"pad.settings.enforcedNotice": "Diese Einstellungen wurden vom Ersteller des Pads gesperrt. Wenden Sie sich an den Ersteller, wenn Sie diese ändern möchten.",
"pad.settings.fontType": "Schriftart:",
"pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Sprache:",
"pad.settings.deletePad": "Pad löschen",
"pad.delete.confirm": "Möchtest du dieses Pad wirklich löschen?",
"pad.deletionToken.modalTitle": "Speichere deinen Pad-Löschtoken.",
"pad.deletionToken.modalBody": "Dieses Token ist die einzige Möglichkeit, dieses Pad zu löschen, falls Deine Browsersitzung unterbrochen wird oder Du das Gerät wechselst. Speichere es an einem sicheren Ort es wird hier nur einmal angezeigt.",
"pad.deletionToken.deleteWithToken": "Pad mit Token löschen",
"pad.deletionToken.tokenFieldLabel": "Pad-Löschtoken",
"pad.deletionToken.tokenValueLabel": "Dein Pad-Löschtoken (schreibgeschützt)",
"pad.deletionToken.invalid": "Das angegebene Token ist für dieses Pad nicht gültig.",
"pad.deletionToken.notCreator": "Sie sind nicht der Ersteller dieses Pads, daher können Sie es nicht löschen.",
"pad.settings.about": "Über",
"pad.settings.poweredBy": "Betrieben von",
"pad.importExport.import_export": "Import/Export",
@ -123,7 +248,13 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Export im Etherpad-Format",
"pad.importExport.exporthtmla.title": "Exportieren als HTML",
"pad.importExport.exportplaina.title": "Export als einfacher Text",
"pad.importExport.exportworda.title": "Exportieren als Microsoft Word",
"pad.importExport.exportpdfa.title": "Als PDF exportieren",
"pad.importExport.exportopena.title": "Export als ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.",
"pad.modals.connected": "Verbunden.",
"pad.modals.reconnecting": "Dein Pad wird neu verbunden...",
"pad.modals.forcereconnect": "Erneutes Verbinden erzwingen",
@ -151,9 +282,11 @@
"pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.",
"pad.modals.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.",
"pad.modals.rejected.cause": "Möglicherweise wurde der Server aktualisiert, während du das Pad angesehen hast, oder es existiert ein Fehler in Etherpad. Versuche, die Seite neu zu laden.",
"pad.modals.disconnected": "Ihre Verbindung wurde getrennt.",
"pad.modals.disconnected": "Deine Verbindung wurde getrennt.",
"pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.",
"pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.",
"pad.gritter.unacceptedCommit.title": "Nicht gespeicherte Bearbeitung",
"pad.gritter.unacceptedCommit.text": "Deine letzte Änderung wurde noch nicht gespeichert. Stelle die Verbindung wieder her und versuche es erneut.",
"pad.share": "Dieses Pad teilen",
"pad.share.readonly": "Eingeschränkter Nur-Lese-Zugriff",
"pad.share.link": "Verknüpfung",
@ -166,12 +299,29 @@
"timeslider.followContents": "Aktualisierungen des Pad-Inhalts verfolgen",
"timeslider.pageTitle": "{{appTitle}} Bearbeitungsverlauf",
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
"pad.historyMode.banner": "Versionsgeschichte ansehen",
"pad.historyMode.return": "Zurück zum Live-Modus",
"pad.historyMode.revisionLabel": "Revision {{rev}}",
"pad.historyMode.controlsLabel": "Steuerung Pad-Verlauf",
"pad.historyMode.sliderLabel": "Pad-Version",
"pad.historyMode.settings.title": "Wiedergabe des Bearbeitungsverlaufs",
"pad.historyMode.settings.follow": "Aktualisierungen des Pad-Inhalts verfolgen",
"pad.historyMode.settings.followShort": "Folgen",
"pad.historyMode.followOn": "Pad-Änderungen verfolgen klicken Sie hier, um die Verfolgung zu beenden.",
"pad.historyMode.followOff": "Änderungen der Pads werden nicht verfolgt zum Folgen klicken",
"pad.historyMode.settings.playbackSpeed": "Wiedergabegeschwindigkeit:",
"pad.historyMode.chat.replayHeader": "Chat mit Stand von {{time}}",
"pad.historyMode.users.authorsHeader": "Autoren bei dieser Revision",
"pad.editor.keyboardHint": "Drücken Sie die Escape-Taste, um den Editor zu verlassen. Drücken Sie Alt+F9, um die Symbolleiste aufzurufen.",
"pad.editor.toolbar.formatting": "Formatierungsleiste",
"pad.editor.toolbar.showMore": "Weitere Schaltflächen anzeigen",
"timeslider.toolbar.authors": "Autoren:",
"timeslider.toolbar.authorsList": "Keine Autoren",
"timeslider.toolbar.exportlink.title": "Diese Version exportieren",
"timeslider.exportCurrent": "Exportiere diese Version als:",
"timeslider.version": "Version {{version}}",
"timeslider.saved": "Gespeichert am {{day}}. {{month}} {{year}}",
"timeslider.settings.playbackSpeed": "Wiedergabegeschwindigkeit:",
"timeslider.playPause": "Padbearbeitung abspielen/pausieren",
"timeslider.backRevision": "Eine Version in diesem Pad zurückgehen",
"timeslider.forwardRevision": "Eine Version in diesem Pad vorwärtsgehen",
@ -193,6 +343,7 @@
"pad.savedrevs.timeslider": "Du kannst gespeicherte Versionen durch den Aufruf des Bearbeitungsverlaufs ansehen.",
"pad.userlist.entername": "Dein Name?",
"pad.userlist.unnamed": "unbenannt",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} verbundener Benutzer, other: {{count}} verbundene Benutzer ]}",
"pad.editbar.clearcolors": "Autorenfarben im gesamten Dokument zurücksetzen? Dies kann nicht rückgängig gemacht werden",
"pad.impexp.importbutton": "Jetzt importieren",
"pad.impexp.importing": "Importiere …",
@ -203,5 +354,6 @@
"pad.impexp.importfailed": "Import fehlgeschlagen",
"pad.impexp.copypaste": "Bitte kopieren und einfügen",
"pad.impexp.exportdisabled": "Der Export im {{type}}-Format ist deaktiviert. Für Einzelheiten kontaktiere bitte deinen Systemadministrator.",
"pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen."
"pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen.",
"pad.social.description": "Ein kollaboratives Dokument, das jeder in Echtzeit bearbeiten kann."
}

View file

@ -12,6 +12,7 @@
]
},
"admin.page-title": "Panoyê İdarekari - Etherpad",
"admin_pads.error_dialog_description": "Yew xeta biye.",
"admin_plugins": "Gıredayışê raverberi",
"admin_plugins.available": "Mewcud Dekerdeki",
"admin_plugins.available_not-found": "Dekerdek nevineya",
@ -45,6 +46,7 @@
"admin_settings.current_save.value": "Eyaran qeyd ke",
"admin_settings.page-title": "Eyari - Etherpad",
"index.newPad": "Bloknoto newe",
"index.code": "Kod",
"index.createOpenPad": "ya zi be nê nameyi ra yew bloknot vıraze/ake:",
"index.openPad": "yew Padê biyayeyi be nê nameyi ra ake:",
"pad.toolbar.bold.title": "Qalınd (Ctrl-B)",

View file

@ -1,6 +1,7 @@
{
"admin.page-title": "Admin Dashboard - Etherpad",
"admin.loading": "Loading…",
"admin.loading_description": "Please wait while the page is loading.",
"admin.toggle_sidebar": "Toggle sidebar",
"admin.shout": "Communication",
"admin_shout.online_one": "There is currently {{count}} user online",
@ -20,7 +21,11 @@
"admin_pads.col.revisions": "Revisions",
"admin_pads.col.users": "Users",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Choose a name for the new pad.",
"admin_pads.delete_pad_dialog_description": "Confirm or cancel pad deletion.",
"admin_pads.delete_pad_dialog_title": "Delete pad",
"admin_pads.empty_never_edited": "empty · never edited",
"admin_pads.error_dialog_description": "An error has occurred.",
"admin_pads.error_prefix": "Error",
"admin_pads.filter.active": "Active",
"admin_pads.filter.all": "All",
@ -132,7 +137,12 @@
"admin_settings.prettify_confirm": "Prettifying will remove all comments. Continue?",
"admin_settings.mode.form": "Form",
"admin_settings.mode.raw": "Raw",
"admin_settings.mode.effective": "Effective",
"admin_settings.mode.effective_tooltip": "Read-only view of the values Etherpad is actually using right now, after environment-variable substitution. Secrets are redacted.",
"admin_settings.mode.aria_label": "Editor mode",
"admin_settings.envvar_banner.title": "This file is a template, not the live config.",
"admin_settings.envvar_banner.body": "Placeholders like ${VAR:default} are substituted into memory at startup; they are never written back to this file. Edit env vars in your environment (Docker compose, systemd, .env) to change the resolved value, or replace the placeholder here with a literal. Switch to the Effective tab to see what Etherpad is using right now.",
"admin_settings.toast.auth_error": "You are not authenticated as admin. Please log in again.",
"admin_settings.section.general": "General",
"admin_settings.parse_error.title": "Cannot parse settings.json",
"admin_settings.parse_error.cta": "Switch to raw to edit",
@ -217,6 +227,7 @@
"index.copyLinkButton": "Copy link to clipboard",
"index.transferToSystem": "3. Copy session to new system",
"index.transferToSystemDescription": "Open the copied link in the target browser or device to transfer your session.",
"index.code": "Code",
"index.transferSessionDescription": "Transfer your current session to browser or device by clicking the button below. This will copy a link to a page that will transfer your session when opened in the target browser or device.",
"index.createOpenPad": "Open pad by name",
"index.openPad": "open an existing Pad with the name:",
@ -303,7 +314,7 @@
"pad.importExport.exportworda.title": "Export as Microsoft Word",
"pad.importExport.exportpdfa.title": "Export as PDF",
"pad.importExport.exportopena.title": "Export as ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "You can only import from plain text or HTML formats. For more advanced import features, please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install LibreOffice</a>.",
"pad.importExport.noConverter.innerHTML": "You can import plain text, HTML, Microsoft Word (.docx) and Etherpad files directly. To import other formats such as PDF, ODT, DOC or RTF, the server administrator needs to install LibreOffice — see the <a href=\"https://docs.etherpad.org/\">Etherpad documentation</a>.",
"pad.modals.connected": "Connected.",
"pad.modals.reconnecting": "Reconnecting to your pad…",

View file

@ -23,6 +23,27 @@
]
},
"admin.page-title": "Ylläpitäjän kojelauta - Etherpad",
"admin.loading": "Ladataan…",
"admin.loading_description": "Odota. Sivu latautuu.",
"admin.toggle_sidebar": "Näytä/piilota sivupalkki",
"admin.shout": "Viestintä",
"admin_shout.online_one": "Tällä hetkellä {{count}} käyttäjä on paikalla",
"admin_shout.online_other": "Tällä hetkellä {{count}} käyttäjää on paikalla",
"admin_shout.sticky_toggle": "Muuta pysyvää viestiä",
"admin_login.title": "Etherpad",
"admin_login.username": "Käyttäjänimi",
"admin_login.password": "Salasana",
"admin_login.submit": "Kirjaudu sisään",
"admin_login.failed": "Kirjautuminen epäonnistui",
"admin_pads.all_pads": "Kaikki muistiot",
"admin_pads.bulk.cleanup_history": "Tyhjennä historia",
"admin_pads.bulk.delete": "Poista",
"admin_pads.cancel": "Peru",
"admin_pads.col.pad": "Muistio",
"admin_pads.col.revisions": "Muokkaushistoria",
"admin_pads.col.users": "Käyttäjät",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Valitse nimi uudelle muistiolle.",
"admin_plugins": "Lisäosien hallinta",
"admin_plugins.available": "Saatavilla olevat liitännäiset",
"admin_plugins.available_not-found": "Lisäosia ei löytynyt.",

View file

@ -7,9 +7,11 @@
"Chpol",
"Cquoi",
"Crochet.david",
"Crowwhailord",
"Derugon",
"Envlh",
"Framafan",
"Framasky",
"Fylip22",
"Gomoko",
"Goofy",
@ -37,13 +39,73 @@
]
},
"admin.page-title": "Tableau de bord administrateur — Etherpad",
"admin.loading": "Chargement en cours…",
"admin.loading_description": "Veuillez patienter pendant le chargement de la page.",
"admin.toggle_sidebar": "Afficher/masquer la barre latérale",
"admin.shout": "Communication",
"admin_shout.online_one": "Il y a actuellement {{count}} utilisateur·ice en ligne",
"admin_shout.online_other": "Il y a actuellement {{count}} utilisateur·ices en ligne",
"admin_shout.sticky_toggle": "Modifier le message épinglé",
"admin_login.title": "Etherpad",
"admin_login.username": "Nom dutilisateur",
"admin_login.password": "Mot de passe",
"admin_login.submit": "Connexion",
"admin_login.failed": "Échec de la connexion",
"admin_pads.all_pads": "Tous les blocs-notes",
"admin_pads.bulk.cleanup_history": "Effacer lhistorique",
"admin_pads.bulk.clear_selection": "Effacer la sélection",
"admin_pads.bulk.delete": "Supprimer",
"admin_pads.cancel": "Annuler",
"admin_pads.col.pad": "Bloc-notes",
"admin_pads.col.revisions": "Révisions",
"admin_pads.col.users": "Utilisateur·ices",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Choisissez un nom pour le nouveau bloc-notes.",
"admin_pads.delete_pad_dialog_description": "Confirmer ou annuler la suppression du bloc-notes.",
"admin_pads.delete_pad_dialog_title": "Supprimer le bloc-notes",
"admin_pads.empty_never_edited": "vide · jamais modifié",
"admin_pads.error_dialog_description": "Une erreur sest produite.",
"admin_pads.error_prefix": "Erreur",
"admin_pads.filter.active": "Actif",
"admin_pads.filter.all": "Tous",
"admin_pads.filter.empty": "Vide",
"admin_pads.filter.recent": "Cette semaine",
"admin_pads.filter.stale": "Sans modifications (> 1 an)",
"admin_pads.open": "Ouvert",
"admin_pads.pagination.next": "Suivant",
"admin_pads.pagination.previous": "Précédent",
"admin_pads.refresh": "Actualiser",
"admin_pads.relative.days": "il y a {{count}} jours",
"admin_pads.relative.hours": "il y a {{count}} heures",
"admin_pads.relative.just_now": "à linstant",
"admin_pads.show": "Afficher",
"admin_pads.sort.name": "Nom (AZ)",
"admin_pads.sort.revision_number": "Révisions",
"admin_pads.sort.user_count": "Utilisateur·ices",
"admin_pads.stats.across_pads": "sur tous les bloc-notes",
"admin_pads.stats.active_users": "Utilisateur·ices actif·ves",
"admin_pads.stats.empty_pads": "Bloc-notes vides",
"admin_pads.stats.last_activity": "Dernière activité :",
"admin_pads.stats.no_active_users": "Aucun·e utilisateur·ice actif·ve",
"admin_pads.stats.revisions_zero": "0 révisions",
"admin_pads.stats.total": "Nombre total de bloc-notes",
"admin_pads.stats.users_active": "{{count}} actuellement actifs",
"admin_pads.subtitle": "Aperçu de tous les bloc-notes de cette instance Etherpad. Rechercher, nettoyer, ouvrir.",
"admin_plugins": "Gestionnaire de greffons",
"admin_plugins.available": "Greffons disponibles",
"admin_plugins.available_not-found": "Aucun greffon trouvé.",
"admin_plugins.available_fetching": "Récupération en cours...",
"admin_plugins.available_install.value": "Installer",
"admin_plugins.available_search.placeholder": "Rechercher des greffons à installer",
"admin_plugins.check_updates": "Vérifier les mises à jour",
"admin_plugins.catalog_disabled": "Le catalogue de greffons est désactivé par votre administrateur (privacy.pluginCatalog=false). Pour installer un greffon, exécutez `pnpm run plugins i ep_<name> ` sur le serveur.",
"admin_plugins.crumbs": "Greffons",
"admin_plugins.description": "Description",
"admin_plugins.disables.label": "Désactive :",
"admin_plugins.disables.warning_title": "Ce greffon supprime intentionnellement les fonctionnalités Etherpad listées.",
"admin_plugins.error_retrieving": "Erreur lors de la récupération des greffons",
"admin_plugins.install_error": "Échec de l'installation de {{plugin}} : {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Impossible d'installer {{plugin}} : une version plus récente d'Etherpad est requise. Veuillez mettre à jour Etherpad et réessayer.",
"admin_plugins.installed": "Greffons installés",
"admin_plugins.installed_fetching": "Récupération des greffons installés en cours...",
"admin_plugins.installed_nothing": "Vous navez encore installé aucun greffon.",
@ -51,8 +113,25 @@
"admin_plugins.last-update": "Dernière mise à jour",
"admin_plugins.name": "Nom",
"admin_plugins.page-title": "Gestionnaire de greffons — Etherpad",
"admin_plugins.reload_catalog": "Recharger le catalogue",
"admin_plugins.search_npm": "Rechercher sur npm",
"admin_plugins.sort_ascending": "Tri croissant",
"admin_plugins.sort_descending": "Tri décroissant",
"admin_plugins.sort.last_updated": "Dernière mise à jour",
"admin_plugins.sort.name": "Nom (AZ)",
"admin_plugins.sort.version": "Version",
"admin_plugins.source": "Source du greffon",
"admin_plugins.subtitle": "Installez, mettez à jour et supprimez les greffons dEtherpad. Toute modification nécessite un redémarrage du serveur.",
"admin_plugins.tag_core": "Cœur",
"admin_plugins.update_tooltip": "Mise à jour",
"admin_plugins.updates_available": "Mises à jour disponibles",
"admin_plugins.update_now": "Mettre à jour",
"admin_plugins.version": "Version",
"admin_plugins_info": "Informations de résolution de problème",
"admin_plugins_info.bindings_label": "{{count}} liaisons",
"admin_plugins_info.copy_diagnostics": "Copie des diagnostics",
"admin_plugins_info.copy_value": "Copier {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hooks": "Crochets installés",
"admin_plugins_info.hooks_client": "Crochets côté client",
"admin_plugins_info.hooks_server": "Crochets côté serveur",
@ -156,6 +235,16 @@
"pad.settings.language": "Langue:",
"pad.settings.deletePad": "Supprimer le bloc-notes",
"pad.delete.confirm": "Voulez-vous vraiment supprimer ce bloc-notes ?",
"pad.deletionToken.modalTitle": "Enregistrez votre jeton de suppression de bloc-notes",
"pad.deletionToken.modalBody": "Ce jeton est le seul moyen de supprimer ce bloc-notes si vous perdez votre session de navigateur ou si vous changez d'appareil. Conservez-le en lieu sûr — il n'apparaît ici qu'une seule fois.",
"pad.deletionToken.copy": "Copier",
"pad.deletionToken.copied": "Copié",
"pad.deletionToken.acknowledge": "Je l'ai enregistré",
"pad.deletionToken.deleteWithToken": "Supprimer le bloc-notes avec un jeton",
"pad.deletionToken.tokenFieldLabel": "Jeton de suppression de bloc-notes",
"pad.deletionToken.tokenValueLabel": "Votre jeton de suppression du bloc-notes (lecture seule)",
"pad.deletionToken.invalid": "Ce jeton n'est pas valable pour ce bloc-notes.",
"pad.deletionToken.notCreator": "Vous nêtes pas le créateur du bloc-notes, vous ne pouvez donc pas le supprimer.",
"pad.settings.about": "À propos",
"pad.settings.poweredBy": "Propulsé par",
"pad.importExport.import_export": "Importer/Exporter",
@ -168,7 +257,10 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Vous pouvez uniquement importer du texte brut ou du HTML. Pour des fonctionnalités d'importation plus avancées, veuillez <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer LibreOffice</a>.",
"pad.importExport.exportetherpada.title": "Exporter au format Etherpad",
"pad.importExport.exporthtmla.title": "Exporter au format HTML",
"pad.importExport.exportplaina.title": "Exporter en texte brut",
"pad.importExport.noConverter.innerHTML": "Vous pouvez importer directement du texte brut, du HTML,Microsoft Word (.docx) et des fichiers Etherpad. Pour importer d'autres formats tels que PDF, ODT, DOC ou RTF, l'administrateur du serveur doit installer LibreOffice ; consultez la <a href=\"https://docs.etherpad.org/\">documentation Etherpad</a> .",
"pad.modals.connected": "Connecté.",
"pad.modals.reconnecting": "Reconnexion à votre bloc-notes en cours...",
"pad.modals.forcereconnect": "Forcer la reconnexion",

View file

@ -6,6 +6,7 @@
},
"admin.page-title": "Painéal Riaracháin - Etherpad",
"admin.loading": "Ag lódáil…",
"admin.loading_description": "Fan go fóill fad atá an leathanach á lódáil.",
"admin.toggle_sidebar": "Scoránaigh an taobhbharra",
"admin.shout": "Cumarsáid",
"admin_shout.online_one": "Tá {{count}} úsáideoir ar líne faoi láthair",
@ -25,7 +26,11 @@
"admin_pads.col.revisions": "Athbhreithnithe",
"admin_pads.col.users": "Úsáideoirí",
"admin_pads.confirm_button": "Ceart go leor",
"admin_pads.create_pad_dialog_description": "Roghnaigh ainm don phainéal nua.",
"admin_pads.delete_pad_dialog_description": "Deimhnigh nó cealaigh scriosadh an eochaircheap.",
"admin_pads.delete_pad_dialog_title": "Scrios an eochaircheap",
"admin_pads.empty_never_edited": "folamh · níor cuireadh in eagar riamh",
"admin_pads.error_dialog_description": "Tharla earráid.",
"admin_pads.error_prefix": "Earráid",
"admin_pads.filter.active": "Gníomhach",
"admin_pads.filter.all": "Gach",
@ -137,7 +142,12 @@
"admin_settings.prettify_confirm": "Bainfear na tráchtanna go léir má dhéantar áilleachtú. Leanfaidh tú ar aghaidh?",
"admin_settings.mode.form": "Foirm",
"admin_settings.mode.raw": "Amh",
"admin_settings.mode.effective": "Éifeachtach",
"admin_settings.mode.effective_tooltip": "Radharc inléite amháin ar na luachanna atá á n-úsáid ag Etherpad faoi láthair, tar éis athsholáthar athróg timpeallachta. Tá rúin curtha in eagar.",
"admin_settings.mode.aria_label": "Mód eagarthóireachta",
"admin_settings.envvar_banner.title": "Is teimpléad an comhad seo, ní an chumraíocht bheo.",
"admin_settings.envvar_banner.body": "Cuirtear áitchoimeádaithe cosúil le ${VAR:default} isteach sa chuimhne ag an am tosaithe; ní scríobhtar ar ais chuig an gcomhad seo iad choíche. Cuir na cineálacha timpeallachta in eagar i do thimpeallacht (Docker compose, systemd, .env) chun an luach réitithe a athrú, nó cuir litriúil in ionad an áitchoimeádaitheora anseo. Téigh go dtí an cluaisín Éifeachtach chun a fheiceáil cad atá á úsáid ag Etherpad faoi láthair.",
"admin_settings.toast.auth_error": "Níl tú fíordheimhnithe mar riarthóir. Logáil isteach arís le do thoil.",
"admin_settings.section.general": "Ginearálta",
"admin_settings.parse_error.title": "Ní féidir settings.json a pharsáil",
"admin_settings.parse_error.cta": "Athraigh go amh le heagarthóireacht",
@ -220,6 +230,7 @@
"index.copyLinkButton": "Cóipeáil nasc chuig an ghearrthaisce",
"index.transferToSystem": "3. Cóipeáil an seisiún chuig an gcóras nua",
"index.transferToSystemDescription": "Oscail an nasc cóipeáilte sa bhrabhsálaí nó ar an ngléas sprice chun do sheisiún a aistriú.",
"index.code": "Cód",
"index.transferSessionDescription": "Aistrigh do sheisiún reatha chuig brabhsálaí nó gléas trí chliceáil ar an gcnaipe thíos. Cóipeálfaidh sé seo nasc chuig leathanach a aistreoidh do sheisiún nuair a osclófar é sa bhrabhsálaí nó sa ghléas sprice.",
"index.createOpenPad": "Oscail ceap de réir ainm",
"index.openPad": "oscail Pad atá ann cheana féin leis an ainm:",

View file

@ -8,6 +8,7 @@
},
"admin.page-title": "Panel de administración - Etherpad",
"admin.loading": "Cargando…",
"admin.loading_description": "Agarda mentres se carga a páxina.",
"admin.toggle_sidebar": "Activar ou desactivar a barra lateral",
"admin.shout": "Comunicación",
"admin_shout.online_one": "Nestes intres hai {{count}} usuario en liña",
@ -27,7 +28,11 @@
"admin_pads.col.revisions": "Revisións",
"admin_pads.col.users": "Usuarios",
"admin_pads.confirm_button": "Aceptar",
"admin_pads.create_pad_dialog_description": "Escolle un nome para o documento novo.",
"admin_pads.delete_pad_dialog_description": "Confirmar ou cancelar a eliminación do documento.",
"admin_pads.delete_pad_dialog_title": "Borrar o documento",
"admin_pads.empty_never_edited": "baleiro · non se editou nunca",
"admin_pads.error_dialog_description": "Houbo un erro.",
"admin_pads.error_prefix": "Erro",
"admin_pads.filter.active": "Activos",
"admin_pads.filter.all": "Todos",
@ -139,7 +144,12 @@
"admin_settings.prettify_confirm": "Ao aquelar, eliminaranse todos os comentarios. Queres continuar?",
"admin_settings.mode.form": "Formulario",
"admin_settings.mode.raw": "En bruto",
"admin_settings.mode.effective": "Efectivo",
"admin_settings.mode.effective_tooltip": "Vista de só lectura dos valores que Etherpad está a usar agora mesmo, despois da substitución das variables de contorno. Os segredos están agochados.",
"admin_settings.mode.aria_label": "Modo de edición",
"admin_settings.envvar_banner.title": "Este ficheiro é un modelo, non a configuración real.",
"admin_settings.envvar_banner.body": "Os marcadores de posición como «${VAR:default}» substitúense na memoria durante o inicio da aplicación; nunca se escribir neste ficheiro. Edita as variables no teu contorno (Docker compose, systemd, .env) para cambiar o valor resolto ou substitúe o marcador de posición aquí por unha cadea de texto literal. Cambia á lapela «Efectivo» para ver o que está a usar Etherpad agora mesmo.",
"admin_settings.toast.auth_error": "Non te conectaches cunha conta administrativa. Inicia sesión de novo.",
"admin_settings.section.general": "Xeral",
"admin_settings.parse_error.title": "Non se pode analizar o ficheiro «settings.json»",
"admin_settings.parse_error.cta": "Cambiar ao editor en bruto para editar",
@ -222,6 +232,7 @@
"index.copyLinkButton": "Copiar a ligazón no portapapeis",
"index.transferToSystem": "3. Copia a sesión no novo sistema",
"index.transferToSystemDescription": "Abre a ligazón copiada no navegador ou dispositivo de destino para transferir a túa sesión.",
"index.code": "Código",
"index.transferSessionDescription": "Transfire a túa sesión actual ao navegador ou dispositivo facendo clic no botón de embaixo. Isto copiará unha ligazón cara a unha páxina que transferirá a túa sesión cando se abra no navegador ou dispositivo de destino.",
"index.createOpenPad": "Abrir un documento por nome",
"index.openPad": "abrir un documento existente co nome:",

View file

@ -15,13 +15,80 @@
]
},
"admin.page-title": "Pannello amministrativo - Etherpad",
"admin.loading": "Caricamento…",
"admin.loading_description": "Attendi il caricamento della pagina.",
"admin.toggle_sidebar": "Attiva/disattiva barra laterale",
"admin.shout": "Comunicazione",
"admin_shout.online_one": "Attualmente ci sono {{count}} utenti online",
"admin_shout.online_other": "Attualmente ci sono {{count}} utenti online",
"admin_shout.sticky_toggle": "Cambiare il messaggio adesivo",
"admin_login.title": "Etherpad",
"admin_login.username": "Nome utente",
"admin_login.password": "Password",
"admin_login.submit": "Accedi",
"admin_login.failed": "Accesso non riuscito!",
"admin_pads.all_pads": "Tutti i Pads",
"admin_pads.bulk.cleanup_history": "Pulizia della cronologia",
"admin_pads.bulk.clear_selection": "Cancella selezione",
"admin_pads.bulk.delete": "Elimina",
"admin_pads.cancel": "Annulla",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Revisioni",
"admin_pads.col.users": "Utenti",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Scegli un nome per il nuovo Pad.",
"admin_pads.delete_pad_dialog_description": "Conferma o annulla la cancellazione del Pad.",
"admin_pads.delete_pad_dialog_title": "Cancella Pad",
"admin_pads.empty_never_edited": "vuoto · mai modificato",
"admin_pads.error_dialog_description": "Si è verificato un errore.",
"admin_pads.error_prefix": "Errore",
"admin_pads.filter.active": "Attivo",
"admin_pads.filter.all": "Tutto",
"admin_pads.filter.empty": "Vuoto",
"admin_pads.filter.recent": "Questa settimana",
"admin_pads.filter.stale": "Stale (>1y)",
"admin_pads.open": "Aperto",
"admin_pads.pagination.next": "Successivo",
"admin_pads.pagination.previous": "Precedente",
"admin_pads.refresh": "Aggiorna",
"admin_pads.relative.days": "{{count}}d fa",
"admin_pads.relative.hours": "{{count}} ore fa",
"admin_pads.relative.just_now": "proprio ora",
"admin_pads.relative.minutes": "{{count}}m fa",
"admin_pads.relative.months": "{{count}}mese fa",
"admin_pads.relative.weeks": "{{count}} settimane fa",
"admin_pads.relative.years": "{{count}} anni fa",
"admin_pads.revisions_count": "{{count}} revisioni",
"admin_pads.selected_count": "{{count}} selezionati",
"admin_pads.show": "Mostra",
"admin_pads.sort.name": "Nome (AZ)",
"admin_pads.sort.revision_number": "Revisioni",
"admin_pads.sort.user_count": "Utenti",
"admin_pads.stats.across_pads": "su tutti i pad",
"admin_pads.stats.active_users": "Utenti attivi",
"admin_pads.stats.empty_pads": "Pads vuoti",
"admin_pads.stats.last_activity": "Ultima attività",
"admin_pads.stats.no_active_users": "Nessun utente attivo",
"admin_pads.stats.revisions_zero": "0 revisioni",
"admin_pads.stats.total": "Pads totali",
"admin_pads.stats.users_active": "{{count}} attualmente attivi",
"admin_pads.subtitle": "Panoramica di tutti i pad su questa istanza di Etherpad. Cerca, pulisci, apri.",
"admin_plugins": "Gestione plugin",
"admin_plugins.available": "Plugin disponibili",
"admin_plugins.available_not-found": "Nessun plugin trovato.",
"admin_plugins.available_fetching": "Recupero in corso…",
"admin_plugins.available_install.value": "Installa",
"admin_plugins.available_search.placeholder": "Cerca i plugin da installare",
"admin_plugins.check_updates": "Verifica la presenza di aggiornamenti",
"admin_plugins.core_count": "{{count}} core",
"admin_plugins.catalog_disabled": "Il catalogo dei plugin è disabilitato dal tuo operatore (privacy.pluginCatalog=false). Per installare un plugin, esegui `pnpm run plugins i ep_<name> dal server.",
"admin_plugins.crumbs": "Plugin",
"admin_plugins.description": "Descrizione",
"admin_plugins.disables.label": "Disabilitati:",
"admin_plugins.disables.warning_title": "Questo plugin rimuove intenzionalmente le funzionalità di Etherpad elencate.",
"admin_plugins.error_retrieving": "Errore durante il recupero dei plugin",
"admin_plugins.install_error": "Impossibile installare {{plugin}}: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Impossibile installare {{plugin}}: richiede una versione più recente di Etherpad. Aggiorna Etherpad e riprova.",
"admin_plugins.installed": "Plugin installati",
"admin_plugins.installed_fetching": "Recupero dei plugin installati…",
"admin_plugins.installed_nothing": "Non hai ancora installato alcun plugin.",
@ -29,27 +96,120 @@
"admin_plugins.last-update": "Ultimo aggiornamento",
"admin_plugins.name": "Nome",
"admin_plugins.page-title": "Gestore dei plugin - Etherpad",
"admin_plugins.reload_catalog": "Ricarica il catalogo",
"admin_plugins.search_npm": "Cerca su npm",
"admin_plugins.sort_ascending": "Ordinamento crescente",
"admin_plugins.sort_descending": "Ordinamento decrescente",
"admin_plugins.sort.last_updated": "Ultimo aggiornamento",
"admin_plugins.sort.name": "Nome (AZ)",
"admin_plugins.sort.version": "Versione",
"admin_plugins.source": "Fonte del plugin",
"admin_plugins.subtitle": "Installa, aggiorna e rimuovi i plugin di Etherpad. Le modifiche richiedono il riavvio del server.",
"admin_plugins.tag_core": "Core",
"admin_plugins.update_tooltip": "Aggiornamento",
"admin_plugins.updates_available": "Aggiornamenti disponibili",
"admin_plugins.update_now": "Aggiorna",
"admin_plugins.version": "Versione",
"admin_plugins_info": "Informazioni sulla risoluzione dei problemi",
"admin_plugins_info.bindings_label": "{{count}} bindings",
"admin_plugins_info.copy_diagnostics": "Copia diagnostica",
"admin_plugins_info.copy_value": "Copia {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hook_bindings": "Hook bindings",
"admin_plugins_info.hooks": "Hook installati",
"admin_plugins_info.hooks_client": "Hook lato client",
"admin_plugins_info.hooks_server": "Hook lato server",
"admin_plugins_info.no_hooks": "Nessun hooks trovato",
"admin_plugins_info.parts": "Parti installate",
"admin_plugins_info.plugins": "Plugin installati",
"admin_plugins_info.page-title": "Informazioni sul plugin - Etherpad",
"admin_plugins_info.search_placeholder": "Cerca hook o parte…",
"admin_plugins_info.subtitle": "Diagnostica di sistema: versione installata, componenti registrati e hook.",
"admin_plugins_info.tab_client": "Client",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aggiornato",
"admin_plugins_info.update_available": "Aggiornamento disponibile: {{version}}",
"admin_plugins_info.version": "Versione di Etherpad",
"admin_plugins_info.version_latest": "Ultima versione disponibile",
"admin_plugins_info.version_number": "Numero di versione",
"admin_settings": "Impostazioni",
"admin_settings.create_pad": "Crea pad",
"admin_settings.current": "Configurazione attuale",
"admin_settings.current_example-devel": "Esempio di modello di impostazioni di sviluppo",
"admin_settings.current_example-prod": "Esempio di modello di impostazioni di produzione",
"admin_settings.current_restart.value": "Riavvia Etherpad",
"admin_settings.current_save.value": "Salva impostazioni",
"admin_settings.invalid_json": "JSON non valido",
"admin_settings.current_test.value": "Validare JSON",
"admin_settings.toast.saved": "Impostazioni salvate correttamente.",
"admin_settings.toast.save_failed": "Salvataggio non riuscito: impossibile scrivere il file settings.json.",
"admin_settings.toast.json_invalid": "Errore di sintassi: verificare virgole, parentesi graffe e virgolette.",
"admin_settings.toast.disconnected": "Impossibile salvare: connessione al server non riuscita.",
"admin_settings.toast.validation_ok": "Il formato JSON è valido.",
"admin_settings.toast.validation_failed": "Il formato JSON non è valido: correggere gli errori di sintassi.",
"admin_settings.mode.form": "Modulo",
"admin_settings.mode.aria_label": "Modalità editor",
"admin_settings.section.general": "Generale",
"admin_settings.parse_error.title": "Impossibile analizzare il file settings.json",
"admin_settings.parse_error.cta": "Passa al formato raw per modificare",
"admin_settings.env_pill.tooltip": "Legge dalla variabile d'ambiente {{variable}}. Il valore seguente viene utilizzato quando {{variable}} non è impostata.",
"admin_settings.env_pill.default_label": "predefinito",
"admin_settings.env_pill.input_aria": "Valore predefinito per {{variable}}",
"admin_settings.env_pill.runtime_label": "valore attivo",
"admin_settings.env_pill.runtime_tooltip": "Etherpad sta attualmente utilizzando questo valore, risolto da {{variable}} o dal suo valore predefinito.",
"admin_settings.env_pill.redacted_tooltip": "Etherpad sta utilizzando un valore per {{variable}}, ma è nascosto perché è un segreto.",
"admin_settings.page-title": "Impostazioni - Etherpad",
"admin_settings.save_error": "Impostazioni di salvataggio degli errori",
"admin_settings.saved_success": "Impostazioni salvate con successo",
"update.banner.title": "Aggiornamento disponibile",
"update.banner.body": "Etherpad {{latest}} è disponibile (stai eseguendo {{current}}).",
"update.banner.cta": "Visualizza aggiornamento",
"update.page.title": "Aggiornamenti di Etherpad",
"update.page.current": "Versione attuale",
"update.page.latest": "Ultima versione",
"update.page.last_check": "Ultimo controllo effettuato",
"update.page.install_method": "Metodo di installazione",
"update.page.changelog": "Changelog",
"update.page.up_to_date": "Stiamo utilizzando l'ultima versione.",
"update.page.disabled": "I controlli degli aggiornamenti sono disabilitati (updates.tier = \"off\").",
"update.page.unauthorized": "Non sei autorizzato a visualizzare lo stato degli aggiornamenti.",
"update.page.error": "Impossibile caricare lo stato dell'aggiornamento (stato {{status}}).",
"update.badge.severe": "Etherpad su questo server è gravemente obsoleto. Segnalalo all'amministratore.",
"update.badge.vulnerable": "Su questo server è in esecuzione una versione di Etherpad con vulnerabilità di sicurezza note. Informa l'amministratore.",
"update.page.apply": "Applica aggiornamento",
"update.page.cancel": "Annulla",
"update.page.log": "Registro dei log (ultime 200 righe)",
"update.page.execution": "Stato",
"update.page.policy.install-method-not-writable": "Gli aggiornamenti dall'interfaccia di amministrazione richiedono un'installazione da Git. Aggiorna tramite il tuo gestore di pacchetti.",
"update.page.policy.rollback-failed-terminal": "Un precedente aggiornamento non è riuscito e non è stato possibile annullarlo. Premi Conferma al termine dell'installazione per rimuovere il blocco.",
"update.page.policy.up-to-date": "Stiamo utilizzando l'ultima versione.",
"update.page.policy.tier-off": "Gli aggiornamenti sono disabilitati (updates.tier = \"off\").",
"update.page.policy.maintenance-window-missing": "Il livello 4 (autonomo) richiede una finestra di manutenzione. Imposta updates.maintenanceWindow in settings.json per abilitare gli aggiornamenti autonomi.",
"update.page.last_result.verified": "Ultimo aggiornamento a {{tag}} verificato.",
"update.page.last_result.rolled-back": "Ultimo tentativo di aggiornamento di {{tag}} annullato: {{reason}}.",
"update.page.last_result.rollback-failed": "Ultimo tentativo di aggiornamento non riuscito E rollback non riuscito: {{reason}}. Intervento manuale necessario.",
"update.page.last_result.cancelled": "Ultimo tentativo di aggiornamento di {{tag}} annullato dall'amministratore.",
"update.execution.idle": "Idle",
"update.execution.scheduled": "Aggiornamento programmato",
"update.execution.executing": "Aggiornamento...",
"update.execution.pending-verification": "In attesa di verifica",
"update.execution.verified": "Verificato",
"update.execution.rolling-back": "Rollback",
"update.execution.rolled-back": "Rolled back",
"update.execution.rollback-failed": "Rollback fallito",
"update.banner.terminal.rollback-failed": "Il tentativo di aggiornamento non è riuscito e non è stato possibile annullarlo. È necessario un intervento manuale.",
"update.banner.scheduled": "Aggiornamento automatico programmato per {{tag}} — si applica tra {{remaining}}.",
"update.banner.maintenance-window-missing": "Gli aggiornamenti automatici sono disabilitati fino a quando non viene configurata una finestra di manutenzione.",
"update.page.scheduled.title": "Aggiornamento programmato",
"update.page.scheduled.countdown": "Etherpad inizierà l'aggiornamento a {{tag}} in {{remaining}}.",
"update.page.scheduled.deferred_until": "Fuori dalla finestra di manutenzione. L'aggiornamento inizierà all'apertura della finestra alle {{at}}.",
"update.page.scheduled.apply_now": "Applica ora",
"update.window.title": "Finestra di manutenzione",
"update.window.unset": "Non configurato.",
"update.window.next_opens_at": "La prossima finestra si aprirà alle {{at}}.",
"update.drain.t60": "Etherpad si riavvierà tra 60 secondi per applicare un aggiornamento.",
"update.drain.t30": "Etherpad si riavvierà tra 30 secondi per applicare un aggiornamento.",
"update.drain.t10": "Etherpad si riavvierà tra 10 secondi per applicare un aggiornamento.",
"index.newPad": "Nuovo pad",
"index.settings": "Impostazioni",
"index.transferSessionTitle": "Sessione di trasferimento",
@ -62,6 +222,7 @@
"index.copyLinkButton": "Copia link negli appunti",
"index.transferToSystem": "3. Copia la sessione sul nuovo sistema",
"index.transferToSystemDescription": "Apri il collegamento copiato nel browser o nel dispositivo di destinazione per trasferire la sessione",
"index.code": "Codice",
"index.transferSessionDescription": "Trasferisci la tua sessione corrente al browser o al dispositivo cliccando sul pulsante qui sotto. Verrà copiato un link a una pagina che trasferirà la tua sessione quando verrà aperta nel browser o dispositivo di destinazione.",
"index.createOpenPad": "Apri pad per nome",
"index.openPad": "apri un Pad esistente col nome:",
@ -111,8 +272,18 @@
"pad.settings.fontType": "Tipo di carattere:",
"pad.settings.fontType.normal": "Normale",
"pad.settings.language": "Lingua:",
"pad.settings.deletePad": "Elimina Pad",
"pad.settings.deletePad": "Cancella Pad",
"pad.delete.confirm": "Vuoi veramente cancellare questo pad?",
"pad.deletionToken.modalTitle": "Salva il token di eliminazione del tuo pad",
"pad.deletionToken.modalBody": "Questo token è l'unico modo per eliminare questo pad se perdi la sessione del browser o il dispositivo di commutazione. Conservalo in un luogo sicuro - qui viene mostrato esattamente solo una volta.",
"pad.deletionToken.copy": "Copia",
"pad.deletionToken.copied": "Copiato",
"pad.deletionToken.acknowledge": "L'ho salvato",
"pad.deletionToken.deleteWithToken": "Cancellare il pad con il token",
"pad.deletionToken.tokenFieldLabel": "Token di eliminazione del pad",
"pad.deletionToken.tokenValueLabel": "Il tuo token di cancellazione del pad (solo-lettura)",
"pad.deletionToken.invalid": "Quel token non è valido per questo pad.",
"pad.deletionToken.notCreator": "Non sei tu il creatore di questo pad, quindi non puoi eliminarlo.",
"pad.settings.about": "Informazioni",
"pad.settings.poweredBy": "Realizzato con",
"pad.importExport.import_export": "Importazione/esportazione",
@ -125,6 +296,13 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Esporta come Etherpad",
"pad.importExport.exporthtmla.title": "Esporta come HTML",
"pad.importExport.exportplaina.title": "Esporta come testo semplice",
"pad.importExport.exportworda.title": "Esporta come Microsoft Word",
"pad.importExport.exportpdfa.title": "Esporta in formato PDF",
"pad.importExport.exportopena.title": "Esporta in formato ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "È possibile importare direttamente file di testo semplice, HTML, Microsoft Word (.docx) e file Etherpad. Per importare altri formati come PDF, ODT, DOC o RTF, l'amministratore del server deve installare LibreOffice: consulta la <a href=\"https://docs.etherpad.org/\">documentazione di Etherpad</a>.",
"pad.modals.connected": "Connesso.",
"pad.modals.reconnecting": "Riconnessione al pad in corso…",
"pad.modals.forcereconnect": "Forza la riconnessione",
@ -171,16 +349,25 @@
"timeslider.toolbar.returnbutton": "Ritorna al Pad",
"pad.historyMode.banner": "Visualizza cronologia",
"pad.historyMode.revisionLabel": "Versione {{rev}}",
"pad.historyMode.controlsLabel": "Controlli della cronologia del pad",
"pad.historyMode.sliderLabel": "Versione Pad",
"pad.historyMode.settings.follow": "Segui gli aggiornamenti dei contenuti del pad",
"pad.historyMode.settings.followShort": "Segui",
"pad.historyMode.settings.playbackSpeed": "Velocità di riproduzione:",
"pad.historyMode.chat.replayHeader": "Chat del {{time}}",
"pad.historyMode.users.authorsHeader": "Autori in questa versione",
"pad.editor.skipToContent": "Vai all'editor",
"pad.editor.keyboardHint": "Premi Esc per uscire dall'editor. Premi Alt+F9 per accedere alla barra degli strumenti.",
"pad.editor.toolbar.formatting": "Strumento di formattazione",
"pad.editor.toolbar.actions": "Barra degli strumenti delle azioni del pad",
"pad.editor.toolbar.showMore": "Mostra altri pulsanti della barra degli strumenti",
"timeslider.toolbar.authors": "Autori:",
"timeslider.toolbar.authorsList": "Nessun autore",
"timeslider.toolbar.exportlink.title": "Esporta",
"timeslider.exportCurrent": "Esporta la versione corrente come:",
"timeslider.version": "Versione {{version}}",
"timeslider.saved": "Salvato {{day}} {{month}} {{year}}",
"timeslider.settings.playbackSpeed": "Velocità di riproduzione:",
"timeslider.settings.playbackSpeed.original": "Velocità originale",
"timeslider.settings.playbackSpeed.realtime": "In tempo reale",
"timeslider.settings.playbackSpeed.200ms": "200 ms",
@ -207,6 +394,7 @@
"pad.savedrevs.timeslider": "Puoi vedere le versioni salvate visitando la cronologia",
"pad.userlist.entername": "Inserisci il tuo nome",
"pad.userlist.unnamed": "senza nome",
"pad.userlist.onlineCount": "{[ plurale(count) uno: {{count}} utente connesso, altro: {{count}} utenti connessi ]}",
"pad.editbar.clearcolors": "Eliminare i colori degli autori sull'intero documento? Questa azione non può essere annullata",
"pad.impexp.importbutton": "Importa ora",
"pad.impexp.importing": "Importazione in corso...",
@ -217,5 +405,6 @@
"pad.impexp.importfailed": "Importazione fallita",
"pad.impexp.copypaste": "Si prega di copiare e incollare",
"pad.impexp.exportdisabled": "L'esportazione come {{type}} è disabilitata. Contattare l'amministratore per i dettagli.",
"pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione"
"pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione",
"pad.social.description": "Un documento collaborativo che tutti possono modificare in tempo reale."
}

View file

@ -6,6 +6,7 @@
"Chqaz",
"Omotecho",
"Shirayuki",
"Tensama0415",
"Torinky"
]
},
@ -30,7 +31,7 @@
"admin_plugins_info.hooks_client": "クライアント側のフック",
"admin_plugins_info.hooks_server": "サーバー側のフック",
"index.newPad": "新規作成",
"index.createOpenPad": "または作成/編集するパッド名を入力:",
"index.createOpenPad": "編集するパッド名を入力",
"index.openPad": "次の名称の既存の Pad を開く:",
"pad.toolbar.bold.title": "太字 (Ctrl+B)",
"pad.toolbar.italic.title": "斜体 (Ctrl+I)",
@ -39,7 +40,7 @@
"pad.toolbar.ol.title": "番号付きリスト (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "番号なしリスト (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "インデント (Tab)",
"pad.toolbar.unindent.title": "インデント解除 (Shift+Tab)",
"pad.toolbar.unindent.title": "アウトデント (Shift+Tab)",
"pad.toolbar.undo.title": "元に戻す (Ctrl+Z)",
"pad.toolbar.redo.title": "やり直し (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "作者の色分けを消去(Ctrl+Shift+C)",
@ -54,7 +55,7 @@
"pad.loading": "読み込み中...",
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. \n\nクッキーが見つかりません。ブラウザの設定でクッキーの使用を許可するまで、アクセスの記録や設定は引き継がれません。原因はブラウザによって Etherpad が iFrame に組み込まれたからと考えられます。親ドメインの iFrame と同じドメイン/サブドメインに置かれているかどうか、Etherpad の設定を確認してください。",
"pad.permissionDenied": "あなたにはこのパッドへのアクセス許可がありません",
"pad.settings.padSettings": "パッドの設定",
"pad.settings.padSettings": "パッドの設定",
"pad.settings.myView": "個人設定",
"pad.settings.stickychat": "画面にチャットを常に表示",
"pad.settings.chatandusers": "チャットとユーザーを表示",
@ -77,7 +78,7 @@
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.modals.connected": "接続されました。",
"pad.modals.reconnecting": "パッドに再接続中...",
"pad.modals.reconnecting": "パッドに再接続中",
"pad.modals.forcereconnect": "強制的に再接続",
"pad.modals.reconnecttimer": "再接続を試行中",
"pad.modals.cancel": "中止",

View file

@ -19,13 +19,80 @@
]
},
"admin.page-title": "관리 대시보드 - 이더패드",
"admin.loading": "불러오는 중…",
"admin.loading_description": "페이지가 로딩되는 동안 잠시 기다려 주세요.",
"admin.toggle_sidebar": "사이드바 토글",
"admin.shout": "커뮤니케이션",
"admin_shout.online_one": "현재 {{count}}명의 사용자가 온라인 상태입니다",
"admin_shout.online_other": "현재 {{count}}명의 사용자가 온라인 상태입니다",
"admin_shout.sticky_toggle": "고정 메시지 변경",
"admin_login.title": "이더패드",
"admin_login.username": "사용자 이름",
"admin_login.password": "비밀번호",
"admin_login.submit": "로그인",
"admin_login.failed": "로그인 실패",
"admin_pads.all_pads": "모든 패드",
"admin_pads.bulk.cleanup_history": "역사 지우기",
"admin_pads.bulk.clear_selection": "선택 항목 지우기",
"admin_pads.bulk.delete": "삭제",
"admin_pads.cancel": "취소",
"admin_pads.col.pad": "패드",
"admin_pads.col.revisions": "판",
"admin_pads.col.users": "사용자",
"admin_pads.confirm_button": "확인",
"admin_pads.create_pad_dialog_description": "새 패드의 이름을 선택하세요.",
"admin_pads.delete_pad_dialog_description": "패드 삭제를 확인하거나 취소하세요.",
"admin_pads.delete_pad_dialog_title": "패드 삭제",
"admin_pads.empty_never_edited": "비었음 · 편집된 적 없음",
"admin_pads.error_dialog_description": "오류가 발생했습니다.",
"admin_pads.error_prefix": "오류",
"admin_pads.filter.active": "활성",
"admin_pads.filter.all": "모두",
"admin_pads.filter.empty": "비었음",
"admin_pads.filter.recent": "이번 주",
"admin_pads.filter.stale": "오래됨 (>1년)",
"admin_pads.open": "열기",
"admin_pads.pagination.next": "다음",
"admin_pads.pagination.previous": "이전",
"admin_pads.refresh": "새로 고침",
"admin_pads.relative.days": "{{count}}일 전",
"admin_pads.relative.hours": "{{count}}시간 전",
"admin_pads.relative.just_now": "방금",
"admin_pads.relative.minutes": "{{count}}분 전",
"admin_pads.relative.months": "{{count}}달 전",
"admin_pads.relative.weeks": "{{count}}주 전",
"admin_pads.relative.years": "{{count}}년 전",
"admin_pads.revisions_count": "수정 내역 {{count}}개",
"admin_pads.selected_count": "{{count}}개 선택됨",
"admin_pads.show": "보기",
"admin_pads.sort.name": "이름 (A-Z)",
"admin_pads.sort.revision_number": "수정내역",
"admin_pads.sort.user_count": "사용자",
"admin_pads.stats.across_pads": "모든 패드에 걸쳐서",
"admin_pads.stats.active_users": "활동적인 사용자",
"admin_pads.stats.empty_pads": "빈 패드",
"admin_pads.stats.last_activity": "마지막 활동",
"admin_pads.stats.no_active_users": "활동적인 사용자 없음",
"admin_pads.stats.revisions_zero": "수정 내역 없음",
"admin_pads.stats.total": "총 패드",
"admin_pads.stats.users_active": "{{count}}명 현재 활동중",
"admin_pads.subtitle": "이 이더패드 인스턴스의 모든 패드 개요입니다. 검색, 정리, 열기를 할 수 있습니다.",
"admin_plugins": "플러그인 관리자",
"admin_plugins.available": "사용 가능한 플러그인",
"admin_plugins.available_not-found": "플러그인이 없습니다.",
"admin_plugins.available_fetching": "검색 중...",
"admin_plugins.available_install.value": "설치",
"admin_plugins.available_search.placeholder": "설치할 플러그인을 검색",
"admin_plugins.check_updates": "업데이트 확인",
"admin_plugins.core_count": "코어 {{count}}개",
"admin_plugins.catalog_disabled": "운영자가 플러그인 카탈로그를 비활성화했습니다(privacy.pluginCatalog=false). 플러그인을 설치하려면 서버에서 `pnpm run plugins i ep_<name>`을 실행하세요.",
"admin_plugins.crumbs": "플러그인",
"admin_plugins.description": "설명",
"admin_plugins.disables.label": "비활성화:",
"admin_plugins.disables.warning_title": "이 플러그인은 나열된 이더패드 기능을 의도적으로 제거합니다.",
"admin_plugins.error_retrieving": "플러그인을 가져오는 중 오류 발생",
"admin_plugins.install_error": "{{plugin}} 설치 실패: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "{{plugin}}을 설치할 수 없습니다. 더 새로운 버전의 이더패드가 필요합니다. 이더패드를 업그레이드한 뒤 다시 시도하세요.",
"admin_plugins.installed": "설치된 플러그인",
"admin_plugins.installed_fetching": "설치된 플러그인을 검색하는 중...",
"admin_plugins.installed_nothing": "아직 플러인을 설치하지 않으셨습니다.",
@ -33,50 +100,161 @@
"admin_plugins.last-update": "마지막 업데이트",
"admin_plugins.name": "이름",
"admin_plugins.page-title": "플러그인 관리자 - 이더패드",
"admin_plugins.reload_catalog": "카탈로그 새로고침",
"admin_plugins.search_npm": "npm에서 검색",
"admin_plugins.sort_ascending": "오름차순 정렬",
"admin_plugins.sort_descending": "내림차순 정렬",
"admin_plugins.sort.last_updated": "최근 업데이트",
"admin_plugins.sort.name": "이름 (A-Z)",
"admin_plugins.sort.version": "버전",
"admin_plugins.source": "플러그인 소스",
"admin_plugins.subtitle": "이더패드 플러그인을 설치, 업데이트 및 제거합니다. 변경 사항을 적용하려면 서버를 다시 시작해야 합니다.",
"admin_plugins.tag_core": "코어",
"admin_plugins.update_tooltip": "업데이트",
"admin_plugins.updates_available": "업데이트가 있습니다",
"admin_plugins.update_now": "업데이트",
"admin_plugins.version": "버전",
"admin_plugins_info": "문제 해결 정보",
"admin_plugins_info.bindings_label": "바인딩 {{count}}개",
"admin_plugins_info.copy_diagnostics": "진단 복사",
"admin_plugins_info.copy_value": "{{label}} 복사",
"admin_plugins_info.git_sha": "깃 SHA",
"admin_plugins_info.hook_bindings": "후크 바인딩",
"admin_plugins_info.hooks": "설치된 훅",
"admin_plugins_info.hooks_client": "클라이언트 사이드 훅",
"admin_plugins_info.hooks_server": "서버사이드 훅",
"admin_plugins_info.no_hooks": "훅이 없습니다",
"admin_plugins_info.parts": "설치된 항목",
"admin_plugins_info.plugins": "설치된 플러그인",
"admin_plugins_info.page-title": "플러그인 정보 - 이더패드",
"admin_plugins_info.search_placeholder": "후크 또는 파츠 검색…",
"admin_plugins_info.subtitle": "시스템 진단: 설치된 버전, 등록된 파츠 및 후크입니다.",
"admin_plugins_info.tab_client": "클라이언트",
"admin_plugins_info.tab_server": "서버",
"admin_plugins_info.up_to_date": "현재 최신 버전",
"admin_plugins_info.update_available": "업데이트 가능: {{version}}",
"admin_plugins_info.version": "이더패드 버전",
"admin_plugins_info.version_latest": "사용 가능한 최신 버전",
"admin_plugins_info.version_number": "버전 번호",
"admin_settings": "설정",
"admin_settings.create_pad": "패드 만들기",
"admin_settings.current": "현재 구성",
"admin_settings.current_example-devel": "예시 개발용 설정 틀",
"admin_settings.current_example-prod": "예시 운영용 설정 틀",
"admin_settings.current_restart.value": "이더패드 다시 시작",
"admin_settings.current_save.value": "설정 저장",
"admin_settings.invalid_json": "잘못된 JSON",
"admin_settings.current_test.value": "JSON 검증",
"admin_settings.current_prettify.value": "JSON를 보기 좋게 정리",
"admin_settings.toast.saved": "설정을 성공적으로 저장했습니다.",
"admin_settings.toast.save_failed": "저장 실패: settings.json에 쓰기 할 수 없습니다.",
"admin_settings.toast.json_invalid": "구문 오류: 쉼표, 중괄호, 따옴표를 확인하세요.",
"admin_settings.toast.disconnected": "저장할 수 없음: 서버에 연결되어 있지 않습니다.",
"admin_settings.toast.validation_ok": "JSON이 유효합니다.",
"admin_settings.toast.validation_failed": "JSON이 유효하지 않습니다. 구문 오류를 수정하세요.",
"admin_settings.toast.prettify_failed": "보기 좋게 정리할 수 없습니다. 먼저 구문 오류를 수정하세요.",
"admin_settings.prettify_confirm": "정리하면 모든 의견이 제거됩니다. 계속하시겠습니까?",
"admin_settings.mode.form": "형태",
"admin_settings.mode.raw": "원본",
"admin_settings.mode.effective": "효과 적용 중",
"admin_settings.mode.effective_tooltip": "환경 변수 대체 후 이더패드가 현재 실제로 사용 중인 값을 읽기 전용으로 보여줍니다. 비밀 정보는 가려졌습니다.",
"admin_settings.mode.aria_label": "편집기 모드",
"admin_settings.envvar_banner.title": "이 파일은 틀이며, 라이브 설정이 아닙니다.",
"admin_settings.envvar_banner.body": "${VAR:default}와 같은 자리 표시자는 시작 시 메모리에 대체되며, 이 파일에는 다시 기록되지 않습니다. 환경 변수(Docker compose, systemd, .env)를 편집하여 확인된 값을 변경하거나, 여기의 자리 표시자를 리터럴 값으로 바꾸세요. 현재 이더패드에서 사용 중인 값을 확인하려면 '효과 적용 중' 탭으로 이동하세요.",
"admin_settings.toast.auth_error": "관리자 계정으로 인증되지 않았습니다. 다시 로그인해 주세요.",
"admin_settings.section.general": "일반",
"admin_settings.parse_error.title": "settings.json을 구문 분석할 수 없습니다",
"admin_settings.parse_error.cta": "원본 편집으로 전환",
"admin_settings.env_pill.tooltip": "{{variable}} 환경 변수에서 읽습니다. {{variable}}이 설정되지 않은 경우 아래 값이 사용됩니다.",
"admin_settings.env_pill.default_label": "기본값",
"admin_settings.env_pill.input_aria": "{{variable}}의 기본값",
"admin_settings.env_pill.runtime_label": "활성 값",
"admin_settings.env_pill.runtime_tooltip": "이더패드는 현재 {{variable}} 또는 해당 기본값에서 확인된 이 값을 사용하고 있습니다.",
"admin_settings.env_pill.redacted_tooltip": "이더패드가 {{variable}}의 값을 사용하고 있지만, 비밀 값이므로 숨겨져 있습니다.",
"admin_settings.page-title": "설정 - 이더패드",
"admin_settings.save_error": "설정 저장 오류",
"admin_settings.saved_success": "설정이 성공적으로 저장되었습니다",
"update.banner.title": "업데이트 사용 가능",
"update.banner.body": "이더패드 {{latest}}을 사용할 수 있습니다(현재 {{current}} 실행 중).",
"update.banner.cta": "업데이트 보기",
"update.page.title": "이더패드 업데이트",
"update.page.current": "현재 판",
"update.page.latest": "최신 버전",
"update.page.last_check": "마지막으로 확인함",
"update.page.install_method": "설치 방법",
"update.page.tier": "업데이트 티어",
"update.page.changelog": "바뀐기록",
"update.page.up_to_date": "최신 버전을 사용 중입니다.",
"update.page.disabled": "업데이트 확인이 비활성화되어 있습니다(updates.tier = \"off\").",
"update.page.unauthorized": "업데이트 상태를 볼 권한이 없습니다.",
"update.page.error": "업데이트 상태를 불러올 수 없습니다(상태 {{status}}).",
"update.badge.severe": "이 서버의 이더패드가 심각하게 오래되었습니다. 관리자에게 알리세요.",
"update.badge.vulnerable": "이 서버의 이더패드는 알려진 보안 문제가 있는 버전을 실행하고 있습니다. 관리자에게 알리세요.",
"update.page.apply": "업데이트 적용",
"update.page.cancel": "취소",
"update.page.acknowledge": "확인",
"update.page.log": "업데이트 로그 (최근 200줄)",
"update.page.execution": "상태",
"update.page.policy.install-method-not-writable": "관리자 UI에서 업데이트하려면 Git 설치가 필요합니다. 패키지 관리자를 통해 업데이트하세요.",
"update.page.policy.rollback-failed-terminal": "이전 업데이트가 실패했으며 롤백할 수 없었습니다. 설치가 정상화된 뒤 확인을 눌러 잠금을 해제하세요.",
"update.page.policy.up-to-date": "최신 버전을 사용 중입니다.",
"update.page.policy.tier-off": "업데이트가 비활성화되었습니다 (updates.tier = \"off\").",
"update.page.policy.maintenance-window-missing": "계층 4(자율)는 유지보수 기간이 필요합니다. 자율 업데이트를 활성화하려면 settings.json에서 updates.maintenanceWindow를 설정하세요.",
"update.page.policy.maintenance-window-invalid": "updates.maintenanceWindow 형식이 잘못되어 계층 4(자율)가 비활성화되었습니다. HH:MM 시간과 \"local\" 또는 \"utc\"인 tz를 포함한 {start, end, tz} 형식이 필요합니다.",
"update.page.last_result.verified": "{{tag}}의 마지막 업데이트가 확인되었습니다.",
"update.page.last_result.rolled-back": "{{tag}}에 대한 마지막 업데이트 시도가 롤백되었습니다: {{reason}}.",
"update.page.last_result.rollback-failed": "마지막 업데이트 시도가 실패했고 롤백도 실패했습니다: {{reason}}. 수동 개입이 필요합니다.",
"update.page.last_result.preflight-failed": "{{tag}}에 대한 마지막 업데이트 시도가 사전 검사에서 실패했습니다: {{reason}}.",
"update.page.last_result.cancelled": "관리자에 의해 {{tag}}에 대한 마지막 업데이트 시도가 취소되었습니다.",
"update.execution.idle": "유휴 상태",
"update.execution.scheduled": "예정된 업데이트",
"update.execution.preflight": "비행 전 점검",
"update.execution.preflight-failed": "비행 전 점검 실패",
"update.execution.draining": "세션 정리 중",
"update.execution.executing": "업데이트 중...",
"update.execution.pending-verification": "확인 대기 중",
"update.execution.verified": "확인됨",
"update.execution.rolling-back": "되돌리는 중",
"update.execution.rolled-back": "되돌려짐",
"update.execution.rollback-failed": "되돌리기 실패",
"update.banner.terminal.rollback-failed": "업데이트 시도가 실패했으며 롤백할 수 없습니다. 수동 조치가 필요합니다.",
"update.banner.scheduled": "{{tag}}에 대한 자동 업데이트가 예약되었습니다. {{remaining}}에 적용됩니다.",
"update.banner.maintenance-window-missing": "유지 관리 기간이 설정될 때까지 자동 업데이트가 비활성화됩니다.",
"update.banner.maintenance-window-invalid": "유지 관리 기간이 잘못 설정되어 있어 자동 업데이트가 비활성화되었습니다.",
"update.page.scheduled.title": "예정된 업데이트",
"update.page.scheduled.countdown": "이더패드는 {{remaining}} 후에 {{tag}}(으)로의 업데이트를 시작합니다.",
"update.page.scheduled.deferred_until": "유지 보수 시간 외입니다. 업데이트는 유지 보수가 시작되는 {{at}}에 시작됩니다.",
"update.page.scheduled.apply_now": "지금 적용",
"update.window.title": "유지보수 창",
"update.window.summary": "{{start}}{{end}} ({{tz}})",
"update.window.unset": "구성되지 않았습니다.",
"update.window.next_opens_at": "다음 창은 {{at}}에 열립니다.",
"update.drain.t60": "업데이트 적용을 위해 이더패드가 60초 뒤에 재시작됩니다.",
"update.drain.t30": "업데이트 적용을 위해 이더패드가 30초 뒤에 재시작됩니다.",
"update.drain.t10": "업데이트 적용을 위해 이더패드가 10초 뒤에 재시작됩니다.",
"index.newPad": "새 패드",
"index.settings": "설정",
"index.transferSessionTitle": "전송 세션",
"index.receiveSessionTitle": "수신 세션",
"index.receiveSessionDescription": "여기에서 다른 브라우저나 기기에서 이더패드 세션을 수신할 수 있습니다. 단, 이렇게 하면 현재 세션이 삭제되므로 주의하세요.",
"index.transferSession": "1. 전송 세션",
"index.transferSessionNow": "전송 세션을 지금 시작",
"index.copyLink": "2. 링크 복사",
"index.copyLinkDescription": "아래 버튼을 클릭하여 링크를 클립보드에 복사하세요.",
"index.copyLinkButton": "클립보드에 링크 복사하기",
"index.transferToSystem": "3. 세션을 새 시스템으로 복사",
"index.transferToSystemDescription": "복사한 링크를 대상 브라우저 또는 기기에서 열어 세션을 전송하세요.",
"index.code": "코드",
"index.transferSessionDescription": "아래 버튼을 클릭하여 현재 세션을 브라우저 또는 기기로 전송하세요. 이 버튼을 클릭하면 대상 브라우저 또는 기기에서 열었을 때 세션이 전송될 문서 링크가 복사됩니다.",
"index.createOpenPad": "이름으로 패드 열기",
"index.openPad": "이름으로 기존 패드 열기:",
"index.recentPads": "최근 패드",
"index.recentPadsEmpty": "최근 패드가 없습니다.",
"index.generateNewPad": "무작위 패드 이름 생성",
"index.labelPad": "패드 이름 (선택 사항)",
"index.placeholderPadEnter": "패드 이름을 입력...",
"index.createAndShareDocuments": "실시간으로 문서를 생성하고 공유하세요",
"index.createAndShareDocumentsDescription": "이더패드를 사용하면 마치 브라우저에서 실행되는 실시간 멀티플레이어 편집기처럼 문서를 실시간으로 공동 편집할 수 있습니다.",
"pad.toolbar.bold.title": "굵게 (Ctrl+B)",
"pad.toolbar.italic.title": "기울임꼴 (Ctrl+I)",
"pad.toolbar.underline.title": "밑줄 (Ctrl+U)",
@ -109,33 +287,45 @@
"pad.settings.stickychat": "화면에 항상 대화 보기",
"pad.settings.chatandusers": "대화와 사용자 보기",
"pad.settings.colorcheck": "작성자 표시 색상",
"pad.settings.fadeInactiveAuthorColors": "비활성 저자의 색상 흐리기",
"pad.settings.linenocheck": "줄 번호",
"pad.settings.rtlcheck": "우횡서(오른쪽에서 왼쪽으로)입니까?",
"pad.settings.enforceSettings": "다른 사용자에게 설정 적용",
"pad.settings.enforcedNotice": "이 설정은 패드 제작자가 잠가 놓았습니다. 설정을 변경해야 하는 경우 패드 제작자에게 문의하세요.",
"pad.settings.fontType": "글꼴 종류:",
"pad.settings.fontType.normal": "보통",
"pad.settings.language": "언어:",
"pad.settings.deletePad": "패드 삭제",
"pad.delete.confirm": "정말로 이 패드를 삭제하겠습니까?",
"pad.deletionToken.modalTitle": "패드 삭제 토큰을 저장",
"pad.deletionToken.modalBody": "이 토큰은 브라우저 세션이 종료되거나 기기를 변경했을 때 이 패드를 삭제할 수 있는 유일한 방법입니다. 안전한 곳에 저장해 두세요. 이 토큰은 여기에 딱 한 번만 표시됩니다.",
"pad.deletionToken.copy": "복사",
"pad.deletionToken.copied": "복사됨",
"pad.deletionToken.acknowledge": "저장했습니다",
"pad.deletionToken.deleteWithToken": "토큰으로 패드 삭제",
"pad.deletionToken.tokenFieldLabel": "패드 삭제 토큰",
"pad.deletionToken.tokenValueLabel": "패드 삭제 토큰(읽기 전용)",
"pad.deletionToken.invalid": "이 패드에 유효한 토큰이 아닙니다.",
"pad.deletionToken.notCreator": "당신은 이 패드의 제작자가 아니므로 삭제할 수 없습니다.",
"pad.settings.about": "정보",
"pad.settings.poweredBy": "제공:",
"pad.importExport.import_export": "가져오기/내보내기",
"pad.importExport.import": "텍스트 파일이나 문서 올리기",
"pad.importExport.importSuccessful": "성공!",
"pad.importExport.export": "다음으로 현재 패드 내보내기:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exportetherpad": "이더패드",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "일반 텍스트",
"pad.importExport.exportword": "Microsoft 워드",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format, 개방형 문서 형식)",
"pad.importExport.exportetherpada.title": "이더패드 형식으로 내보내기",
"pad.importExport.exporthtmla.title": "HTML로 내보내기",
"pad.importExport.exportplaina.title": "일반 텍스트로 내보내기",
"pad.importExport.exportworda.title": "Microsoft Word 파일로 내보내기",
"pad.importExport.exportpdfa.title": "PDF로 내보내기",
"pad.importExport.exportopena.title": "ODF(Open Document Format) 형식으로 내보내기",
"pad.importExport.noConverter.innerHTML": "일반 텍스트, HTML, 마이크로소프트 워드 (.docx) 또는 이더패드 파일을 직접 가져올 수 있습니다. PDF, ODT, DOC 또는 RTF와 같은 다른 형식을 가져오려면 서버 관리자가 리브레오피스를 설치해야 합니다. 자세한 내용은 <a href=\"https://docs.etherpad.org/\">이더패드 설명서</a>를 참고하세요.",
"pad.modals.connected": "연결함.",
"pad.modals.reconnecting": "내 패드에 다시 연결하는 중...",
"pad.modals.forcereconnect": "강제로 다시 연결",
@ -181,9 +371,23 @@
"timeslider.pageTitle": "{{appTitle}} 시간슬라이더",
"timeslider.toolbar.returnbutton": "패드로 돌아가기",
"pad.historyMode.banner": "역사 보는 중",
"pad.historyMode.return": "라이브로 돌아가기",
"pad.historyMode.revisionLabel": "판 {{rev}}",
"pad.historyMode.controlsLabel": "패드 기록 제어",
"pad.historyMode.sliderLabel": "패드 판",
"pad.historyMode.settings.title": "역사 재생",
"pad.historyMode.settings.follow": "패드 콘텐츠의 갱신 주시하기",
"pad.historyMode.settings.followShort": "팔로우",
"pad.historyMode.followOn": "패드 변경 사항 추적중 — 추적 중지하려면 클릭",
"pad.historyMode.followOff": "패드 변경 사항 추적 안함 — 추적하려면 클릭",
"pad.historyMode.settings.playbackSpeed": "재생 속도:",
"pad.historyMode.chat.replayHeader": "{{time}} 시점의 채팅",
"pad.historyMode.users.authorsHeader": "이 판의 작성자",
"pad.editor.skipToContent": "편집기로 바로 가기",
"pad.editor.keyboardHint": "Esc 키를 눌러 편집기를 종료하세요. Alt+F9 키를 눌러 도구 모음에 액세스하세요.",
"pad.editor.toolbar.formatting": "도구 모음 설정",
"pad.editor.toolbar.actions": "패드 작업 도구 모음",
"pad.editor.toolbar.showMore": "도구 모음 버튼 더 보기",
"timeslider.toolbar.authors": "작성자:",
"timeslider.toolbar.authorsList": "저자 없음",
"timeslider.toolbar.exportlink.title": "내보내기",
@ -217,6 +421,7 @@
"pad.savedrevs.timeslider": "당신은 타임슬라이더를 통해 저장된 버전을 볼 수 있습니다",
"pad.userlist.entername": "이름을 입력하세요",
"pad.userlist.unnamed": "이름없음",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}}명의 연결된 사용자, other: {{count}}명의 연결된 사용자 ]}",
"pad.editbar.clearcolors": "전체 문서의 작성자 표시 색상을 지우시겠습니까? 이 작업은 취소할 수 없습니다",
"pad.impexp.importbutton": "지금 가져오기",
"pad.impexp.importing": "가져오는 중...",
@ -227,5 +432,6 @@
"pad.impexp.importfailed": "가져오기를 실패했습니다",
"pad.impexp.copypaste": "복사하여 붙여넣으세요",
"pad.impexp.exportdisabled": "{{type}} 형식으로 내보내기가 비활성화되어 있습니다. 자세한 내용은 시스템 관리자에게 문의하시기 바랍니다.",
"pad.impexp.maxFileSize": "파일의 용량이 너무 큽니다. 가져올 파일의 허용 크기를 늘리려면 사이트 관리자에게 문의하십시오"
"pad.impexp.maxFileSize": "파일의 용량이 너무 큽니다. 가져올 파일의 허용 크기를 늘리려면 사이트 관리자에게 문의하십시오",
"pad.social.description": "모두가 실시간으로 편집할 수 있는 공동 작업 문서입니다."
}

View file

@ -18,6 +18,7 @@
"admin_pads.col.users": "Benotzer",
"admin_pads.confirm_button": "OK",
"admin_pads.empty_never_edited": "eidel · nach ni geännert",
"admin_pads.error_dialog_description": "Et ass e Feeler geschitt.",
"admin_pads.error_prefix": "Feeler",
"admin_pads.filter.active": "Aktiv",
"admin_pads.filter.empty": "Eidel",
@ -57,6 +58,7 @@
"admin_settings": "Astellungen",
"admin_settings.current_save.value": "Astellunge späicheren",
"admin_settings.invalid_json": "Ongültegen JSON",
"admin_settings.toast.auth_error": "Dir sidd net als Admin authentifizéiert. Loggt Iech wgl. nei an.",
"admin_settings.env_pill.runtime_label": "aktive Wäert",
"admin_settings.page-title": "Astellungen - Etherpad",
"admin_settings.save_error": "Feeler beim Späichere vun den Astellungen",

View file

@ -8,6 +8,7 @@
},
"admin.page-title": "Администраторска управувачница — Etherpad",
"admin.loading": "Вчитувам…",
"admin.loading_description": "Почекајте додека се вчита страницата.",
"admin.toggle_sidebar": "Префрли страничник",
"admin.shout": "Општење",
"admin_shout.online_one": "Моментално има {{count}} корисник на линија",
@ -27,7 +28,11 @@
"admin_pads.col.revisions": "Преработки",
"admin_pads.col.users": "Корисници",
"admin_pads.confirm_button": "ОК",
"admin_pads.create_pad_dialog_description": "Изберете име на новата тетратка.",
"admin_pads.delete_pad_dialog_description": "Потврдете или откажете го бришењето на тетратката.",
"admin_pads.delete_pad_dialog_title": "Избриши тетратка",
"admin_pads.empty_never_edited": "празно · никогаш неуредено",
"admin_pads.error_dialog_description": "Се појави грешка.",
"admin_pads.error_prefix": "Грешка",
"admin_pads.filter.active": "Активно",
"admin_pads.filter.all": "Сите",
@ -111,16 +116,52 @@
"admin_plugins_info.plugins": "Воспоставени приклучоци",
"admin_plugins_info.page-title": "Информации за приклучоци — Etherpad",
"admin_plugins_info.search_placeholder": "Пребарај пресретник или дел…",
"admin_plugins_info.subtitle": "Системска дијагностика: воспоставена верзија, регистрирани делови и пресретници.",
"admin_plugins_info.tab_client": "Клиент",
"admin_plugins_info.tab_server": "Опслужувач",
"admin_plugins_info.up_to_date": "Подновено",
"admin_plugins_info.update_available": "Достапна поднова: {{version}}",
"admin_plugins_info.version": "Верзија на Etherpad",
"admin_plugins_info.version_latest": "Најнова достапна верзија",
"admin_plugins_info.version_number": "Број на верзијата",
"admin_settings": "Нагодувања",
"admin_settings.create_pad": "Создај тетратка",
"admin_settings.current": "Тековна поставеност",
"admin_settings.current_example-devel": "Предлошка за примерни разработни нагодувања",
"admin_settings.current_example-prod": "Предлошка за примерни производни нагодувања",
"admin_settings.current_restart.value": "Пушти го Etherpad одново",
"admin_settings.current_save.value": "Зачувај нагодувања",
"admin_settings.invalid_json": "Неважечки JSON",
"admin_settings.current_test.value": "Провери го JSON",
"admin_settings.current_prettify.value": "Разубави го JSON",
"admin_settings.toast.saved": "Нагодувањата се успешно зачувани.",
"admin_settings.toast.save_failed": "Зачувувањето на успеа: не можев да запишам во settings.json.",
"admin_settings.toast.json_invalid": "Синтаксна грешка: проверете запирки, загради и наводници.",
"admin_settings.toast.disconnected": "Не можам да зачуван: не сте поврзани на опслужувачот.",
"admin_settings.toast.validation_ok": "JSON е важечки.",
"admin_settings.toast.validation_failed": "JSON е неважечки: исправете ги синтаксните грешки.",
"admin_settings.toast.prettify_failed": "Не можам да разубавам: прво поправете ги синтаксните грешки.",
"admin_settings.prettify_confirm": "Разубавувањето ќе ги отстрани сите коментари. Да продолжам?",
"admin_settings.mode.form": "Образец",
"admin_settings.mode.raw": "Сирово",
"admin_settings.mode.effective": "На дело",
"admin_settings.mode.effective_tooltip": "Преглед само за читање на вредностите кои Etherpad всушност во моментов ги користи по околински променливо заменување. Тајните се скриени.",
"admin_settings.mode.aria_label": "Уредувачки режим",
"admin_settings.envvar_banner.title": "Оваа податотека е предлошка, не поставките во живо.",
"admin_settings.envvar_banner.body": "Пополнувачите како ${VAR:default} се заменуваат во складот при покревање; никогаш не се запишуваат назад во оваа податотека. Уредете околински променливи во вашата околина (Docker compose, systemd, .env) за да ја измените произлезената вредност, или пак да го замените пополнувачот тука со буквална низа. Прејдете на јазичето „На дело“ за да видите што во тековно користи Etherpad.",
"admin_settings.toast.auth_error": "Не сте заверени како администратор. Најавете се повторно.",
"admin_settings.section.general": "Општо",
"admin_settings.parse_error.title": "Не можам да го расчленам settings.json",
"admin_settings.parse_error.cta": "Префрлете се на сирово за да уредувате",
"admin_settings.env_pill.tooltip": "Чита од околинската променлива {{variable}}. Вредноста подолу се користи кога нема зададено ништо за {{variable}}.",
"admin_settings.env_pill.default_label": "по основно",
"admin_settings.env_pill.input_aria": "Стандардна вредност за {{variable}}",
"admin_settings.env_pill.runtime_label": "активна вредност",
"admin_settings.env_pill.runtime_tooltip": "Etherpad тековно ја користи оваа вредност, изведена од {{variable}} или основно зададената.",
"admin_settings.env_pill.redacted_tooltip": "Etherpad користи вредност за {{variable}}, но таа е скриена бидејќи е тајна.",
"admin_settings.page-title": "Нагодувања — Etherpad",
"admin_settings.save_error": "Грешка при зачувувањето на нагодувањата",
"admin_settings.saved_success": "Успешно зачувани нагодувањата",
"update.banner.title": "Достапна е надградба",
"update.banner.body": "Достапен е Etherpad {{latest}} (ја користите {{current}}).",
"update.banner.cta": "Погл. надградба",
@ -132,8 +173,53 @@
"update.page.tier": "Степен на надградба",
"update.page.changelog": "Дневник на измени",
"update.page.up_to_date": "Ја користите најновата верзија.",
"update.page.disabled": "Проверките за поднова се оневозможени (updates.tier = „off“).",
"update.page.unauthorized": "Не сте овластени да го гледате статусот на подновеност.",
"update.page.error": "Не можев да го вчитам статусот на подновеност (статус {{status}}).",
"update.badge.severe": "Etherpad на овој опслужувач е многу застарен. Известете го администраторот.",
"update.badge.vulnerable": "Etherpad на овој опслужувач работи на верзија со познати безбедносни проблеми. Известете го администраторот.",
"update.page.apply": "Примени поднова",
"update.page.cancel": "Откажи",
"update.page.acknowledge": "Прифати",
"update.page.log": "Дневник на поднови (барем 200 редови)",
"update.page.execution": "Статус",
"update.page.policy.install-method-not-writable": "Подновите од администраторскиот посредник бараат Git-воспоставка. Подновете преку вашиот управувач со пакети.",
"update.page.policy.rollback-failed-terminal": "Претходната поднова не успеа и не можеше да се отповика. Стиснете на „Прифати“ откако воспоставката ќе стане задрава за да отклучите.",
"update.page.policy.up-to-date": "Ја користите најновата верзија.",
"update.page.policy.tier-off": "Подновие се оневозможени (updates.tier = „off“).",
"update.page.policy.maintenance-window-missing": "Ниво 4 (автономно) бара период на одржување. Задајте го updates.maintenanceWindow во settings.json за да овозможите автономни поднови.",
"update.page.policy.maintenance-window-invalid": "Ниво 4 (автономно) е оневозможено бидејќи updates.maintenanceWindow е погрешно срочен. Се очекуваше {start, end, tz} со времиња ЧЧ:ММ и tz да биде „local“ или „utc“.",
"update.page.last_result.verified": "Потврдена последната поднова на {{tag}}.",
"update.page.last_result.rolled-back": "Отповикан последниот обид за поднова на {{tag}}: {{reason}}.",
"update.page.last_result.rollback-failed": "Последниот обид за поднова не успеа И отповикувањето не успеа: {{reason}}. Потребна е рачна интервенција.",
"update.page.last_result.preflight-failed": "Не успеа последниот обид за поднова на {{tag}} во подготовката: {{reason}}.",
"update.page.last_result.cancelled": "Последниот обид за поднова на {{tag}} е откажан од администратор.",
"update.execution.idle": "Неактивно",
"update.execution.scheduled": "Подновата е закажана",
"update.execution.preflight": "Подготвителни проверки",
"update.execution.preflight-failed": "Проверките не успеаја",
"update.execution.draining": "Исцрпувачки седници",
"update.execution.executing": "Подновувам...",
"update.execution.pending-verification": "Чека на потврда",
"update.execution.verified": "Потврдено",
"update.execution.rolling-back": "Отповикување",
"update.execution.rolled-back": "Отповикано",
"update.execution.rollback-failed": "Отповикувањето не успеа",
"update.banner.terminal.rollback-failed": "Не успеа обидот за поднова и не можеше да биде отповикан. Потребна е рачна интервенција.",
"update.banner.scheduled": "Автоподновата на {{tag}} е закажана — на сила за {{remaining}}.",
"update.banner.maintenance-window-missing": "Автономните поднови се оневозможени додека не се постави период на одржување.",
"update.banner.maintenance-window-invalid": "Автономните поднови се оневозможени бидејќи периодот на одржување е погрешно срочен.",
"update.page.scheduled.title": "Подновата е закажана",
"update.page.scheduled.countdown": "Etherpad ќе почне со подновување на {{tag}} за {{remaining}}.",
"update.page.scheduled.deferred_until": "Вон периодот на одржување. Подновата ќе почне кога периодот ќе започне во {{at}}.",
"update.page.scheduled.apply_now": "Примени сега",
"update.window.title": "Период на одржување",
"update.window.summary": "{{start}}{{end}} ({{tz}})",
"update.window.unset": "Не е наместено.",
"update.window.next_opens_at": "Следниот период почнува на {{at}}.",
"update.drain.t60": "Etherpad ќе се превклучи за 60 секунди за да направи примена и поднова.",
"update.drain.t30": "Etherpad ќе се превклучи за 30 секунди за да направи примена и поднова.",
"update.drain.t10": "Etherpad ќе се превклучи за 10 секунди за да направи примена и поднова.",
"index.newPad": "Нова тетратка",
"index.settings": "Нагодувања",
"index.transferSessionTitle": "Префрли седница",
@ -146,6 +232,7 @@
"index.copyLinkButton": "Копирај врска во меѓускладот",
"index.transferToSystem": "3. Копирај седница во нов систем",
"index.transferToSystemDescription": "Отворете ја ископираната врска во целниот прелистувач или уред за да ја префрлите вашата седница.",
"index.code": "Код",
"index.transferSessionDescription": "Префрлете ја вашата тековна седница на прелистувач или уред стискајќи на копчето подолу. Ова ќе ја прекопира врската во страница која ќе ви ја префрли седницата кога ќе се отвори во целниот прелистувач или уред.",
"index.createOpenPad": "Отвори тетратка по име",
"index.openPad": "отвори постоечка тетратка наречена:",
@ -220,6 +307,12 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Извези како Etherpad",
"pad.importExport.exporthtmla.title": "Извези како HTML",
"pad.importExport.exportplaina.title": "Извези како прост текст",
"pad.importExport.exportworda.title": "Извези како Microsoft Word",
"pad.importExport.exportpdfa.title": "Извези како PDF",
"pad.importExport.exportopena.title": "Извези како ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Можете да увезувате само од прост текст или HTML-форматти. Понапредни можности за увоз ќе добиете ако <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">го воспоставите LibreOffice</a>.",
"pad.modals.connected": "Поврзано.",
"pad.modals.reconnecting": "Ве преповрзувам со тетратката...",
@ -265,13 +358,31 @@
"timeslider.followContents": "Следи ги подновите во содржината на тетратката",
"timeslider.pageTitle": "{{appTitle}} Историски преглед",
"timeslider.toolbar.returnbutton": "Назад на тетратката",
"pad.historyMode.banner": "Историја на посети",
"pad.historyMode.return": "Назад во живо",
"pad.historyMode.revisionLabel": "Преработка {{rev}}",
"pad.historyMode.controlsLabel": "Контроли за историја на тетратката",
"pad.historyMode.sliderLabel": "Преработка на тетратката",
"pad.historyMode.settings.title": "Преглед на историјата",
"pad.historyMode.settings.follow": "Следи поднови на содржини тетратки",
"pad.historyMode.settings.followShort": "Следи",
"pad.historyMode.followOn": "Следите промени во тетратката — стиснете за да престанете со следење",
"pad.historyMode.followOff": "Не следите промени во тетратката — стиснете за да следите",
"pad.historyMode.settings.playbackSpeed": "Брзина на прегледувањето:",
"pad.historyMode.chat.replayHeader": "Разговор од {{time}}",
"pad.historyMode.users.authorsHeader": "Авроти на оваа преработка",
"pad.editor.skipToContent": "Прејди на уредувачот",
"pad.editor.keyboardHint": "Стиснете Escape за да излезете од уредувачот. Стиснете Alt+F9 за алатникот.",
"pad.editor.toolbar.formatting": "Алатник за форматирање",
"pad.editor.toolbar.actions": "Алатник за дејства врз тетратка",
"pad.editor.toolbar.showMore": "Покажи повеќе копчиња на алатникот",
"timeslider.toolbar.authors": "Автори:",
"timeslider.toolbar.authorsList": "Нема автори",
"timeslider.toolbar.exportlink.title": "Извоз",
"timeslider.exportCurrent": "Извези ја тековната верзија како:",
"timeslider.version": "Верзија {{version}}",
"timeslider.saved": "Зачувано на {{day}} {{month}} {{year}} г.",
"timeslider.settings.playbackSpeed": "Брзина на репродукција:",
"timeslider.settings.playbackSpeed": "Брзина на прегледување:",
"timeslider.settings.playbackSpeed.original": "Изворна брзина",
"timeslider.settings.playbackSpeed.realtime": "Реално време",
"timeslider.settings.playbackSpeed.200ms": "200 мс",
@ -298,6 +409,7 @@
"pad.savedrevs.timeslider": "Можете да ги погледате зачуваните преработки посетувајќи го времеследниот лизгач",
"pad.userlist.entername": "Внесете го вашето име",
"pad.userlist.unnamed": "без име",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} поврзан корисник, other: {{count}} поврзани корисници ]}",
"pad.editbar.clearcolors": "Да ги отстранам авторските бои од целиот документ? Ова е неповратно",
"pad.impexp.importbutton": "Увези сега",
"pad.impexp.importing": "Увезувам...",

View file

@ -28,17 +28,17 @@
"pad.modals.cancel": "Xikxolewa",
"pad.modals.deleted": "Omopohpoloh.",
"pad.modals.deleted.explanation": "Ōmopoloh inīn Pad.",
"timeslider.version": "Inīc {{version}} Cuepaliztli",
"timeslider.month.january": "Eneroh",
"timeslider.month.february": "Febreroh",
"timeslider.month.march": "Marsoh",
"timeslider.month.april": "April",
"timeslider.month.may": "Mayoh",
"timeslider.month.june": "Honioh",
"timeslider.month.july": "Holioh",
"timeslider.month.august": "Ahostoh",
"timeslider.month.september": "Septiempreh",
"timeslider.month.october": "Oktopreh",
"timeslider.month.november": "Noviempreh",
"timeslider.month.december": "Tisiempreh"
"timeslider.version": "Inic {{version}} Tlacepaliztli",
"timeslider.month.january": "Enero",
"timeslider.month.february": "Febrero",
"timeslider.month.march": "Marzo",
"timeslider.month.april": "Abril",
"timeslider.month.may": "Mayo",
"timeslider.month.june": "Junio",
"timeslider.month.july": "Julio",
"timeslider.month.august": "Agosto",
"timeslider.month.september": "Septiembre",
"timeslider.month.october": "Octubre",
"timeslider.month.november": "Noviembre",
"timeslider.month.december": "Diciembre"
}

View file

@ -20,6 +20,7 @@
},
"admin.page-title": "Beheerdashboard Etherpad",
"admin.loading": "Bezig met laden...",
"admin.loading_description": "Even geduld, de pagina wordt geladen.",
"admin.toggle_sidebar": "Zijbalk omschakelen",
"admin.shout": "Communicatie",
"admin_shout.online_one": "Er zijn momenteel {{count}} gebruikers online",
@ -39,7 +40,11 @@
"admin_pads.col.revisions": "Versies",
"admin_pads.col.users": "Gebruikers",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Kies een naam voor de nieuwe notitie.",
"admin_pads.delete_pad_dialog_description": "Verwijderen van de notitie bevestigen of annuleren.",
"admin_pads.delete_pad_dialog_title": "Notitie verwijderen",
"admin_pads.empty_never_edited": "leeg · nooit bewerkt",
"admin_pads.error_dialog_description": "Er is een fout opgetreden.",
"admin_pads.error_prefix": "Fout",
"admin_pads.filter.active": "Actief",
"admin_pads.filter.all": "Alle",
@ -151,7 +156,12 @@
"admin_settings.prettify_confirm": "Met opmaken worden alle opmerkingen verwijderd. Doorgaan?",
"admin_settings.mode.form": "Formulier",
"admin_settings.mode.raw": "Ruw",
"admin_settings.mode.effective": "Actief",
"admin_settings.mode.effective_tooltip": "Alleen-lezen weergave van de waarden die Etherpad momenteel daadwerkelijk gebruikt, na vervanging van omgevingsvariabelen. Geheimen zijn verwijderd.",
"admin_settings.mode.aria_label": "Bewerkingsmodus",
"admin_settings.envvar_banner.title": "Dit bestand is een sjabloon, niet de daadwerkelijke configuratie.",
"admin_settings.envvar_banner.body": "Plaatsaanduidingen zoals ${VAR:default} worden bij het opstarten in het geheugen geladen; ze worden nooit naar dit bestand teruggeschreven. Bewerk omgevingsvariabelen in uw omgeving (Docker Compose, systemd, .env) om de opgeloste waarde te wijzigen, of vervang de plaatsaanduiding hier door een letterlijke waarde. Ga naar het tabblad Actief om te zien wat Etherpad momenteel gebruikt.",
"admin_settings.toast.auth_error": "U bent niet aangemeld als beheerder. Meld alstublieft opnieuw aan.",
"admin_settings.section.general": "Algemeen",
"admin_settings.parse_error.title": "Kan settings.json niet verwerken",
"admin_settings.parse_error.cta": "Overschakelen naar ruw bewerken",
@ -186,7 +196,7 @@
"update.page.log": "Updatelogboek (laatste 200 regels)",
"update.page.execution": "Status",
"update.page.policy.install-method-not-writable": "Updates via de beheerdersinterface vereisen een Git-installatie. Update via uw pakketbeheerder.",
"update.page.policy.rollback-failed-terminal": "Een eerdere update is mislukt en kon niet ongedaan worden gemaakt. Druk op 'Bevestigen' nadat de installatie succesvol is voltooid om de vergrendeling op te heffen.",
"update.page.policy.rollback-failed-terminal": "Een eerdere update is mislukt en kon niet ongedaan worden gemaakt. Druk op 'Bevestigen' nadat de installatie is voltooid om de vergrendeling op te heffen.",
"update.page.policy.up-to-date": "U gebruikt de nieuwste versie.",
"update.page.policy.tier-off": "Updates zijn uitgeschakeld (updates.tier = \"off\").",
"update.page.policy.maintenance-window-missing": "Niveau 4 (autonoom) vereist een onderhoudsvenster. Stel updates.maintenanceWindow in settings.json in om autonoom bijwerken in te schakelen.",
@ -234,6 +244,7 @@
"index.copyLinkButton": "Koppeling naar klembord kopiëren",
"index.transferToSystem": "3. Kopieer de sessie naar het nieuwe systeem",
"index.transferToSystemDescription": "Open de gekopieerde koppeling in de doelbrowser of het doelapparaat om uw sessie over te zetten.",
"index.code": "Code",
"index.transferSessionDescription": "Zet uw huidige sessie over naar uw browser of apparaat door op de onderstaande knop te klikken. Hiermee wordt een koppeling naar een pagina gekopieerd die uw sessie overzet wanneer deze in de gewenste browser of op het gewenste apparaat wordt geopend.",
"index.createOpenPad": "Notitie openen met de naam",
"index.openPad": "open een bestaande notitie met de naam:",
@ -295,7 +306,7 @@
"pad.deletionToken.tokenFieldLabel": "Padverwijderingstoken",
"pad.deletionToken.tokenValueLabel": "Uw padverwijderingstoken (alleen-lezen)",
"pad.deletionToken.invalid": "Dat token is niet geldig voor deze notitie.",
"pad.deletionToken.notCreator": "U bent niet de maker van dit notitieblok, dus je kunt het niet verwijderen.",
"pad.deletionToken.notCreator": "U bent niet de maker van dit notitieblok, dus u kunt het niet verwijderen.",
"pad.settings.about": "Over",
"pad.settings.poweredBy": "Mogelijk gemaakt door",
"pad.importExport.import_export": "Importeren/exporteren",

View file

@ -7,13 +7,71 @@
},
"admin.page-title": "Cruscòt d'aministrator - Etherpad",
"admin.loading": "Antramentre ch'as caria…",
"admin.loading_description": "Për piasì, ch'a speta antramentre che la pàgina as caria.",
"admin.toggle_sidebar": "Ativé o disativé la bara lateral",
"admin.shout": "Comunicassion",
"admin_shout.online_one": "Al moment a-i é {{count}} utent an linia",
"admin_shout.online_other": "Al moment a-i son {{count}} utent an linia",
"admin_shout.sticky_toggle": "Cangé ël mëssagi an evidensa",
"admin_login.title": "Etherpad",
"admin_login.username": "Stranòm",
"admin_login.password": "Ciav",
"admin_login.submit": "Intré ant ël sistema",
"admin_login.failed": "Falì a intré ant ël sistema",
"admin_pads.all_pads": "Tuti ij blochèt",
"admin_pads.bulk.cleanup_history": "Dëscancelé la stòria",
"admin_pads.bulk.clear_selection": "Dëscancelé la selession",
"admin_pads.bulk.delete": "Dëscancelé",
"admin_pads.cancel": "Anulé",
"admin_pads.col.pad": "Blochèt",
"admin_pads.col.revisions": "Revision",
"admin_pads.col.users": "Utent",
"admin_pads.confirm_button": "Va bin",
"admin_pads.create_pad_dialog_description": "Serne un nòm për ël neuv blochèt.",
"admin_pads.delete_pad_dialog_description": "Confirmé o anulé la dëscancelassion dël blochèt.",
"admin_pads.delete_pad_dialog_title": "Dëscancelé ël blochèt",
"admin_pads.empty_never_edited": "veuid · mai modificà",
"admin_pads.error_dialog_description": "A l'é rivaje n'eror.",
"admin_pads.error_prefix": "Eror",
"admin_pads.filter.active": "Ativ",
"admin_pads.filter.all": "Tuti",
"admin_pads.filter.empty": "Veuid",
"admin_pads.filter.recent": "Costa sman-a",
"admin_pads.filter.stale": "Blocà (>1a)",
"admin_pads.open": "Duverté",
"admin_pads.pagination.next": "Sucessiv",
"admin_pads.pagination.previous": "Precedent",
"admin_pads.refresh": "Rinfrësché",
"admin_pads.relative.days": "{{count}}d fa",
"admin_pads.relative.hours": "{{count}}o fa",
"admin_pads.relative.just_now": "pròpi adess",
"admin_pads.relative.minutes": "{{count}}m fa",
"admin_pads.relative.months": "{{count}}mè fa",
"admin_pads.relative.weeks": "{{count}}s fa",
"admin_pads.relative.years": "{{count}}a fa",
"admin_pads.revisions_count": "{{count}} revision",
"admin_pads.selected_count": "{{count}} selessionà",
"admin_pads.show": "Smon-e",
"admin_pads.sort.name": "Nòm (AZ)",
"admin_pads.sort.revision_number": "Revision",
"admin_pads.sort.user_count": "Utent",
"admin_pads.stats.across_pads": "a travers tuti ij blochèt",
"admin_pads.stats.active_users": "Utent ativ",
"admin_pads.stats.empty_pads": "Blochèt veuid",
"admin_pads.stats.last_activity": "Ùltima atività",
"admin_pads.stats.no_active_users": "Gnun utent ativ",
"admin_pads.stats.revisions_zero": "0 revision",
"admin_pads.stats.total": "Blochèt totaj",
"admin_pads.stats.users_active": "{{count}} ativ al moment",
"admin_pads.subtitle": "Vista global ëd tuti ij blochèt ansima a costa istansa d'Etherpad. Arserché, polidé, duverté.",
"admin_plugins": "Mansé dj'anstalassion",
"admin_plugins.available": "Anstalassion disponìbij",
"admin_plugins.available_not-found": "Gnun-e anstalassion trovà.",
"admin_plugins.available_fetching": "Arcuperassion…",
"admin_plugins.available_install.value": "Anstalé",
"admin_plugins.available_search.placeholder": "Arserca d'aplicassion da anstalé",
"admin_plugins.check_updates": "Controlé për dj'agiornament",
"admin_plugins.core_count": "{{count}} nos",
"admin_plugins.description": "Descrission",
"admin_plugins.installed": "Aplicassion anstalà",
"admin_plugins.installed_fetching": "Arcuperassion dj'aplicassion anstalà…",

View file

@ -5,19 +5,113 @@
"شاه زمان پټان"
]
},
"admin.page-title": "پازوال ليددړه - اېترپيډ",
"admin.loading": "بارېږي...",
"admin.loading_description": "مهرباني وکړئ د مخ بارېدلو پرمهال په تمه شئ.",
"admin.toggle_sidebar": "څنگپټه بدلول",
"admin.shout": "اړيکتيا",
"admin_shout.online_one": "اوسمهال {{count}} کارنان پرليکه دي",
"admin_shout.online_other": "اوسمهال {{count}} کارنان پرليکه دي",
"admin_login.title": "اېترپيډ",
"admin_login.username": "کارن‌نوم",
"admin_login.password": "پټنوم",
"admin_login.submit": "ننوتل",
"admin_login.failed": "ننوتل نابرياله شول",
"admin_pads.all_pads": "ټولې ليکچې",
"admin_pads.bulk.cleanup_history": "پېښليک سپينول",
"admin_pads.bulk.clear_selection": "ټاکنه سپاکول",
"admin_pads.bulk.delete": "ړنگول",
"admin_pads.cancel": "ناگارل",
"admin_pads.col.pad": "ليکچه",
"admin_pads.col.revisions": "بياليدنې",
"admin_pads.col.users": "کارنان",
"admin_pads.confirm_button": "ښه",
"admin_pads.create_pad_dialog_description": "نوې ليکچې ته نوم خوښ کړئ.",
"admin_pads.delete_pad_dialog_description": "د ليکچې د ړنگول تاييد يا ناگارل",
"admin_pads.delete_pad_dialog_title": "ليکچه ړنگول",
"admin_pads.empty_never_edited": "تش . هېڅکله سم شوی نه دی",
"admin_pads.error_dialog_description": "یوه تېروتنه رامنځته شوې.",
"admin_pads.error_prefix": "تېروتنه",
"admin_pads.filter.active": "کارنده",
"admin_pads.filter.all": "ټول",
"admin_pads.filter.empty": "تش",
"admin_pads.filter.recent": "دا اوونۍ",
"admin_pads.open": "پرانېستل",
"admin_pads.pagination.next": "راتلونکی",
"admin_pads.pagination.previous": "مخکنی",
"admin_pads.refresh": "تاندول",
"admin_pads.relative.days": "{{count}} ورځې دمخه",
"admin_pads.relative.hours": "{{count}} گړۍ دمخه",
"admin_pads.relative.just_now": "همدا اوس",
"admin_pads.relative.minutes": "{{count}} دقيقې دمخه",
"admin_pads.relative.months": "{{count}} مياشتې دمخه",
"admin_pads.relative.weeks": "{{count}} اوونۍ دمخه",
"admin_pads.relative.years": "{{count}} کاله دمخه",
"admin_pads.revisions_count": "{{count}} بياليدنې",
"admin_pads.selected_count": "{{count}} غوره‌شوي",
"admin_pads.show": "ښودل",
"admin_pads.sort.name": "نوم (ا ـ ي)",
"admin_pads.sort.revision_number": "بياليدنې",
"admin_pads.sort.user_count": "کارنان",
"admin_pads.stats.across_pads": "په ټولو ليکچو کې",
"admin_pads.stats.active_users": "کارنده کارنان",
"admin_pads.stats.empty_pads": "تشې ليکچې",
"admin_pads.stats.last_activity": "وروستۍ کړنه",
"admin_pads.stats.no_active_users": "هېڅ کارنده کارنان نشته",
"admin_pads.stats.revisions_zero": "۰ بياليدنې",
"admin_pads.stats.total": "ټولټال ليکچې",
"admin_pads.stats.users_active": "وسمهال {{count}} کارنده",
"admin_plugins.description": "څرگنداوی",
"admin_plugins.disables.label": "ناچارن‌شوي:",
"admin_plugins.last-update": "وروستۍ هم‌مهالېدنه",
"admin_plugins.name": "نوم",
"admin_plugins.sort.last_updated": "وروستۍ هم‌مهالېدنه",
"admin_plugins.sort.name": "نوم (ا ـ ي)",
"admin_plugins.sort.version": "بلبڼه",
"admin_plugins.update_tooltip": "هم‌مهالول",
"admin_plugins.updates_available": "هم‌مهالېدنې د لاسرسي وړ دي",
"admin_plugins.update_now": "هم‌مهالول",
"admin_plugins.version": "بل‌بڼه",
"admin_plugins_info": "ستونزو اواري مالومات",
"admin_plugins_info.copy_value": "{{label}} لمېسل",
"admin_plugins_info.hooks": "ځای‌پرځای‌شوي چنگکونه",
"admin_plugins_info.tab_client": "غوښتندوی",
"admin_plugins_info.tab_server": "پالنگر",
"admin_plugins_info.up_to_date": "هم مهاله شوی",
"admin_plugins_info.update_available": "هم‌مهالېدنه شتون لري: {{version}}",
"admin_plugins_info.version_latest": "وروستۍ د لاسرسي وړ بل‌بڼه",
"admin_plugins_info.version_number": "بل‌بڼې شمېره",
"admin_settings": "اوڼنې",
"admin_settings.create_pad": "ليکنه جوړول",
"admin_settings.current": "اوسنی ترتيب",
"admin_settings.current_example-devel": "د پراختيااوڼنې کينډۍ بېلگه",
"admin_settings.current_example-prod": "د توليد اوڼنې کينډۍ بېلگه",
"admin_settings.current_save.value": "اوڼنې خوندي‌کول",
"admin_settings.mode.form": "فورمه",
"admin_settings.mode.raw": "اومه",
"admin_settings.mode.effective": "اغېزمن",
"admin_settings.mode.aria_label": "سمونگر ونگ‌ډول",
"admin_settings.section.general": "ټولواله",
"admin_settings.env_pill.default_label": "تلواله",
"admin_settings.env_pill.runtime_label": "کارنده ارزښت",
"update.banner.title": "هم‌مهالېدنه شتون لري",
"update.banner.cta": "هم‌مهالېدنه کتل",
"update.page.current": "اوسنۍ بلبڼه",
"update.page.latest": "اوسنۍ بل‌بڼه",
"update.page.apply": "هم‌مهالېدنه پلې‌کول",
"update.page.cancel": "ناگارل",
"update.page.execution": "دريځ",
"update.execution.executing": "هم‌مهالول...",
"update.execution.pending-verification": "د تاييد په تمه",
"update.execution.verified": "تایید شوی",
"index.newPad": "نوې ليکچه",
"index.settings": "اوڼنې",
"index.transferSessionTitle": "لېږد غونډه",
"index.receiveSessionTitle": "غونډې ته لاسرسی",
"index.transferSession": "۱. لېږد غونډه",
"index.copyLink": "۲. تړونی لمېسل",
"index.copyLinkButton": "تړونی ټينگدړې ته لمېسل",
"index.code": "کوډ",
"index.createOpenPad": "ليکچه د نوم له مخې پرانېستل",
"index.recentPads": "وروستۍ ليکچې",
"index.recentPadsEmpty": "هېڅ وروستۍ ليکچې ونه موندل شوې.",
@ -34,14 +128,18 @@
"pad.toolbar.undo.title": "ناکړل (Ctrl-Z)",
"pad.toolbar.redo.title": "بياکړل (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "د ليکوالۍ رنگونه سپينول (Ctrl+Shift+C)",
"pad.toolbar.savedRevision.title": "مخکتنه خوندي کول",
"pad.toolbar.savedRevision.title": "بياليدنه خوندي‌کول",
"pad.toolbar.settings.title": "اوڼنې",
"pad.toolbar.home.title": "بېرته کور ته",
"pad.colorpicker.save": "خوندي کول",
"pad.colorpicker.cancel": "ناگارل",
"pad.loading": "رابرسېرېږي...",
"pad.settings.padSettings": "د ليکچې اوڼنې",
"pad.settings.title": "اوڼنې",
"pad.settings.padSettings": "د ليکچې-پراخ اوڼنې",
"pad.settings.userSettings": "کارن اوڼنې",
"pad.settings.myView": "زما کتنه",
"pad.settings.disablechat": "بنډار ناچارنول",
"pad.settings.darkMode": "تياره ونگ‌ډول",
"pad.settings.stickychat": "تل په پردې بانډار کول",
"pad.settings.chatandusers": "کارنان او بانډار ښکاره کول",
"pad.settings.colorcheck": "د ليکوالۍ رنگونه",
@ -49,52 +147,71 @@
"pad.settings.fontType": "ليکبڼې ډول:",
"pad.settings.fontType.normal": "نورمال",
"pad.settings.language": "ژبه:",
"pad.settings.deletePad": "ليکچه ړنگول",
"pad.delete.confirm": "ايا تاسو په رښتيا دا ليکچه ړنگول غواړئ؟",
"pad.deletionToken.copy": "لمېسل",
"pad.deletionToken.copied": "ولمېسل شو",
"pad.deletionToken.acknowledge": "ما دا خوندي کړی دی",
"pad.settings.about": "په‌اړه",
"pad.settings.poweredBy": "چلوونکی",
"pad.importExport.import_export": "رالېږدول/بهرلېږل",
"pad.importExport.importSuccessful": "بريالی شو!",
"pad.importExport.exportetherpad": "اېترپډ",
"pad.importExport.export": "اوسنۍ ليکچه په لاندې ډول بهرلېږل:",
"pad.importExport.exportetherpad": "اېترپيډ",
"pad.importExport.exporthtml": "اچ ټي ام اېل",
"pad.importExport.exportplain": "ساده متن",
"pad.importExport.exportplain": "ساده ليک",
"pad.importExport.exportword": "مايکروسافټ ورډ",
"pad.importExport.exportpdf": "پي ډي اېف",
"pad.importExport.exportopen": "ODF (اوپن ډاکومنټ فارمټ)",
"pad.importExport.exporthtmla.title": "اېچ‌ټي‌اېم‌اېل په توگه بهرلېږل",
"pad.importExport.exportplaina.title": "ساده ليک په توگه بهرلېږل",
"pad.importExport.exportworda.title": "مايکروسافټ ورډ په توگه بهرلېږل",
"pad.importExport.exportpdfa.title": "پي‌ډي‌اېف په توگه بهرلېږل",
"pad.modals.connected": "اړيکمن شو.",
"pad.modals.reconnecting": "بېرته ستاسو ليکې سره پېلېږي...",
"pad.modals.forcereconnect": "په زوره بيانښلونه",
"pad.modals.reconnecttimer": "د بيانښلولو هڅه‌کول",
"pad.modals.cancel": "ناگارل",
"pad.modals.userdup": "په بله کړکۍ کې پرانېستل شو",
"pad.modals.slowcommit.explanation": "پالنگر ځواب نه وايي.",
"pad.modals.slowcommit.cause": "دا کېدای شي د جال د اړيکتيايي ستونزو په سبب وي.",
"pad.modals.deleted": "ړنگ شو.",
"pad.gritter.unacceptedCommit.title": "ناخوندي شوی سمون",
"pad.share": "دا ليکچه شريکول",
"pad.share.readonly": "يوازې لوستنه",
"pad.share.link": "تړونی",
"pad.share.emebdcode": "يو آر اېل ټومبل",
"pad.share.emebdcode": "وېبتړ ټومبل",
"pad.chat": "بانډار",
"pad.chat.loadmessages": "نور پيغامونه برسېرول",
"pad.historyMode.banner": "پېښليک کتل",
"pad.historyMode.revisionLabel": "بياليدنه {{rev}}",
"pad.historyMode.sliderLabel": "د ليکچې بياليدنه",
"pad.historyMode.settings.followShort": "څارل",
"timeslider.toolbar.authors": "ليکوال:",
"timeslider.toolbar.authorsList": "بې ليکواله",
"timeslider.toolbar.exportlink.title": "صادرول",
"timeslider.version": "بڼه {{version}}",
"timeslider.toolbar.exportlink.title": "بهرلېږل",
"timeslider.version": لبڼه {{version}}",
"timeslider.saved": "خوندي شو {{month}} {{day}}, {{year}}",
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "جنوري",
"timeslider.month.february": بروري",
"timeslider.month.february": ېبروري",
"timeslider.month.march": "مارچ",
"timeslider.month.april": "اپرېل",
"timeslider.month.may": ۍ",
"timeslider.month.april": "اپریل",
"timeslider.month.may": ی",
"timeslider.month.june": "جون",
"timeslider.month.july": "جولای",
"timeslider.month.august": "اگسټ",
"timeslider.month.september": "سېپتمبر",
"timeslider.month.october": "اکتوبر",
"timeslider.month.november": "نومبر",
"timeslider.month.december": يسمبر",
"timeslider.month.december": ېسمبر",
"timeslider.unnamedauthors": "{{num}} بې نومه {[ډېرگړي(num) يو: ليکوال، نور: ليکوالان ]}",
"pad.savedrevs.marked": "اوس دا مخکتنه د يوې خوندي شوې مخکتنې په توگه په نښه شوه",
"pad.userlist.entername": "نوم مو ورکړۍ",
"pad.savedrevs.marked": "اوس دا بياليدنه د يوې خوندي شوې بياليدنې په توگه په نښه شوه",
"pad.userlist.entername": "نوم مو ورکړئ",
"pad.userlist.unnamed": "بې نومه",
"pad.impexp.importbutton": "اوس واردول",
"pad.impexp.importing": "په واردولو کې دی...",
"pad.impexp.importbutton": "اوس رالېږدول",
"pad.impexp.importing": "رالېدول...",
"pad.impexp.uploadFailed": "راپورته‌کول نابرياله شول، مهرباني وکړئ بيا هڅه وکړئ.",
"pad.impexp.importfailed": "رالېږدول نابرياله شو",
"pad.impexp.copypaste": "لطفاً لمېسل لېښل ترسره کړئ"
}

View file

@ -4,7 +4,8 @@
"AmaryllisGardener",
"CiphriusKane",
"John Reid",
"Nintendofan885"
"Nintendofan885",
"Tensama0415"
]
},
"index.newPad": "New Pad",
@ -72,7 +73,7 @@
"pad.modals.corruptPad.cause": "This micht be cause o ae wrang server confeeguration or some ither onexpected behavior. Please contact the service admeenistrater.",
"pad.modals.deleted": "Deletit.",
"pad.modals.deleted.explanation": "This pad has been hif't.",
"pad.modals.disconnected": "Ye'v been disconnected.",
"pad.modals.disconnected": "切断されました。",
"pad.modals.disconnected.explanation": "The connection til the server wis loast",
"pad.modals.disconnected.cause": "The server micht be onavailable. Please notify the service admeenistrater gif this continues tae happen.",
"pad.share": "Share this pad",

View file

@ -7,17 +7,85 @@
"Mark",
"Rudko",
"Teslaton",
"Wizzard",
"Yardom78"
]
},
"admin.page-title": "Ovládací panel správu - Etherpad",
"admin.loading": "Načítava sa…",
"admin.loading_description": "Počkajte, prosím, kým sa stránka načíta.",
"admin.toggle_sidebar": "Prepnúť bočný panel",
"admin.shout": "Komunikácia",
"admin_shout.online_one": "Momentálne je online {{count}} používateľ",
"admin_shout.online_other": "Momentálne je online {{count}} používateľov",
"admin_shout.sticky_toggle": "Zmeniť pripnutú správu",
"admin_login.title": "Etherpad",
"admin_login.username": "Používateľské meno",
"admin_login.password": "Heslo",
"admin_login.submit": "Prihlásiť sa",
"admin_login.failed": "Prihlásenie zlyhalo",
"admin_pads.all_pads": "Všetky pady",
"admin_pads.bulk.cleanup_history": "Vyčistiť históriu",
"admin_pads.bulk.clear_selection": "Zrušiť výber",
"admin_pads.bulk.delete": "Zmazať",
"admin_pads.cancel": "Zrušiť",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Revízie",
"admin_pads.col.users": "Používatelia",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Zvoľte názov nového padu.",
"admin_pads.delete_pad_dialog_description": "Potvrďte alebo zrušte zmazanie padu.",
"admin_pads.delete_pad_dialog_title": "Zmazať pad",
"admin_pads.empty_never_edited": "prázdny · nikdy neupravovaný",
"admin_pads.error_dialog_description": "Vyskytla sa chyba.",
"admin_pads.error_prefix": "Chyba",
"admin_pads.filter.active": "Aktívne",
"admin_pads.filter.all": "Všetky",
"admin_pads.filter.empty": "Prázdne",
"admin_pads.filter.recent": "Tento týždeň",
"admin_pads.filter.stale": "Neaktívne (>1 r.)",
"admin_pads.open": "Otvoriť",
"admin_pads.pagination.next": "Ďalej",
"admin_pads.pagination.previous": "Späť",
"admin_pads.refresh": "Obnoviť",
"admin_pads.relative.days": "pred {{count}} d",
"admin_pads.relative.hours": "pred {{count}} h",
"admin_pads.relative.just_now": "práve teraz",
"admin_pads.relative.minutes": "pred {{count}} min",
"admin_pads.relative.months": "pred {{count}} mes.",
"admin_pads.relative.weeks": "pred {{count}} týž.",
"admin_pads.relative.years": "pred {{count}} r.",
"admin_pads.revisions_count": "{{count}} revízií",
"admin_pads.selected_count": "Vybraté: {{count}}",
"admin_pads.show": "Zobraziť",
"admin_pads.sort.name": "Názov (A Z)",
"admin_pads.sort.revision_number": "Revízie",
"admin_pads.sort.user_count": "Používatelia",
"admin_pads.stats.across_pads": "vo všetkých padoch",
"admin_pads.stats.active_users": "Aktívni používatelia",
"admin_pads.stats.empty_pads": "Prázdne pady",
"admin_pads.stats.last_activity": "Posledná aktivita",
"admin_pads.stats.no_active_users": "Žiadni aktívni používatelia",
"admin_pads.stats.revisions_zero": "0 revízií",
"admin_pads.stats.total": "Pady spolu",
"admin_pads.stats.users_active": "Momentálne aktívnych: {{count}}",
"admin_pads.subtitle": "Prehľad všetkých padov na tejto inštancii Etherpadu. Vyhľadávajte, čistite, otvárajte.",
"admin_plugins": "Správca doplnkov",
"admin_plugins.available": "Dostupné doplnky",
"admin_plugins.available_not-found": "Doplnky neboli nájdené.",
"admin_plugins.available_fetching": "Načítavanie...",
"admin_plugins.available_install.value": "Inštalovať",
"admin_plugins.available_search.placeholder": "Vyhľadať doplnky na inštaláciu",
"admin_plugins.check_updates": "Skontrolovať aktualizácie",
"admin_plugins.core_count": "{{count}} základných",
"admin_plugins.catalog_disabled": "Katalóg zásuvných modulov je vypnutý vaším prevádzkovateľom (privacy.pluginCatalog=false). Zásuvný modul nainštalujete spustením `pnpm run plugins i ep_<name>` na serveri.",
"admin_plugins.crumbs": "Zásuvné moduly",
"admin_plugins.description": "Popis",
"admin_plugins.disables.label": "Vypína:",
"admin_plugins.disables.warning_title": "Tento zásuvný modul zámerne odstraňuje uvedené funkcie Etherpadu.",
"admin_plugins.error_retrieving": "Chyba pri získavaní zásuvných modulov",
"admin_plugins.install_error": "Nepodarilo sa nainštalovať {{plugin}}: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Nemožno nainštalovať {{plugin}}: vyžaduje novšiu verziu Etherpadu. Aktualizujte Etherpad a skúste to znova.",
"admin_plugins.installed": "Nainštalované doplnky",
"admin_plugins.installed_fetching": "Načítavanie nainštalovaných doplnkov...",
"admin_plugins.installed_nothing": "Ešte ste nenainštalovali žiadne doplnky.",
@ -25,27 +93,161 @@
"admin_plugins.last-update": "Posledná aktualizácia",
"admin_plugins.name": "Názov",
"admin_plugins.page-title": "Správca doplnkov - Etherpad",
"admin_plugins.reload_catalog": "Znovu načítať katalóg",
"admin_plugins.search_npm": "Hľadať na npm",
"admin_plugins.sort_ascending": "Zoradiť vzostupne",
"admin_plugins.sort_descending": "Zoradiť zostupne",
"admin_plugins.sort.last_updated": "Naposledy aktualizované",
"admin_plugins.sort.name": "Názov (A Z)",
"admin_plugins.sort.version": "Verzia",
"admin_plugins.source": "Zdroj zásuvného modulu",
"admin_plugins.subtitle": "Inštalujte, aktualizujte a odstraňujte zásuvné moduly Etherpadu. Zmeny si vyžadujú reštart servera.",
"admin_plugins.tag_core": "Základné",
"admin_plugins.update_tooltip": "Aktualizovať",
"admin_plugins.updates_available": "Dostupné aktualizácie",
"admin_plugins.update_now": "Aktualizovať",
"admin_plugins.version": "Verzia",
"admin_plugins_info": "Informácie k riešeniu problémov",
"admin_plugins_info.bindings_label": "{{count}} väzieb",
"admin_plugins_info.copy_diagnostics": "Kopírovať diagnostiku",
"admin_plugins_info.copy_value": "Kopírovať {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hook_bindings": "Väzby hookov",
"admin_plugins_info.hooks": "Nainštalované súčasti",
"admin_plugins_info.hooks_client": "Súčasti na strane klienta",
"admin_plugins_info.hooks_server": "Súčasti na strane servera",
"admin_plugins_info.no_hooks": "Nenašli sa žiadne hooky",
"admin_plugins_info.parts": "Nainštalované súčasti",
"admin_plugins_info.plugins": "Nainštalované doplnky",
"admin_plugins_info.page-title": "Informácie o doplnkoch - Etherpad",
"admin_plugins_info.search_placeholder": "Hľadať hook alebo časť…",
"admin_plugins_info.subtitle": "Diagnostika systému: nainštalovaná verzia, registrované časti a hooky.",
"admin_plugins_info.tab_client": "Klient",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aktuálne",
"admin_plugins_info.update_available": "Dostupná aktualizácia: {{version}}",
"admin_plugins_info.version": "Verzia Etherpadu",
"admin_plugins_info.version_latest": "Posledná dostupná verzia",
"admin_plugins_info.version_number": "Číslo verzie",
"admin_settings": "Nastavenia",
"admin_settings.create_pad": "Vytvoriť pad",
"admin_settings.current": "Aktuálne nastavenia",
"admin_settings.current_example-devel": "Príklad šablóny vývojárskeho nastavenia",
"admin_settings.current_example-prod": "Príklad šablóny výrobného nastavenia",
"admin_settings.current_restart.value": "Reštartovať Etherpad",
"admin_settings.current_save.value": "Uložiť nastavenia",
"admin_settings.invalid_json": "Neplatný JSON",
"admin_settings.current_test.value": "Overiť JSON",
"admin_settings.current_prettify.value": "Sformátovať JSON",
"admin_settings.toast.saved": "Nastavenia boli úspešne uložené.",
"admin_settings.toast.save_failed": "Uloženie zlyhalo: súbor settings.json sa nepodarilo zapísať.",
"admin_settings.toast.json_invalid": "Syntaktická chyba: skontrolujte čiarky, zátvorky a úvodzovky.",
"admin_settings.toast.disconnected": "Nemožno uložiť: bez pripojenia k serveru.",
"admin_settings.toast.validation_ok": "JSON je platný.",
"admin_settings.toast.validation_failed": "JSON je neplatný: opravte syntaktické chyby.",
"admin_settings.toast.prettify_failed": "Nemožno sformátovať: najprv opravte syntaktické chyby.",
"admin_settings.prettify_confirm": "Formátovaním sa odstránia všetky komentáre. Pokračovať?",
"admin_settings.mode.form": "Formulár",
"admin_settings.mode.raw": "Surový",
"admin_settings.mode.effective": "Efektívny",
"admin_settings.mode.effective_tooltip": "Zobrazenie hodnôt, ktoré Etherpad práve teraz skutočne používa (po dosadení premenných prostredia), iba na čítanie. Tajné hodnoty sú skryté.",
"admin_settings.mode.aria_label": "Režim editora",
"admin_settings.envvar_banner.title": "Tento súbor je šablóna, nie živá konfigurácia.",
"admin_settings.envvar_banner.body": "Zástupné výrazy ako ${VAR:default} sa pri štarte dosadia do pamäte; nikdy sa nezapisujú späť do tohto súboru. Ak chcete zmeniť výslednú hodnotu, upravte premenné prostredia vo svojom prostredí (Docker compose, systemd, .env), alebo tu nahraďte zástupný výraz konkrétnou hodnotou. Prepnutím na kartu Efektívny zistíte, čo Etherpad práve používa.",
"admin_settings.toast.auth_error": "Nie ste overený ako správca. Prihláste sa, prosím, znova.",
"admin_settings.section.general": "Všeobecné",
"admin_settings.parse_error.title": "Nemožno spracovať settings.json",
"admin_settings.parse_error.cta": "Prepnite na surový režim na úpravu",
"admin_settings.env_pill.tooltip": "Číta sa z premennej prostredia {{variable}}. Hodnota nižšie sa použije, keď {{variable}} nie je nastavená.",
"admin_settings.env_pill.default_label": "predvolené",
"admin_settings.env_pill.input_aria": "Predvolená hodnota pre {{variable}}",
"admin_settings.env_pill.runtime_label": "aktívna hodnota",
"admin_settings.env_pill.runtime_tooltip": "Etherpad práve používa túto hodnotu, určenú z {{variable}} alebo jej predvolenej hodnoty.",
"admin_settings.env_pill.redacted_tooltip": "Etherpad používa hodnotu pre {{variable}}, je však skrytá, pretože ide o tajnú hodnotu.",
"admin_settings.page-title": "Nastavenia - Etherpad",
"admin_settings.save_error": "Chyba pri ukladaní nastavení",
"admin_settings.saved_success": "Nastavenia boli úspešne uložené",
"update.banner.title": "Dostupná aktualizácia",
"update.banner.body": "Je dostupný Etherpad {{latest}} (používate {{current}}).",
"update.banner.cta": "Zobraziť aktualizáciu",
"update.page.title": "Aktualizácie Etherpadu",
"update.page.current": "Aktuálna verzia",
"update.page.latest": "Najnovšia verzia",
"update.page.last_check": "Naposledy skontrolované",
"update.page.install_method": "Spôsob inštalácie",
"update.page.tier": "Úroveň aktualizácií",
"update.page.changelog": "Zoznam zmien",
"update.page.up_to_date": "Používate najnovšiu verziu.",
"update.page.disabled": "Kontroly aktualizácií sú vypnuté (updates.tier = „off“).",
"update.page.unauthorized": "Nemáte oprávnenie zobraziť stav aktualizácií.",
"update.page.error": "Nepodarilo sa načítať stav aktualizácií (stav {{status}}).",
"update.badge.severe": "Etherpad na tomto serveri je vážne zastaraný. Upozornite správcu.",
"update.badge.vulnerable": "Etherpad na tomto serveri používa verziu so známymi bezpečnostnými problémami. Upozornite správcu.",
"update.page.apply": "Použiť aktualizáciu",
"update.page.cancel": "Zrušiť",
"update.page.acknowledge": "Potvrdiť",
"update.page.log": "Záznam aktualizácie (posledných 200 riadkov)",
"update.page.execution": "Stav",
"update.page.policy.install-method-not-writable": "Aktualizácie zo správcovského rozhrania vyžadujú inštaláciu cez git. Aktualizujte cez svojho správcu balíkov.",
"update.page.policy.rollback-failed-terminal": "Predchádzajúca aktualizácia zlyhala a nepodarilo sa ju vrátiť späť. Po uvedení inštalácie do poriadku stlačte Potvrdiť, čím sa zámok zruší.",
"update.page.policy.up-to-date": "Používate najnovšiu verziu.",
"update.page.policy.tier-off": "Aktualizácie sú vypnuté (updates.tier = „off“).",
"update.page.policy.maintenance-window-missing": "Úroveň 4 (autonómna) vyžaduje okno údržby. Autonómne aktualizácie zapnete nastavením updates.maintenanceWindow v settings.json.",
"update.page.policy.maintenance-window-invalid": "Úroveň 4 (autonómna) je vypnutá, pretože updates.maintenanceWindow má nesprávny formát. Očakáva sa {start, end, tz} s časmi vo formáte HH:MM a tz „local“ alebo „utc“.",
"update.page.last_result.verified": "Posledná aktualizácia na {{tag}} bola overená.",
"update.page.last_result.rolled-back": "Posledná pokusná aktualizácia na {{tag}} bola vrátená späť: {{reason}}.",
"update.page.last_result.rollback-failed": "Posledný pokus o aktualizáciu zlyhal A zlyhalo aj vrátenie späť: {{reason}}. Vyžaduje sa ručný zásah.",
"update.page.last_result.preflight-failed": "Posledná pokusná aktualizácia na {{tag}} zlyhala pri kontrole pred spustením: {{reason}}.",
"update.page.last_result.cancelled": "Poslednú pokusnú aktualizáciu na {{tag}} zrušil správca.",
"update.execution.idle": "Nečinné",
"update.execution.scheduled": "Aktualizácia naplánovaná",
"update.execution.preflight": "Kontroly pred spustením",
"update.execution.preflight-failed": "Kontrola pred spustením zlyhala",
"update.execution.draining": "Ukončovanie relácií",
"update.execution.executing": "Aktualizuje sa…",
"update.execution.pending-verification": "Čaká sa na overenie",
"update.execution.verified": "Overené",
"update.execution.rolling-back": "Vracia sa späť",
"update.execution.rolled-back": "Vrátené späť",
"update.execution.rollback-failed": "Vrátenie späť zlyhalo",
"update.banner.terminal.rollback-failed": "Pokus o aktualizáciu zlyhal a nepodarilo sa ho vrátiť späť. Vyžaduje sa ručný zásah.",
"update.banner.scheduled": "Automatická aktualizácia na {{tag}} je naplánovaná — použije sa o {{remaining}}.",
"update.banner.maintenance-window-missing": "Autonómne aktualizácie sú vypnuté, kým sa nenastaví okno údržby.",
"update.banner.maintenance-window-invalid": "Autonómne aktualizácie sú vypnuté, pretože okno údržby má nesprávny formát.",
"update.page.scheduled.title": "Aktualizácia naplánovaná",
"update.page.scheduled.countdown": "Etherpad sa začne aktualizovať na {{tag}} o {{remaining}}.",
"update.page.scheduled.deferred_until": "Mimo okna údržby. Aktualizácia sa spustí, keď sa okno otvorí o {{at}}.",
"update.page.scheduled.apply_now": "Použiť teraz",
"update.window.title": "Okno údržby",
"update.window.summary": "{{start}} {{end}} ({{tz}})",
"update.window.unset": "Nie je nastavené.",
"update.window.next_opens_at": "Ďalšie okno sa otvorí o {{at}}.",
"update.drain.t60": "Etherpad sa o 60 sekúnd reštartuje, aby použil aktualizáciu.",
"update.drain.t30": "Etherpad sa o 30 sekúnd reštartuje, aby použil aktualizáciu.",
"update.drain.t10": "Etherpad sa o 10 sekúnd reštartuje, aby použil aktualizáciu.",
"index.newPad": "Nový poznámkový blok",
"index.createOpenPad": "alebo vytvoriť/otvoriť poznámkový blok s názvom:",
"index.settings": "Nastavenia",
"index.transferSessionTitle": "Preniesť reláciu",
"index.receiveSessionTitle": "Prijať reláciu",
"index.receiveSessionDescription": "Tu môžete prijať reláciu Etherpadu z iného prehliadača alebo zariadenia. Upozorňujeme však, že tým sa odstráni vaša súčasná relácia, ak nejakú máte.",
"index.transferSession": "1. Preniesť reláciu",
"index.transferSessionNow": "Preniesť reláciu teraz",
"index.copyLink": "2. Kopírovať odkaz",
"index.copyLinkDescription": "Kliknutím na tlačidlo nižšie skopírujete odkaz do schránky.",
"index.copyLinkButton": "Kopírovať odkaz do schránky",
"index.transferToSystem": "3. Skopírovať reláciu do nového systému",
"index.transferToSystemDescription": "Otvorením skopírovaného odkazu v cieľovom prehliadači alebo zariadení prenesiete svoju reláciu.",
"index.code": "Kód",
"index.transferSessionDescription": "Kliknutím na tlačidlo nižšie prenesiete svoju súčasnú reláciu do prehliadača alebo zariadenia. Skopíruje sa odkaz na stránku, ktorá po otvorení v cieľovom prehliadači alebo zariadení prenesie vašu reláciu.",
"index.createOpenPad": "Otvoriť pad podľa názvu",
"index.openPad": "otvoriť poznámkový blok s názvom:",
"index.recentPads": "Nedávne pady",
"index.recentPadsEmpty": "Nenašli sa žiadne nedávne pady.",
"index.generateNewPad": "Vygenerovať náhodný názov padu",
"index.labelPad": "Názov padu (nepovinné)",
"index.placeholderPadEnter": "Zadajte názov padu…",
"index.createAndShareDocuments": "Vytvárajte a zdieľajte dokumenty v reálnom čase",
"index.createAndShareDocumentsDescription": "Etherpad vám umožňuje spoločne upravovať dokumenty v reálnom čase, podobne ako živý viacpoužívateľský editor, ktorý beží vo vašom prehliadači.",
"pad.toolbar.bold.title": "Tučné (Ctrl+B)",
"pad.toolbar.italic.title": "Kurzíva (Ctrl+I)",
"pad.toolbar.underline.title": "Podčiarknuté (Ctrl+U)",
@ -62,22 +264,42 @@
"pad.toolbar.savedRevision.title": "Uložiť revíziu",
"pad.toolbar.settings.title": "Nastavenia",
"pad.toolbar.embed.title": "Zdieľať alebo vložiť tento poznámkový blok",
"pad.toolbar.home.title": "Späť na domovskú stránku",
"pad.toolbar.showusers.title": "Zobraziť používateľov tohoto poznámkového bloku",
"pad.colorpicker.save": "Uložiť",
"pad.colorpicker.cancel": "Zrušiť",
"pad.loading": "Načítava sa...",
"pad.noCookie": "Cookie nebolo možné nájsť. Povoľte prosím cookies vo vašom prehliadači. Vaše sedenie a nastavenia sa medzi návštevami stránky neuložia. To môže byť spôsobené tým že Etherpad je zahrnutý do iFrame v niektorých prehliadačoch. Prosím uistite sa, že Etherpad sa nachádza na tej istej doméne ako hlavný iFrame",
"pad.permissionDenied": "Ľutujeme, nemáte oprávnenie pristupovať k tomuto poznámkovému bloku",
"pad.settings.padSettings": "Nastavenia poznámkového bloku",
"pad.settings.title": "Nastavenia",
"pad.settings.padSettings": "Nastavenia pre celý pad",
"pad.settings.userSettings": "Používateľské nastavenia",
"pad.settings.myView": "Vlastný pohľad",
"pad.settings.disablechat": "Vypnúť chat",
"pad.settings.darkMode": "Tmavý režim",
"pad.settings.stickychat": "Rozhovor stále na obrazovke",
"pad.settings.chatandusers": "Zobraziť rozhovor a používateľov",
"pad.settings.colorcheck": "Farby autorov",
"pad.settings.fadeInactiveAuthorColors": "Stlmiť farby neaktívnych autorov",
"pad.settings.linenocheck": "Čísla riadkov",
"pad.settings.rtlcheck": "Čítať obsah sprava doľava?",
"pad.settings.enforceSettings": "Vynútiť nastavenia pre ostatných používateľov",
"pad.settings.enforcedNotice": "Tieto nastavenia vám uzamkol tvorca tohto padu. Ak ich potrebujete zmeniť, požiadajte tvorcu padu.",
"pad.settings.fontType": "Typ písma:",
"pad.settings.fontType.normal": "Normálne",
"pad.settings.language": "Jazyk:",
"pad.settings.deletePad": "Zmazať pad",
"pad.delete.confirm": "Naozaj chcete zmazať tento pad?",
"pad.deletionToken.modalTitle": "Uložte si token na zmazanie padu",
"pad.deletionToken.modalBody": "Tento token je jediný spôsob, ako zmazať tento pad, ak stratíte reláciu prehliadača alebo zmeníte zariadenie. Uložte si ho na bezpečné miesto — zobrazí sa tu iba raz.",
"pad.deletionToken.copy": "Kopírovať",
"pad.deletionToken.copied": "Skopírované",
"pad.deletionToken.acknowledge": "Uložil som si ho",
"pad.deletionToken.deleteWithToken": "Zmazať pad pomocou tokenu",
"pad.deletionToken.tokenFieldLabel": "Token na zmazanie padu",
"pad.deletionToken.tokenValueLabel": "Váš token na zmazanie padu (iba na čítanie)",
"pad.deletionToken.invalid": "Tento token nie je platný pre tento pad.",
"pad.deletionToken.notCreator": "Nie ste tvorcom tohto padu, takže ho nemôžete zmazať.",
"pad.settings.about": "O Etherpade",
"pad.settings.poweredBy": "Poháňané cez",
"pad.importExport.import_export": "Import/Export",
@ -90,6 +312,13 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Exportovať ako Etherpad",
"pad.importExport.exporthtmla.title": "Exportovať ako HTML",
"pad.importExport.exportplaina.title": "Exportovať ako čistý text",
"pad.importExport.exportworda.title": "Exportovať ako Microsoft Word",
"pad.importExport.exportpdfa.title": "Exportovať ako PDF",
"pad.importExport.exportopena.title": "Exportovať ako ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Importovať môžete iba z formátov čistého textu alebo HTML. Pre pokročilejšie funkcie importu si <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">nainštalujte LibreOffice</a>.",
"pad.modals.connected": "Pripojené.",
"pad.modals.reconnecting": "Opätovné pripájanie k vášmu poznámkovému bloku...",
"pad.modals.forcereconnect": "Vynútiť znovupripojenie",
@ -120,6 +349,8 @@
"pad.modals.disconnected": "Boli ste odpojení.",
"pad.modals.disconnected.explanation": "Spojenie so serverom sa prerušilo",
"pad.modals.disconnected.cause": "Server môže byť nedostupný. Ak by problém pretrvával, informujte správcu služby.",
"pad.gritter.unacceptedCommit.title": "Neuložená úprava",
"pad.gritter.unacceptedCommit.text": "Vaša nedávna úprava stále nie je uložená. Znova sa pripojte a skúste to ešte raz.",
"pad.share": "Zdieľať tento poznámkový blok",
"pad.share.readonly": "Len na čítanie",
"pad.share.link": "Odkaz",
@ -132,12 +363,36 @@
"timeslider.followContents": "Sledovať aktualizácie obsahu poznámkového bloku",
"timeslider.pageTitle": "Časová os {{appTitle}}",
"timeslider.toolbar.returnbutton": "Späť do poznámkového bloku",
"pad.historyMode.banner": "Prezeranie histórie",
"pad.historyMode.return": "Návrat k živej verzii",
"pad.historyMode.revisionLabel": "Revízia {{rev}}",
"pad.historyMode.controlsLabel": "Ovládanie histórie padu",
"pad.historyMode.sliderLabel": "Revízia padu",
"pad.historyMode.settings.title": "Prehrávanie histórie",
"pad.historyMode.settings.follow": "Sledovať zmeny obsahu padu",
"pad.historyMode.settings.followShort": "Sledovať",
"pad.historyMode.followOn": "Sledujú sa zmeny padu — kliknutím sledovanie ukončíte",
"pad.historyMode.followOff": "Zmeny padu sa nesledujú — kliknutím začnete sledovať",
"pad.historyMode.settings.playbackSpeed": "Rýchlosť prehrávania:",
"pad.historyMode.chat.replayHeader": "Chat k {{time}}",
"pad.historyMode.users.authorsHeader": "Autori v tejto revízii",
"pad.editor.skipToContent": "Preskočiť na editor",
"pad.editor.keyboardHint": "Stlačením Escape opustíte editor. Stlačením Alt+F9 sa dostanete na panel nástrojov.",
"pad.editor.toolbar.formatting": "Panel nástrojov formátovania",
"pad.editor.toolbar.actions": "Panel akcií padu",
"pad.editor.toolbar.showMore": "Zobraziť viac tlačidiel panela",
"timeslider.toolbar.authors": "Autori:",
"timeslider.toolbar.authorsList": "Bez autorov",
"timeslider.toolbar.exportlink.title": "Export",
"timeslider.exportCurrent": "Exportovať aktuálnu verziu ako:",
"timeslider.version": "Verzia {{version}}",
"timeslider.saved": "Uložené {{day}}. {{month}} {{year}}",
"timeslider.settings.playbackSpeed": "Rýchlosť prehrávania:",
"timeslider.settings.playbackSpeed.original": "Pôvodná rýchlosť",
"timeslider.settings.playbackSpeed.realtime": "Reálny čas",
"timeslider.settings.playbackSpeed.200ms": "200 ms",
"timeslider.settings.playbackSpeed.500ms": "500 ms",
"timeslider.settings.playbackSpeed.1000ms": "1000 ms",
"timeslider.playPause": "Pustiť / Pozastaviť obsah poznámkového bloku",
"timeslider.backRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu späť",
"timeslider.forwardRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu vpred",
@ -159,6 +414,7 @@
"pad.savedrevs.timeslider": "Návštevou časovej osi môžete zobraziť uložené revízie",
"pad.userlist.entername": "Zadajte svoje meno",
"pad.userlist.unnamed": "nemenovaný",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} pripojený používateľ, few: {{count}} pripojení používatelia, many: {{count}} pripojených používateľov, other: {{count}} pripojených používateľov ]}",
"pad.editbar.clearcolors": "Odstrániť farby autorov z celého dokumentu? Táto akcia sa nedá vrátiť",
"pad.impexp.importbutton": "Importovať teraz",
"pad.impexp.importing": "Prebieha import...",
@ -169,5 +425,6 @@
"pad.impexp.importfailed": "Import zlyhal",
"pad.impexp.copypaste": "Vložte prosím kópiu cez schránku",
"pad.impexp.exportdisabled": "Export do formátu {{type}} nie je povolený. Kontaktujte prosím administrátora pre zistenie detailov.",
"pad.impexp.maxFileSize": "Súbor je príliš veľký. Kontaktujte správcu pre zväčšenie povolenej veľkosti súborov pre import"
"pad.impexp.maxFileSize": "Súbor je príliš veľký. Kontaktujte správcu pre zväčšenie povolenej veľkosti súborov pre import",
"pad.social.description": "Kolaboratívny dokument, ktorý môže každý upravovať v reálnom čase."
}

View file

@ -18,6 +18,7 @@
"Stang",
"TFX202X",
"VulpesVulpes825",
"XtexChooser",
"Yfdyh000",
"乌拉跨氪",
"列维劳德",
@ -26,6 +27,30 @@
]
},
"admin.page-title": "管理员面板 - Etherpad",
"admin.loading": "正在加载…",
"admin.loading_description": "页面加载中,请稍安勿躁。",
"admin.toggle_sidebar": "开关侧边栏",
"admin.shout": "通讯",
"admin_shout.online_one": "目前有{{count}}位用户在线",
"admin_shout.online_other": "目前有{{count}}位用户在线",
"admin_shout.sticky_toggle": "更改置顶消息",
"admin_login.title": "Etherpad",
"admin_login.username": "用户名",
"admin_login.password": "密码",
"admin_login.submit": "登录",
"admin_login.failed": "登录失败",
"admin_pads.all_pads": "所有记事本",
"admin_pads.bulk.cleanup_history": "清理历史",
"admin_pads.bulk.clear_selection": "清除选择",
"admin_pads.bulk.delete": "删除",
"admin_pads.cancel": "取消",
"admin_pads.col.users": "用户",
"admin_pads.confirm_button": "确定",
"admin_pads.error_prefix": "错误",
"admin_pads.pagination.next": "下一页",
"admin_pads.pagination.previous": "上一页",
"admin_pads.refresh": "刷新",
"admin_pads.relative.just_now": "刚刚",
"admin_plugins": "插件管理器",
"admin_plugins.available": "可用插件",
"admin_plugins.available_not-found": "找不到插件。",

View file

@ -15,6 +15,16 @@
]
},
"admin.page-title": "管理員面板 - Etherpad",
"admin.loading": "載入中…",
"admin_login.title": "Etherpad",
"admin_login.username": "使用者名稱",
"admin_login.password": "密碼",
"admin_login.submit": "登入",
"admin_login.failed": "登入失敗",
"admin_pads.bulk.delete": "刪除",
"admin_pads.cancel": "取消",
"admin_pads.col.users": "使用者",
"admin_pads.error_prefix": "錯誤",
"admin_plugins": "外掛程式管理器",
"admin_plugins.available": "可用套件",
"admin_plugins.available_not-found": "沒有找到套件。",
@ -29,6 +39,9 @@
"admin_plugins.last-update": "最後更新",
"admin_plugins.name": "名稱",
"admin_plugins.page-title": "套件管理 - Etherpad",
"admin_plugins.update_tooltip": "更新",
"admin_plugins.updates_available": "有可用更新",
"admin_plugins.update_now": "更新",
"admin_plugins.version": "版本",
"admin_plugins_info": "問題排除資訊",
"admin_plugins_info.hooks": "已安裝的掛勾",
@ -37,6 +50,8 @@
"admin_plugins_info.parts": "已安裝部分",
"admin_plugins_info.plugins": "已安裝的套件",
"admin_plugins_info.page-title": "套件資訊 - Etherpad",
"admin_plugins_info.tab_client": "客戶端",
"admin_plugins_info.tab_server": "伺服器",
"admin_plugins_info.version": "Etherpad 版本",
"admin_plugins_info.version_latest": "最新可用版本",
"admin_plugins_info.version_number": "版本號",
@ -46,6 +61,9 @@
"admin_settings.current_example-prod": "生產設定模板範例",
"admin_settings.current_restart.value": "重新啟動 Etherpad",
"admin_settings.current_save.value": "儲存設定",
"admin_settings.invalid_json": "無效 JSON",
"admin_settings.current_test.value": "驗證 JSON",
"admin_settings.current_prettify.value": "美化 JSON 格式",
"admin_settings.page-title": "設定 - Etherpad",
"index.newPad": "新記事本",
"index.settings": "設定",
@ -116,6 +134,11 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF開放文件格式",
"pad.importExport.exporthtmla.title": "匯出成 HTML",
"pad.importExport.exportplaina.title": "匯出成純文字",
"pad.importExport.exportworda.title": "匯出成 Microsoft Word",
"pad.importExport.exportpdfa.title": "匯出成 PDF",
"pad.importExport.exportopena.title": "匯出成 ODF開放文件格式",
"pad.modals.connected": "已連線。",
"pad.modals.reconnecting": "重新連線到您的記事本…",
"pad.modals.forcereconnect": "強制重新連線",

View file

@ -414,7 +414,13 @@ exports.appendChatMessage = async (padID: string, text: string|object, authorID:
time = Date.now();
}
// @TODO - missing getPadSafe() call ?
// Reject messages addressed to a pad that doesn't exist. Without this check
// the downstream padManager.getPad() would create the pad on demand with
// default content, so the documented {code:1,"padID does not exist"} result
// would never be returned.
if (!await padManager.doesPadExists(padID)) {
throw new CustomError('padID does not exist', 'apierror');
}
// save chat message to database and send message to all connected clients
await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
@ -545,10 +551,11 @@ exports.createPad = async (padID: string, text: string, authorId = '') => {
// create pad
await getPadSafe(padID, false, text, authorId);
// When requireAuthentication is on, every creator has a stable identity, so
// the cookie/identity path covers recovery and the one-time token is just
// an extra surface to leak.
const deletionToken = settings.requireAuthentication
// No recovery token when it cannot help: requireAuthentication gives every
// creator a stable identity, and allowPadDeletionByAllUsers lets anyone delete
// the pad with no token at all (issue #7926). Either way the token is just an
// extra surface to leak.
const deletionToken = settings.requireAuthentication || settings.allowPadDeletionByAllUsers
? null
: await padDeletionManager.createDeletionTokenIfAbsent(padID);
return {deletionToken};

View file

@ -590,25 +590,42 @@ class Pad {
Object.assign(this, value);
if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool);
} else {
if (text == null) {
// Auto-generated default content (settings.defaultPadText or whatever a
// padDefaultContent hook substitutes) is not written by the user who
// happens to open the pad first, so the text must not carry their author
// attribute — otherwise the welcome text shows up in the creator's
// authorship colour (issue #7885). Track whether the text came from the
// default-content path so its insert op can be attributed to the system
// author.
const usedDefaultContent = (text == null);
if (usedDefaultContent) {
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
await hooks.aCallAll('padDefaultContent', context);
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
}
// When the initial pad text is non-empty but no authorId was
// supplied (internal getPad calls during HTTP API setup,
// padDefaultContent flows, plugin-driven pad creation), fall back
// to the stable system author so the initial changeset's insert
// op carries an `author` attribute. Mirrors the same substitution
// The author *attribute* applied to the initial text — i.e. what colours
// it in the editor — is the stable system author when the content is
// auto-generated default text (#7885), or when non-empty text was
// supplied without an authorId (internal getPad calls during HTTP API
// setup, plugin-driven pad creation). The latter keeps the insert op
// carrying an `author` attribute, mirroring the substitution
// setText/appendText already do via spliceText.
const effectiveAuthorId =
(text.length > 0 && !authorId) ? Pad.SYSTEM_AUTHOR_ID : authorId;
const firstAttribs = effectiveAuthorId
? [['author', effectiveAuthorId] as [string, string]]
const attribAuthorId =
((usedDefaultContent || !authorId) && text.length > 0)
? Pad.SYSTEM_AUTHOR_ID : authorId;
const firstAttribs = attribAuthorId
? [['author', attribAuthorId] as [string, string]]
: undefined;
// The *revision* author (revs:0 meta.author) stays the real creator so
// pad ownership is preserved: isPadCreator() / the pad-wide settings gate
// and the deletion token all key off getRevisionAuthor(0). Only when no
// author was supplied at all do we fall back to the system author, so the
// initial revision still records a stable, non-empty author.
const revisionAuthorId =
authorId || (text.length > 0 ? Pad.SYSTEM_AUTHOR_ID : '');
const firstChangeset = makeSplice('\n', 0, 0, text, firstAttribs, this.pool);
await this.appendRevision(firstChangeset, effectiveAuthorId);
await this.appendRevision(firstChangeset, revisionAuthorId);
}
this.padSettings = Pad.normalizePadSettings(this.padSettings);
await hooks.aCallAll('padLoad', {pad: this});
@ -851,6 +868,8 @@ class Pad {
await this.saveToDatabase();
}
// Returns the newly created saved revision, or undefined if this revision
// was already saved (so callers can broadcast only genuine additions).
async addSavedRevision(revNum: string, savedById: string, label: string) {
// if this revision is already saved, return silently
for (const i in this.savedRevisions) {
@ -870,6 +889,7 @@ class Pad {
// save this new saved revision
this.savedRevisions.push(savedRevision);
await this.saveToDatabase();
return savedRevision;
}
getSavedRevisions() {

View file

@ -192,7 +192,15 @@ exports.sanitizePadId = async (padId: string) => {
return padId;
};
exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
// Pad IDs consisting only of URL "dot-segments" ('.' or '..') are unreachable:
// per the WHATWG URL standard a browser normalises "/p/." to "/p/" and "/p/.."
// to "/", so the pad can never be opened or exported — the request arrives as
// "/p/" and Etherpad answers "Cannot GET /p/". Reject them so such (broken) pads
// can never be created.
const dotSegmentPadId = /^\.{1,2}$/;
exports.isValidPadId = (padId: string) =>
/^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId) && !dotSegmentPadId.test(padId);
/**
* Removes the pad from database and unloads it.

Some files were not shown because too many files have changed in this diff Show more