mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
9738 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3113c5096f
|
ci(rate-limit): wait for etherpad readiness before running test (#7726)
* ci(rate-limit): wait for etherpad to be ready before running the test The workflow starts the etherpad container in the background, then runs `pnpm install`, then runs the test. On a warm pnpm-store the install can finish before etherpad is listening on 9001, at which point nginx returns 502 for the test request and the run fails. Recent README-only commits on develop hit this race on three consecutive runs. Poll the nginx-proxied endpoint (port 8081 — also what the test uses) until it stops returning 5xx, with a 2-minute timeout and `docker logs etherpad-docker` on giving up to make diagnosis straightforward. * ci(rate-limit): address Qodo review (nginx logs, tighter timeout) - Name the nginx container so its logs can be captured when the readiness poll times out — previously nginx was started anonymously and a 502 caused by nginx itself (rather than etherpad) would have been hard to diagnose from the workflow log alone. - On timeout also dump `docker ps -a` for container-state visibility. - Tighten the readiness wait: 30 iterations × (1s curl timeout + 1s sleep) gives ~60s budget instead of ~240s, which is still well above observed cold-start time and keeps the failure-fast contract. |
||
|
|
8b14eb6eee
|
docs: Readme tidy (#7725)
* docs: Fix links in README! * docs: More readme tidy * docs: More readme tidy |
||
|
|
5692c117d7
|
Readme tidy (#7724)
* docs: Fix links in README! * docs: More readme tidy |
||
|
|
f784d2b01f
|
docs: Fix links in README! (#7723) | ||
|
|
113324c6be
|
Localisation updates from https://translatewiki.net. | ||
|
|
e56cb659d3
|
fix(tests): close socket.io clients in lowerCasePadIds spec (#7722)
This spec opened three socket.io-client connections (for ALREADYexistingPad, alreadyexistingpad, maliciousattempt) but never closed them. Each leaked client kept a 5-second reconnect timer armed in node_modules/.pnpm/socket.io-client/.../manager.js past mocha's "passing" output. That triggered the unclean-exit force-quit (server.ts setTimeout(..., 5000)) which then exits the process with code 1 — visible on Windows + Node 24 in CI and 100% reproducible locally on Node 24. Verified with `wtfnode.dump()` patched into the force-exit path: before this change, three timers at socket.io-client manager.js:375 were live; after, the same suite exits cleanly with code 0 and the force-exit timer never fires. The fix tracks every socket returned by common.connect() in an array and disconnects each one in afterEach. Refs: src/tests/backend/diagnostics.ts comment block, PR #7663. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d6a55c2283
|
feat(7642): bin/compactStalePads — staleness-gated bulk compaction (#7708)
* feat(7642): bin/compactStalePads — staleness-gated bulk compaction Adds bin/compactStalePads with --older-than / --keep / --dry-run. Composes listAllPads → getLastEdited → compactPad so hot pads in active timeslider use are left alone and only the cold tail is compacted. Targeting stays a CLI concern; compactPad's API surface is unchanged. Per-pad failures (including a getLastEdited fault) don't stop the run — same error-tolerance shape as compactAllPads. End-to-end test plumbs through the real /api/1.3.1/getLastEdited + compactPad endpoints to lock the adapter contract. Daily-cron variant (cleanup.compactOlderThanDays setting) deferred to a follow-up so this PR stays focused on the on-demand operator tool from the issue's primary acceptance bullet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7642): TOCTOU recheck before compaction + admin CLI docs Qodo flagged two real issues: 1. Race window between staleness selection and compaction. On a long bulk run a pad could become active between first-pass filtering and compactPad, which would then kick those sessions. Added a getLastEdited recheck right before each compact call; if the pad is now fresh it's reclassified as skippedFresh rather than failed (the user did the right thing — edited it — and we bow out). 2. doc/cli.md had nothing on pad compaction at all (gap predates this PR; #6194 landed without doc updates). Added a Pad compaction section covering all three CLIs — compactPad, compactAllPads, compactStalePads — so the toolset is discoverable as a unit. Tests cover both the recheck-skip path and a recheck-failure path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
49466a2870
|
build(deps): bump undici from 7.25.0 to 8.2.0 (#7701)
Bumps [undici](https://github.com/nodejs/undici) from 7.25.0 to 8.2.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.25.0...v8.2.0) --- updated-dependencies: - dependency-name: undici dependency-version: 8.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
41d10ecae5
|
chore: fixed admin design rework (#7716)
* chore: fixed admin design rework * chore: fixed qodoos remarks |
||
|
|
451bd9c3eb
|
feat(pad): scrub history in-place on the pad URL (#7659) (#7710)
* feat(pad): scrub history in-place on the pad URL (#7659) Clicking the timeslider toolbar button now keeps the user on /p/:pad and toggles a hash-based history mode (#rev/N) instead of navigating to a separate /timeslider page. The pad shell — chat, users panel, settings, plugin chrome — stays mounted across the transition. A sticky banner plus a sepia tint on the toolbar make it unmistakable that what is visible is historical, not live. Implementation: - New PadModeController (src/static/js/pad_mode.ts) owns enter/exit, the URL hash, browser back/forward, and a mutation-observer bridge from the inner timeslider's revision label/date into the outer banner. Esc and a Return-to-live button both exit history. - pad.html grows a banner element and an iframe mount slot. The live ACE iframe stays mounted but hidden during history; on exit the socket is still alive, so the user snaps straight back to the current state without a reconnect. - The /p/:pad/timeslider route 302-redirects to the pad page for direct visits (legacy bookmarks), and serves the timeslider HTML for the in-pad iframe when called with ?embed=1. The embedded variant hides the redundant title and return-to-pad button via CSS; the slider, settings, and export controls stay reachable. - Legacy #NN shortlinks are preserved through the redirect by the browser and translated to #rev/NN client-side. Tests: - New backend spec asserts the 302 redirect, pad-name preservation, and the ?embed=1 path still serves the timeslider HTML. - New padmode.spec.ts exercises toolbar entry, return-to-live, browser back, and direct /timeslider URL handling. Asserts the rendered localized banner string, not just element presence. - Existing timeslider specs that hit /p/:pad/timeslider directly now pass ?embed=1 to bypass the redirect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): make history iframe fill the editor area (#7659) Without an explicit positioning model the history-frame-mount inherited half-width from a phantom flex parent and the embedded timeslider rendered at 640×625 instead of the full editor area. Switch to the same absolute-fill model the live ACE iframe uses by making #editorcontainerbox the positioning anchor when in history mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): address Qodo review and CI failures (#7659) Concrete review fixes for PR #7710: - Tighten the embed query check from `if (!req.query.embed)` to `req.query.embed !== '1'` so values like `?embed=0` no longer bypass the redirect. - Fix the `#rev/latest` mapping: the parser yields -1 for "latest", which the iframe sync handler was clamping to 0 and so jumping the embedded timeslider to revision 0. Resolve "latest" to the inner BroadcastSlider's upper bound instead. - Update existing backend tests (`socialMeta`, `specialpages`) that hit `/p/:pad/timeslider` directly — they now pass `?embed=1` like the rest of the suite. Without this fix three pre-existing tests failed CI (302 instead of 200). - Document the route change in `doc/skins.md` and `doc/skins.adoc`: direct visits redirect; iframe consumers use `?embed=1`. - Back out a stray `data-theme="editorial"` attribute and the hardcoded Google Fonts `<link>` tags from `pad.html` that leaked into the branch from an unrelated working-tree change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): consolidate chrome and replay chat/users in history mode (#7659) Picks up the rough edges left by the initial in-place history mode: the embedded timeslider iframe was rendering its own duplicate Settings and Export buttons, and the chat panel + users list still showed live state while the editor scrubbed back in time. Chrome consolidation - Hide the entire inner editbar's right-side toolbar and modal popups in embedded mode (slider stays). Outer pad shell now owns Settings, Export, Share, Users, Chat across both modes. - Outer Settings popup grows a "History playback" section (visible only when scrubbing) with playback speed + follow-contents. Both bridge to the iframe's BroadcastSlider state. - Outer Export anchors are rewritten to /p/<pad>/<rev>/export/<type> on each scrub and restored on exit, so Save As exports the visible historical revision. Chat replay - Each chat message is annotated with data-timestamp at render time. In history mode, messages newer than the scrubbed revision's timestamp are display:none'd; a "Chat as of HH:MM" header sits above the chat log. - Restores cleanly on exit (inline display cleared, header removed). Users replay - Live users table is replaced with the embedded timeslider's authors-at-this-revision label while scrubbing; restored on exit. Plumbing - Expose padContents on window in broadcast.ts so the outer pad can read currentTime after each scrub without postMessage. - Expose BroadcastSlider on window in timeslider.ts so the outer pad can register an onSlider callback to drive replay UI. Tests - New padmode specs cover: history-only Settings section, hidden embedded chrome, chat filter + replay header, Export href rewriting + restore, authors-row swap + restore. - timeslider_line_numbers cookie-persistence test updated to bypass the now-hidden inner Settings popup (programmatic checkbox). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): theme propagation, hide inert buttons, plugin loading (#7659) Picks up rough edges from the in-place history mode that turned up in real usage: Theme / dark mode - skin_variants.updateSkinVariantsClasses now also walks the history iframe (and its ace_outer/ace_inner) so toggling dark mode while scrubbing re-themes the embedded view in lockstep. - timeslider.ts inherits the parent's skinVariant tokens (super-dark-* / dark-* / full-width-editor) on first paint when it detects it is embedded — same-origin guarantee, falls through silently if not. Toolbar UX - Hide #editbar .menu_left (Bold/Italic/Lists/Indent/Undo/...) and the show-more chevron while in history mode. Those buttons target the hidden live editor and would do nothing useful; rendering them disabled-looking implied state the user doesn't have. Right-side menu (Settings / Share / Users / Chat / Home) stays at full opacity and fully interactive. Slider position - Pin the embedded #editbar to the bottom of the iframe so the outer banner and the slider can't visually compete for the same band of pixels. Reserve padding-bottom on the iframe's editorcontainerbox so the editor never scrolls under the slider. Plugin loading in timeslider - timeSliderBootstrap.js now pre-loads plugin modules into a Map and passes them to plugins.update(), mirroring padBootstrap.js. Without this the loadFn fallback called require(path) at runtime, which the esbuild-bundled timeslider couldn't resolve, so client_hooks like ep_headings2's aceRegisterBlockElements silently failed to register and historical revisions rendered without plugin chrome. Tests - New padmode specs cover: outer toolbar's left/right asymmetry, slider pinned to bottom, dark-mode class propagation into the history iframe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): move history slider into the outer toolbar (#7659) The slider previously rendered inside the embedded iframe — first at the top (where it visually competed with the banner), then briefly at the bottom (where the chat icon overlapped it). Both were wrong. Move the controls into the outer toolbar's left zone, where #editbar .menu_left is hidden in history mode and the slider can occupy the full width without colliding with anything. - pad.html grows a #history-controls div (slider + play/pause/step buttons + timer) inside #editbar, between menu_left and menu_right. Hidden by default; revealed via body.history-mode CSS. - pad.css swaps #editbar .menu_left out for #history-controls in history mode (display:none / display:flex). - timeslider.css fully hides the embedded iframe's #editbar — the outer toolbar now owns the slider, and the iframe is purely the editor surface. - pad_mode.ts wires the outer controls as a remote control: the range input calls inner BroadcastSlider.setSliderPosition, the play button calls BroadcastSlider.playpause, step buttons forward clicks to the inner #leftstep/#rightstep so they share the existing logic. An onSlider subscription mirrors inner state back into the outer slider value, timer label, and play-button .pause class. Tests - Existing timeslider.spec asserts the outer controls are visible. - New padmode specs cover: inner editbar fully hidden, outer toolbar swap (menu_left → history-controls), and outer slider drives the iframe's revision via BroadcastSlider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): exempt embedded history iframe from userdup kick (#7659) When the in-place history iframe opens its socket, the server's duplicate-author kick treats it as a stale tab and disconnects the parent pad's live socket — toolbar-overlay drops over the editor and Settings/Share/Users/Chat all stop responding. Mark the iframe's connection with `embed=1` in the socket.io handshake query, record it on sessionInfo, and skip the kick whenever either side is embedded. - timeslider.ts: detect `?embed=1` (and parent !== window) on the iframe URL, pass through as a query parameter to socketio.connect. - PadMessageHandler: read socket.handshake.query.embed on CLIENT_READY, set sessionInfo.embed; the duplicate-author kick now skips when either the connecting session OR the existing session is embedded. Behavior preserved - Two real tabs (both non-embedded): older tab still gets kicked. - Authenticated sessions still bypass the kick entirely. - Live pad socket survives entry into history mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): a11y of history toolbar controls (#7659) The new history controls (slider + play/step/timer) had hardcoded English aria-labels, which html10n won't replace because they were present without the data-l10n-aria-label marker. Screen readers in non-English locales would have heard English. Drop the static aria labels and let html10n.translateElement populate aria-label from the data-l10n-id translation, matching how the rest of the toolbar works. - pad.html: remove hardcoded aria-label on play/step buttons and the range input; keep titles (hover tooltip) and data-l10n-id. Add role="toolbar" + data-l10n-id on the controls container so the toolbar landmark is announced. Mark play button as a toggle with aria-pressed reflecting playback state. - en.json: add pad.historyMode.controlsLabel and pad.historyMode.sliderLabel for the toolbar landmark and the slider. - pad_mode.ts: keep aria-pressed in sync with the inner playback state on every revision update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): a11y + responsive for history controls (#7659) Two issues with the previous a11y attempt: the data-l10n-id on icon buttons was setting their textContent (drawing "Playback / Pause Pad Contents" on screen next to the glyph), and there was no responsive treatment so the timer + slider could overflow narrow viewports. - pad.html: drop data-l10n-id from the icon buttons. They're now empty <button>s. Localized title (hover tooltip) and aria-label (screen reader name) are populated by pad_mode.localizeControls() using the existing timeslider.* keys, with an html10n.bind subscription so language switches re-localize. - Mark #history-timer as hide-for-mobile. - pad.css: dedicated @media (max-width: 800px) and 480px rules shrink padding, gap, and button widths so play + slider + step buttons stay on a single toolbar line at narrow viewports. Mirrors the legacy timeslider's responsive behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): inline Follow + Playback speed, match toolbar height (#7659) Two follow-ups from real testing: - Move "Follow pad content updates" (now "Follow") and "Playback speed" out of the Settings popup and inline them in the history-mode toolbar, alongside the slider + play/step buttons. They were always needed while scrubbing; one extra click into Settings was friction. Removed the now-empty #history-settings-section. - The history controls toolbar was visibly shorter than the live toolbar because the icon buttons sat as bare <button> elements without the live editbar's <li><a> wrapping. Add explicit min-height (40px) and per-button padding so the toolbar is the same vertical size in both modes — switching between live and history no longer reflows. - Differentiate "iframe-mounted history view" from "direct ?embed=1 visit". Only the former hides the inner timeslider editbar — direct visits keep their full chrome so existing test/legacy entry points stay independently usable. Marker: timeslider.ts adds an `iframe-mode` class on body when window.parent !== window; CSS scopes the hide to that combo. Tests - padmode spec asserts Follow + Speed live in the toolbar (not the Settings popup) and are visible in history mode, hidden in live. - timeslider*.spec direct-?embed=1 flows continue to pass because the inner editbar is no longer hidden when not iframe-mounted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): use absolute path for legacy /timeslider redirect (#7659) CI Firefox failed the legacy-URL redirect test (1 of 32 jobs); Chromium passed. The redirect Location header was a relative `../padname`, which both browsers resolve to /p/padname for `/p/padname/timeslider`. Firefox flaked on it once consistently. Switch to an absolute path including the proxy prefix so the resolution is unambiguous across browsers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): accept 304 on legacy timeslider redirect (#7659) CI Firefox failed `expect(res.status()).toBe(200)` because Firefox issues a conditional GET when the redirect target is the same URL the test just loaded via goToNewPad — the server returns 304 Not Modified and the test treats that as a regression. Chromium happens to send fresh requests so it stayed green. Accept either 200 or 304 — both are valid completed navigations to the pad page; what we actually care about is the pathname assertion above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pad): eye toggle for Follow, fix line-number alignment (#7659) Two refinements from real testing in 9002: Follow as an eye toggle - Replace the labeled checkbox with an inline-SVG eye icon. The eye is always rendered; a diagonal slash is overlaid via SVG <line> only when the underlying (visually hidden) checkbox is unchecked. Default state is on (auto-following) so the eye renders unobstructed. - Localized hover tooltip + aria-label flips with state — html10n populates "Following pad changes — click to stop following" vs "Not following pad changes — click to follow", and pad_mode.ts re-applies on every change event so screen readers narrate the action the click would take. - Hidden checkbox keeps the existing pad_mode.ts bridge code working (still reads .checked) and lets <label for="…"> handle the click. Line-number alignment fix (broadcast.ts) - The first-line height formula was `nextDocLine.offsetTop - innerdocbody.padding-top` which only computes the right value when innerdocbody is the offsetParent. In the in-pad history iframe, outerdocbody contributes its own padding-top to the offsetTop chain, so the first gutter row was 20px too tall and every subsequent line drifted out of alignment. Use the consistent `next.offsetTop - current.offsetTop` formula for every iteration — same result in the standalone timeslider, correct result in the embedded one. - New padmode spec asserts every gutter row's top matches the editor line's top within 2px, in iframe-mounted history mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cbf71285a2
|
feat(api): clean up published OpenAPI spec for downstream codegens (#7714)
* feat(api): clean up published OpenAPI spec for downstream codegens
The /api/openapi.json doc had four issues that broke generated tooling
(printingpress.dev, openapi-generator, Postman): empty top-level tags
array, every operation duplicated as GET+POST, 14 operations missing
from the resources map (drift since API 1.2.8), and empty summaries on
several tracked ops.
This PR splits the runtime spec from the published spec via a {public}
flag on generateDefinitionForVersion: the runtime definition fed to
openapi-backend keeps both verbs (existing third-party clients that
call GET /api/x.x.x/foo?apikey=... continue to work), while the spec
served at /api/openapi.json, /rest/openapi.json, and per-version paths
advertises only POST.
Other changes:
- top-level tags array declares pad/author/session/group/chat/server
- per-op tags override added to SwaggerUIResource type so chat ops
(still nested under pad for routing) and checkToken can be tagged
without changing existing REST URLs
- 14 missing ops (getAttributePool, getRevisionChangeset, copyPad,
movePad, getPadID, getSavedRevisionsCount, listSavedRevisions,
saveRevision, restoreRevision, appendText, copyPadWithoutHistory,
compactPad, anonymizeAuthor, getStats) backfilled with summaries
- empty summaries on listSessionsOfGroup, listAllGroups,
createDiffHTML, createPad filled in
- new backend tests assert the public spec shape (tags, summaries,
POST-only) and that runtime routing still resolves both verbs
Driven by integrating Etherpad with printingpress.dev: pointing the
generator at the previous spec produced a 96-command CLI with no
resource grouping and many empty descriptions. Design notes in
docs/superpowers/specs/2026-05-10-openapi-cleanup-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): keep checkToken at /rest/<ver>/pad/checkToken (Qodo #2)
Moving checkToken to a new `server` resource broke REST-style
backward compat: existing callers of /rest/<ver>/pad/checkToken
would have hit `code: 3` (no such function). The whole point of
per-op tag overrides is to preserve REST URLs while still grouping
correctly in OpenAPI tags — checkToken should follow the same
pattern as the chat ops.
Keep checkToken in `resources.pad`, give it `tags: ['server']`,
and add a regression test asserting /rest/<ver>/pad/checkToken
still resolves. The `server` resource still exists for `getStats`
(genuinely new server-level op with no prior REST URL).
Updates the design doc accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
df0ecec834
|
a11y: localize aria-label on form-control elements (<select>, <input>, <textarea>) (#7713)
#7584 introduced auto-population of aria-label from a translation when an element has data-l10n-id and no author-supplied aria-label. That branch only fires for elements with no children (or with data-l10n-id ending in a recognized attribute suffix like .title). Form-control elements break the assumption: a <select> always has <option> children, an <input>/<textarea> may have implicit value content. The textContent branch handles them, but the aria-label fallback wasn't called from there. Plugins like ep_font_size, ep_headings2, and ep_hljs end up with a localized translation applied to a <select> but no accessible name on the element itself. Calls populateAriaLabel() from the textContent branch when the node is a <select>, <input>, or <textarea>. Keeps the same "author-supplied aria-label wins on first pass; the data-l10n-aria-label marker lets us refresh values we wrote" semantics from #7584. Adds Playwright coverage in src/tests/frontend-new/specs/html10n_form_controls_aria.spec.ts: - aria-label is populated on <select> with data-l10n-id - aria-label is populated on <textarea> with data-l10n-id - author-supplied aria-label is preserved on first pass |
||
|
|
2adc228e61
|
fix(tests): unblock CI — Windows updater paths + admin-plugins row count (#7712)
* fix(updater): build expected paths via path.join in updater tests The Windows backend job has been red on develop since #7607 (tier-2 auto-update) merged: RollbackHandler.test.ts:122 and UpdateExecutor.test.ts:66 asserted POSIX-style paths, but the implementations build the same paths via path.join — which emits backslashes on Windows. The tests pass on Linux/macOS only by coincidence. Switch the expected values to path.join(deps.repoDir, ...) / path.join(deps.backupDir, ...) so they track whatever the implementation produces on the host platform. No production code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin-spec): assert installed-plugins by name, not row count ep_set_title_on_pad@0.7.2 took on ep_plugin_helpers as a transitive plugin dependency, so installing it now adds two rows to the installed-plugins table (the new plugin + its helper plugin) rather than one. The Playwright spec hard-coded `toHaveCount(2)` after install and `toHaveCount(1)` after uninstall, so it has been red on develop since #7705 merged the new admin bundle (every Frontend admin tests job, every Node version). Switch the assertions to scope by row text — `tr` containing `ep_set_title_on_pad` — and check that exactly one such row exists after install and zero after uninstall. This survives whatever transitive plugin deps the chosen test plugin pulls along, which is the only thing this spec actually cares about. Verified locally on a fresh Etherpad install: 3/3 admin-update-plugins tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4c3fdaf699
|
chore(admin): typesafe API client + TanStack Query rails (#7638) (#7695)
* chore(admin): typesafe API client + TanStack Query rails (#7638) (#2) * docs(admin): design for typesafe API client + TanStack Query rails (#7638) Rails-only scope: codegen toolchain, runtime client, and provider — no call-site migrations. Admin endpoints are not yet covered by the OpenAPI spec, so a separate issue will follow before any migration is useful. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(admin): implementation plan for typesafe API client rails (#7638) Step-by-step task breakdown for the rails-only PR: codegen toolchain, runtime client, TanStack Query provider, CI freshness check, docs. No call-site migrations until admin endpoints are added to the OpenAPI spec (separate follow-up). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api): export generateDefinitionForVersion from openapi hook Required by the admin codegen script (#7638) to dump the OpenAPI spec without booting Express. No behavior change for the request hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(admin): add OpenAPI codegen + TanStack Query deps (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(admin): add OpenAPI spec dump entry (#7638) Loaded via tsx by gen-api.mjs in the next commit. Writes JSON to a file path argument so log4js stdout output (from Settings init) does not pollute the spec output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(admin): wire OpenAPI codegen into build (#7638) Adds gen:api script and amends build/build-copy to regenerate admin/src/api/schema.d.ts before compiling. The generated file is checked in so it shows up in PR review and so a fresh checkout doesn't need codegen to typecheck. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): typed openapi-fetch + react-query client (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): TanStack Query provider, dev-only devtools (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): mount TanStack Query provider at root (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(admin): smoke test for typed openapi-fetch client (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(admin): verify generated OpenAPI schema is up to date (#7638) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(admin): document OpenAPI codegen workflow (#7638) Replaces the default Vite scaffold README with admin-specific scripts table and codegen workflow notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(admin): exclude __tests__ from tsc include (#7638) The smoke test imports node:test/node:assert which need @types/node. Admin source is browser-only, so excluding __tests__ from the production typecheck is cleaner than adding Node types to the bundle config. The test still runs under tsx, which doesn't share this constraint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate lockfile with pnpm 10 to restore overrides block (#7638) Adding admin deps with pnpm 11 stripped the top-level \`overrides:\` section from pnpm-lock.yaml, which CI uses pnpm 10 to verify. Result: ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every job. Re-running pnpm 10 \`install --lockfile-only\` restores the overrides block; the new admin package entries land in the same commit. Two stale lockfile entries not present in package.json (\`serialize-javascript\` version pin and \`uuid@<14.0.0\`) were normalized by the regen — package.json is the source of truth for those. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(docker): preserve strictDepBuilds=false in trimmed workspace yaml (#7638) Adding tsx as an admin devDep brings esbuild@0.27.x into the resolved subgraph, and the Dockerfile's runtime stage was overwriting pnpm-workspace.yaml with a stripped-down version that lost the strictDepBuilds=false setting from the source repo. With pnpm 10's default of strictDepBuilds=true, the install then errors on ERR_PNPM_IGNORED_BUILDS for esbuild + scarf rather than warning. Restore the strictDepBuilds=false and the @scarf/scarf ignore in the trimmed yaml so the production install matches develop's behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): point client baseUrl at /api/<version> via codegen (#7638) Qodo flagged: with baseUrl='/' and schema paths like '/createGroup', calls landed at /createGroup, but the backend mounts the FLAT-style spec under /api/<version>/. So once a call site lands, every request 404s. gen:api now also emits admin/src/api/version.ts containing LATEST_API_VERSION (read from info.version in the spec) and a derived API_BASE_URL = `/api/<version>`. client.ts imports API_BASE_URL. Workflow freshness check covers both generated files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(admin): cross-platform spawn in gen-api.mjs (#7638) Windows CI failed because spawnSync('pnpm', ...) cannot resolve pnpm.cmd without a shell. Set shell:true on win32 only so Linux/macOS runs avoid Node's DEP0190 warning. All spawn args are literal strings, so the shell variant is not an injection risk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(admin): gitignore generated schema/version, regen on every script (#7638) Qodo flagged the committed admin/src/api/schema.d.ts as a build artifact that violates the rule against committing generated files (rule 467291, "Exclude build artifacts and runtime-generated files from version control"). This commit: - Adds admin/src/api/{schema.d.ts,version.ts} to .gitignore. - Removes both from VCS (the prior squash had committed them). - Chains \`gen:api\` into the dev and test scripts so a fresh checkout lands a working dev server / test run without an extra step. build and build-copy already chained gen:api. - Drops the now-redundant CI freshness diff step from frontend-admin-tests.yml — with the files no longer committed, the build step's gen:api invocation is the only check needed. - Updates admin/README.md to describe the new generated-file workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
efb8328084
|
feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704)
* docs(updater): PR 2 (Tier 2 manual-click) implementation plan 20-task TDD plan for shipping the manual-click update flow on top of the Tier 1 (notify) work merged in #7601. Covers UpdateExecutor, RollbackHandler, SessionDrainer, lock + trustedKeys, four admin endpoints (apply / cancel / acknowledge / log), admin UI updates, integration tests against a tmp git repo, and a manual smoke runbook for the spec's "before each tier ships" gate. Plan deliberately scopes signature verification to an opt-in stub (updates.requireSignature: false default) to avoid blocking on a separate release-signing project. Plan: docs/superpowers/plans/2026-05-08-auto-update-pr2-manual-click.md Spec: docs/superpowers/specs/2026-04-25-auto-update-design.md Issue: ether/etherpad#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): extend state + settings for Tier 2 manual-click Adds ExecutionStatus discriminated union, bootCount, and lastResult to UpdateState, plus the preApplyGraceMinutes/drainSeconds/diskSpaceMinMB/ requireSignature/trustedKeysPath knobs that Tier 2's executor needs. loadState backfills the new fields on Tier 1 state files so existing installs keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): PID-based update.lock with stale-pid reaping Single-flight guard for Tier 2's UpdateExecutor. Atomic O_CREAT|O_EXCL acquire; on EEXIST, sends signal 0 to the recorded PID and reaps if dead. Unparseable / partially-written lock files are treated as stale rather than fatal so a half-written lock from a SIGKILL'd parent doesn't lock the install out forever. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): verifyReleaseTag — gpg-via-git stub for Tier 2 preflight Default updates.requireSignature=false: log a warning and return ok with reason=signature-not-required. Set true to make preflight refuse a tag whose signature does not verify under the system keyring (or trustedKeysPath via GNUPGHOME). Etherpad's release process does not yet sign tags consistently; turning the check on by default would break Tier 2 for every admin and forcing a release-signing change is out of scope for this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): preflight check pipeline for Tier 2 Pure orchestrator over injected probes for install-method, working tree, disk space, pnpm presence, lock state, remote tag existence and signature verification. Cheap-and-definitive checks run first; first failure short-circuits with a typed reason that the route layer will surface in the preflight-failed admin banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): rolling update.log helpers (appendLine + tailLines) Direct file-append + size-based rotation rather than a log4js appender — avoids re-configuring log4js on top of the user's existing logconfig. appendLine creates parents, rotates at 10MB (configurable), keeps 5 backups by default. tailLines reads the last N lines for /admin/update/log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): SessionDrainer + handshake guard Drainer schedules T-60 / -30 / -10 broadcasts and resolves at T=0; isAcceptingConnections() flips off for the duration. PadMessageHandler consults the flag at the start of CLIENT_READY and disconnects new joiners with reason "updateInProgress" — existing sockets are unaffected. Drains shorter than 30s collapse the early timers to fire ASAP rather than queue past the drain end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): UpdateExecutor — snapshot, fetch/checkout/install/build, exit 75 Pure-DI orchestrator: spawnFn, copyFile, readSha, saveState, exit are all injected so unit tests run the full pipeline without spawning real children or mutating the real install. Streams stdout/stderr to update.log via the now-best-effort appendLine helper (swallows fs errors so the executor itself never breaks on read-only / unwritable log dirs). Failure paths transition to rolling-back and return — the route layer hands off to RollbackHandler which owns the rollback exit, so we don't double-exit and lose tail lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): RollbackHandler — health-check timer + crash-loop guard checkPendingVerification arms a 60s timer at boot when state is pending-verification and increments bootCount; bootCount>2 forces an immediate rollback (crash-loop guard). markVerified persists the verified state and stops the timer. performRollback restores the backup lockfile, runs git checkout <fromSha> and pnpm install, lands on rolled-back or rollback-failed (terminal) on sub-step failure, exits 75 either way so the supervisor restart brings the new state up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): wire RollbackHandler into boot + UpdatePolicy honours rollback-failed - expressCreateServer now invokes checkPendingVerification before polling starts so a previous boot's pending-verification either re-arms the health-check timer or, when bootCount has climbed past the crash-loop threshold, forces an immediate rollback. - server.ts calls markBootHealthy after state hits RUNNING so /health-being-up is the implicit happy-path signal that cancels the rollback timer. - /admin/update/status surfaces execution + lastResult + lockHeld so the admin UI can render the right Apply / Cancel / Acknowledge state. - UpdatePolicy gains an `executionStatus` input. While it equals 'rollback-failed', canAuto / canAutonomous are denied (reason: rollback-failed-terminal); manual stays on because clicking Apply IS the intervention the terminal state needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): apply / cancel / acknowledge / log endpoints Strict admin-only POSTs that drive Tier 2's manual-click flow: - POST /admin/update/apply: acquire lock, persist preflight, run preflight, drain $drainSeconds, executeUpdate (which exits 75 on success), or run performRollback on a failure path (also exits 75). - POST /admin/update/cancel: cancel a pre-execute drain/preflight, write cancelled lastResult, release lock. - POST /admin/update/acknowledge: clear terminal states (preflight-failed, rolled-back, rollback-failed) back to idle. lastResult is preserved so the admin still sees what happened. - GET /admin/update/log: tail var/log/update.log (200 lines) for the in- progress UI. Strict admin auth. Also: - socketio hook exports getIo() so the apply endpoint can broadcast the drain shoutMessage outside the regular hook surface. - ep.json registers updateActions after admin/updateStatus. - 11 mocha integration tests cover auth, policy denial, execution-busy, acknowledge-clears-terminal, log content-type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): admin UI Apply/Cancel/Acknowledge + live log stream UpdatePage renders the right action set based on execution.status: Apply when idle/verified and policy allows, Cancel during preflight/draining, Acknowledge on terminal preflight-failed / rolled-back / rollback-failed. While the executor is in flight (preflight/draining/executing/rolling-back) the page polls /admin/update/log + /admin/update/status once a second and shows the rolling tail; polling stops automatically when the run terminates. lastResult and policy denial reasons surface localised copy. Buttons disable themselves while a network round-trip is in flight to dodge double-clicks. New i18n keys live under update.page.{apply,cancel, acknowledge,log,execution,policy.*,last_result.*}, update.execution.*, update.banner.terminal.rollback-failed, and update.drain.{t60,t30,t10}. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): pad shoutMessage renders update.drain.* via html10n broadcastShout now sends {messageKey, values, sticky} so the existing pad-side shout pipeline can route through html10n.get(). The renderer gains a values pass-through so update.drain.t60 etc. interpolate {{seconds}}, and gives updater shouts a different gritter title (the banner.title localised string) so users know it's a system event rather than a generic admin message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): rollback uses git checkout -f + integration suite over tmp git repo RollbackHandler now does git checkout -f <fromSha> BEFORE overlaying the backup lockfile. Without -f, git refuses checkout when there are unstaged modifications to files it would overwrite — exactly the case after a partial executor run that mutated the working tree. With -f the partial mutation is discarded and the working tree returns to fromSha cleanly. The backup-lockfile copy is still done (belt-and-braces) but tolerates ENOENT since checkout already restored the right lockfile. The new integration suite at src/tests/backend/specs/updater-integration.ts exercises the full pipeline against a disposable git repo: happy path, install-fail rollback, build-fail rollback, crash-loop guard, and a target-sha-doesn't-exist rollback-failed terminal case. 5 mocha tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(updater): Playwright admin Apply / Cancel / Acknowledge flow Stubs /admin/update/status (and /admin/update/apply for the apply path) at the route level so we can assert UI transitions without actually running an update. Four scenarios: - Apply button POSTs and re-fetches status (>=2 status fetches total). - install-method-not-writable hides the button and shows localised denial copy. - rollback-failed terminal state shows the Acknowledge button and the "Manual intervention required" lastResult copy. - lockHeld=true hides Apply even when policy.canManual is on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): admin banner shows rollback-failed terminal alert When execution.status === 'rollback-failed' the banner switches to a role=alert with the strong update.banner.terminal.rollback-failed copy and overrides the regular "update available" framing — an admin who left the system in this state needs to fix it before any other admin work matters. Other terminal states (preflight-failed, rolled-back) are informational and surface on the page itself, not the banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): Tier 2 admin docs + manual smoke runbook + CHANGELOG doc/admin/updates.md gains a full Tier 2 section: prerequisites (git install + process supervisor with sample systemd unit), Apply flow with timings, every failure mode and the resulting state, the four endpoints, and the signature-verification opt-in. Settings table picks up the new updates.* knobs. docs/superpowers/specs/2026-04-25-auto-update-runbook.md is the manual smoke runbook the design spec calls for: disposable VM, systemd unit, every observable transition (happy path, install/ build-fail rollback, crash-loop guard, rollback-failed terminal, cancel during drain) plus a sign-off checklist for the release cut. CHANGELOG Unreleased section explains the supervisor requirement and points readers at the runbook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): note docker-friendly update flows as follow-up work Tier 2 refuses Apply on installMethod=docker because in-container mutation doesn't survive a container restart. Adds a future-work note covering the two reasonable paths for an in-product docker Apply button (instructions-only vs deploy-webhook) and explicitly rules out mounting /var/run/docker.sock as a footgun. Watchtower gets a pointer for admins who want fully autonomous docker updates today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo review (1-6) + Playwright strict-mode CI fix 1. Tier 2 endpoints now gate on tier in {manual, auto, autonomous} — notify and off return 404 to match the prior PR-1 behaviour. Gate is evaluated per-request via app.use middleware so a settings.json reload takes effect without a full restart, and so integration tests can flip the tier dynamically. Adds a regression test that exercises 404 at tier=notify across all four endpoints. 2. cancel/apply race fixed: /admin/update/cancel no longer releases the lock — apply's finally block owns it for the request's lifetime. Apply now reloads state after preflight and aborts with 409 cancelled-during- preflight if execution.status is no longer 'preflight' for the same targetTag. Prevents a second apply from sneaking in while the first is still running its slow checks, and prevents the post-cancel apply from continuing into drain/execute. 3. SessionDrainer now restores acceptingConnections=true at drain completion (not just on cancel). The lock + persisted execution.status prevent a fresh apply from racing in — the in-memory flag was redundant safety that turned into a wedge if the executor threw post-drain. Adds a unit test asserting the flag is restored after natural drain end. 4. PadMessageHandler drain guard switched from socket.json.send (a socket.io v2/v3 API that may not exist on v4) to socket.emit('message', ...) for consistency with the other disconnect paths in the file. 5. Spawn 'error' handlers added to runStep helpers in UpdateExecutor and RollbackHandler, plus the gpg verify-tag spawn in trustedKeys. Without them, a missing/unexecutable binary leaves the promise hanging forever and the update flow stuck in-flight. SpawnFn type extended to allow on('error', ...) listeners cleanly. Spawn errors now resolve with code 1 + the error message in stderr, so the existing failure-detection branches fire normally. 6. executeUpdate body wrapped in try/catch. An exception from readSha, saveState, copyFile, or any step now lands in a rolling-back persist + returns failed-checkout, so the route's post-executor rollback path picks it up. State can no longer wedge at 'executing'. The catch's inner saveState is itself try/wrapped so a write-after-write failure doesn't crash the route either. CI: Playwright update-page-actions strict-mode violation fixed. Both the banner and the lastResult <p> contain "Manual intervention required"; selector now scopes to p.last-result-rollback-failed for the lastResult assertion specifically. 129 vitest unit tests + 23 mocha integration tests passing; ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo #7 (status leak) + #8 (short-drain values) #7. /admin/update/status now redacts diagnostic strings for unauth callers even when requireAdminForStatus is left at its default (false). Status enum + outcome enum are kept (the admin banner / pad-side badge need them to render the right UI) but execution.reason / execution.fromSha / execution.targetTag and the same fields on lastResult are stripped. Authed admin sessions still get the full payload — they're looking at their own server's diagnostics. Two new mocha tests cover both paths: "redacts execution.reason / lastResult.reason for unauth callers" and "returns full diagnostic payload to authed admin sessions". #8. SessionDrainer no longer schedules T-30 / T-10 broadcasts when the configured drainSeconds can't honour them. Previously, with drainSeconds < 30 the T-30 timer fired at zero remaining but the broadcast still claimed "30 seconds" — misleading. Now T-30 only schedules when drainSeconds > 30 and T-10 only when > 10. Admins picking a short drain get fewer announcements but each carries an accurate countdown. The opening announcement now reports the configured drain length rather than a hardcoded 60. Two updated unit tests: drainSeconds=15 (skips T-30, still fires T-10) and drainSeconds=5 (skips both). 131 vitest unit + 26 mocha integration tests passing; ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo follow-up — tag injection, rollback rejections, state validation Qodo posted three new concerns after the first fix push. 1. Git tag option injection (security). The release tag from GitHub's tag_name flowed into `git checkout` / `git verify-tag` as a positional arg. A tag starting with '-' would be parsed as an option and could bypass signature verification or change checkout semantics. Mitigated in three layers: - New refSafety helper (isValidTag / assertValidTag / refsTagsForm) enforces a strict subset of git's check-ref-format spec: rejects leading '-' or '.', whitespace, control chars, and ~ ^ : ? * [ \\ and the '..' sequence. - VersionChecker validates tag_name before persisting to state, so a malformed value from a misconfigured githubRepo never lands on disk. - UpdateExecutor calls assertValidTag and uses the refs/tags/<tag> form for git checkout. trustedKeys also validates and adds '--' to git verify-tag for an end-of-options marker. updateActions does an up-front isValidTag check on state.latest.tag so a corrupt state file gets a clean 409 instead of a 500. 2. Unhandled rollback rejections. checkPendingVerification was firing `void deps.saveState(...)` and `void performRollback(...)` without .catch(), so an fs error during boot's rollback path would bubble out as an unhandled rejection. Both callsites now go through fireSaveState / fireRollback helpers that catch and log; rollback rejections fall through to a best-effort terminal-state write + exit 75 so the supervisor can re-try the next boot with bootCount++. 3. Execution state under-validated. isValidExecution previously checked only that `status` was a known enum value, so a hand-edited state file with `{execution: {status: 'pending-verification'}}` (missing fromSha / targetTag / deadlineAt) would pass validation and reach RollbackHandler with undefined refs. The validator now consults a per-status required-fields map mirroring the ExecutionStatus union in types.ts and rejects empty strings as well as missing fields. Same tightening applied to lastResult.outcome (must be in the allowed enum, not just any string). Six new unit tests cover hand-edited corruption. 145 vitest + 26 mocha tests green; ts-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4f1b524864
|
feat(admin): document admin endpoints in OpenAPI (#7693) (#7705)
* chore(admin): typesafe API client + TanStack Query rails (#7638) (#2)
* docs(admin): design for typesafe API client + TanStack Query rails (#7638)
Rails-only scope: codegen toolchain, runtime client, and provider — no
call-site migrations. Admin endpoints are not yet covered by the OpenAPI
spec, so a separate issue will follow before any migration is useful.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): implementation plan for typesafe API client rails (#7638)
Step-by-step task breakdown for the rails-only PR: codegen toolchain,
runtime client, TanStack Query provider, CI freshness check, docs. No
call-site migrations until admin endpoints are added to the OpenAPI
spec (separate follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(api): export generateDefinitionForVersion from openapi hook
Required by the admin codegen script (#7638) to dump the OpenAPI spec
without booting Express. No behavior change for the request hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): add OpenAPI codegen + TanStack Query deps (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): add OpenAPI spec dump entry (#7638)
Loaded via tsx by gen-api.mjs in the next commit. Writes JSON to a file
path argument so log4js stdout output (from Settings init) does not
pollute the spec output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): wire OpenAPI codegen into build (#7638)
Adds gen:api script and amends build/build-copy to regenerate
admin/src/api/schema.d.ts before compiling. The generated file is
checked in so it shows up in PR review and so a fresh checkout doesn't
need codegen to typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): typed openapi-fetch + react-query client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): TanStack Query provider, dev-only devtools (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): mount TanStack Query provider at root (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(admin): smoke test for typed openapi-fetch client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(admin): verify generated OpenAPI schema is up to date (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): document OpenAPI codegen workflow (#7638)
Replaces the default Vite scaffold README with admin-specific scripts
table and codegen workflow notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(admin): exclude __tests__ from tsc include (#7638)
The smoke test imports node:test/node:assert which need @types/node.
Admin source is browser-only, so excluding __tests__ from the production
typecheck is cleaner than adding Node types to the bundle config. The
test still runs under tsx, which doesn't share this constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate lockfile with pnpm 10 to restore overrides block (#7638)
Adding admin deps with pnpm 11 stripped the top-level \`overrides:\`
section from pnpm-lock.yaml, which CI uses pnpm 10 to verify. Result:
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every job. Re-running pnpm 10
\`install --lockfile-only\` restores the overrides block; the new admin
package entries land in the same commit. Two stale lockfile entries
not present in package.json (\`serialize-javascript\` version pin and
\`uuid@<14.0.0\`) were normalized by the regen — package.json is the
source of truth for those.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(docker): preserve strictDepBuilds=false in trimmed workspace yaml (#7638)
Adding tsx as an admin devDep brings esbuild@0.27.x into the resolved
subgraph, and the Dockerfile's runtime stage was overwriting
pnpm-workspace.yaml with a stripped-down version that lost the
strictDepBuilds=false setting from the source repo. With pnpm 10's
default of strictDepBuilds=true, the install then errors on
ERR_PNPM_IGNORED_BUILDS for esbuild + scarf rather than warning.
Restore the strictDepBuilds=false and the @scarf/scarf ignore in the
trimmed yaml so the production install matches develop's behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): point client baseUrl at /api/<version> via codegen (#7638)
Qodo flagged: with baseUrl='/' and schema paths like '/createGroup',
calls landed at /createGroup, but the backend mounts the FLAT-style
spec under /api/<version>/. So once a call site lands, every request
404s.
gen:api now also emits admin/src/api/version.ts containing
LATEST_API_VERSION (read from info.version in the spec) and a derived
API_BASE_URL = `/api/<version>`. client.ts imports API_BASE_URL.
Workflow freshness check covers both generated files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(admin): cross-platform spawn in gen-api.mjs (#7638)
Windows CI failed because spawnSync('pnpm', ...) cannot resolve
pnpm.cmd without a shell. Set shell:true on win32 only so Linux/macOS
runs avoid Node's DEP0190 warning. All spawn args are literal strings,
so the shell variant is not an injection risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): gitignore generated schema/version, regen on every script (#7638)
Qodo flagged the committed admin/src/api/schema.d.ts as a build artifact
that violates the rule against committing generated files (rule 467291,
"Exclude build artifacts and runtime-generated files from version control").
This commit:
- Adds admin/src/api/{schema.d.ts,version.ts} to .gitignore.
- Removes both from VCS (the prior squash had committed them).
- Chains \`gen:api\` into the dev and test scripts so a fresh checkout
lands a working dev server / test run without an extra step. build
and build-copy already chained gen:api.
- Drops the now-redundant CI freshness diff step from
frontend-admin-tests.yml — with the files no longer committed, the
build step's gen:api invocation is the only check needed.
- Updates admin/README.md to describe the new generated-file workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): design for admin OpenAPI coverage (#7693)
Adds the design doc for documenting `/admin-auth/*` and `/admin/update/status`
in the OpenAPI spec so the typed client generated by #7695 (`admin/src/api/
schema.d.ts`) gains admin call-sites the day it lands.
This PR is stacked on #7695 — codegen rails must merge first. Schema-only,
no call-site migrations (those are an explicit follow-up named in #7693).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): correct UpdateStatus schema to base-branch shape (#7693)
The first draft mirrored the Tier 2 (#7607) response shape, but this PR
stacks on #7695 whose updateStatus.ts only emits the Tier 1 fields. Fix
the schema, install-method enum, and tier enum to match types.ts and
the actual handler. Tier 2 amends UpdateStatus when it lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): implementation plan for admin OpenAPI coverage (#7693)
Step-by-step task breakdown for the schema-only PR: stub the document,
add /admin-auth/ + /admin/update/status paths and sub-schemas,
collision regression, /admin/openapi.json route, codegen merge, and CI
verification. No call-site migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): stub OpenAPI document for admin endpoints (#7693)
Adds generateAdminDefinition() returning a minimal valid OpenAPI 3.0
document with no paths yet, plus security schemes for the two auth
modes (Basic + session cookie). Subsequent tasks fill in the actual
admin paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): document POST /admin-auth/ in OpenAPI (#7693)
Adds verifyAdminAccess as the operation that the admin UI's LoginScreen
and App session check both call. Documents Basic auth, session cookie,
and anonymous request modes plus their 200/401/403 responses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): document GET /admin/update/status in OpenAPI (#7693)
Adds getUpdateStatus operation plus UpdateStatus, ReleaseInfo,
PolicyResult, and VulnerableBelowDirective sub-schemas. Property names
and enums mirror src/node/updater/types.ts and the response object
emitted by updateStatus.ts. Tier 2 (#7607) will amend UpdateStatus when
it ships execution/lastResult/lockHeld.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(admin): regression net for admin/public OpenAPI collisions (#7693)
Cross-checks admin paths, operationIds, and schema names against the
latest public spec. Today there are no overlaps; the test exists to
catch future renames before they break the merged client codegen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): expose admin OpenAPI doc at /admin/openapi.json (#7693)
Mounts the admin OpenAPI document at /admin/openapi.json (CORS: *) via
an expressPreSession hook, matching the /api/openapi.json convention.
The admin SPA wildcard at /admin/{*filename} registers later in
expressCreateServer, so the JSON route wins. Live-route test confirms
JSON content-type and CORS header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): mergeOpenAPI helper for codegen pipeline (#7693)
Pure-JS deep-merge of two OpenAPI 3.0 documents. Unions paths and
components by key; throws on collisions. Public document's info,
servers, and root security win over the admin document's. Used by
dump-spec.ts to produce a single merged JSON for openapi-typescript.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): fix stale "Section 3" reference in spec (#7693)
Drafting numbered the design walkthrough; final spec uses titled
sections. Update the back-reference accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): include admin OpenAPI in generated client (#7693)
Modifies dump-spec.ts to import generateAdminDefinition alongside the
public generator and feed both through mergeOpenAPI before writing the
JSON consumed by openapi-typescript. The resulting admin/src/api/
schema.d.ts paths interface now exposes /admin-auth/ and
/admin/update/status, ready for typed call-site adoption in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): gate /admin/openapi.json behind a feature flag (#7693)
Address Qodo finding 1: new features must ship behind a flag, disabled
by default (CONTRIBUTING.md, AGENTS.MD, best_practices.md). Adds
settings.adminOpenAPI.enabled (default false). expressPreSession
returns early when the flag is off, so the route is dormant on a fresh
install.
The codegen pipeline imports generateAdminDefinition() in-process and
does not depend on the runtime route, so default-off has no effect on
the typed client. Operators who want third-party tooling (Postman,
swagger-ui, downstream clients) to consume the spec at runtime opt in
via settings.
Adds:
- SettingsType + defaults entry in src/node/utils/Settings.ts
- settings.json.template documentation
- A backend spec asserting expressPreSession is a no-op when the flag
is off (live route test now sets the flag explicitly)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): split fetchClient by surface; admin client baseUrl='/' (#7693)
Address Qodo finding 2 (correctness bug). The merged schema introduced
in this PR exposes both public-API paths (under /api/<version>/) and
admin paths (at root, e.g. /admin-auth/). The single fetchClient from
#7695 has baseUrl=API_BASE_URL ("/api/<version>"), so calling an admin
path through it would resolve to /api/<version>/admin-auth/ — wrong.
Fix:
- Narrow the generated `paths` interface by URL prefix
(`/admin*` vs everything else).
- Export two clients: `fetchClient` (public, baseUrl=/api/<version>)
and `adminFetchClient` (admin, baseUrl='/').
- Mirror with `$api` / `$adminApi` query hook factories.
TypeScript now rejects mixing surfaces at compile time:
fetchClient.GET('/admin-auth/') // type error: not in PublicPaths
adminFetchClient.GET('/createGroup') // type error: not in AdminPaths
Smoke test updated to assert all four exports exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): spec — feature flag + dual-client design (#7693)
Reflect Qodo-driven changes:
- /admin/openapi.json route is now feature-flagged (default off).
- admin/src/api/client.ts splits paths by URL prefix and exports two
clients (public + admin) so TypeScript rejects mixing surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): check adminOpenAPI flag per-request, not at hook setup (#7693)
The first feature-flag commit (
|
||
|
|
ed4579c701
|
docs(7538): soffice is now optional for docx/pdf (#7707)
Native DOCX export, PDF export, and DOCX import shipped in #7568 via pure-JS in-process converters -- LibreOffice/soffice is no longer required for those formats. Stale comments in settings.json.template and settings.json.docker still implied otherwise ("will only allow plain text and HTML import/exports"), and the docker docs told users to configure soffice for DOCX as well. Update them to match what's actually in core: - soffice present: handles all office formats (existing behavior) - soffice null: docx export, pdf export, docx import work natively; odt/doc/rtf export and pdf import still need soffice Touches: - settings.json.template (soffice + docxExport comments) - settings.json.docker (same) - doc/docker.md ("Office-format import/export" section) - doc/docker.adoc (same section + the SOFFICE table row, matching what doc/docker.md already says since #7568) No code changes, no behavior change -- documentation only. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fd2f3baf55 | chore: fixed commit path | ||
|
|
84e8461b69 | chore: update docker write location in values-dev.yaml | ||
|
|
ac9751cb0b
|
build(deps-dev): bump the dev-dependencies group across 1 directory with 6 updates (#7706)
Bumps the dev-dependencies group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.6.0` | `25.6.2` | | [i18next](https://github.com/i18next/i18next) | `26.0.9` | `26.0.10` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.5` | `19.2.6` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.5` | `19.2.6` | | [react-i18next](https://github.com/i18next/react-i18next) | `17.0.6` | `17.0.7` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.10` | `8.0.11` | Updates `@types/node` from 25.6.0 to 25.6.2 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `i18next` from 26.0.9 to 26.0.10 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.0.9...v26.0.10) Updates `react` from 19.2.5 to 19.2.6 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react) Updates `react-dom` from 19.2.5 to 19.2.6 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react-dom) Updates `react-i18next` from 17.0.6 to 17.0.7 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v17.0.6...v17.0.7) Updates `vite` from 8.0.10 to 8.0.11 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.11/packages/vite) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.6.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.0.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react dependency-version: 19.2.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-dom dependency-version: 19.2.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-i18next dependency-version: 17.0.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite dependency-version: 8.0.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
c47ffd5705
|
feat(export): native DOCX export via html-to-docx (opt-in) (#7568)
* feat(export): native DOCX export via html-to-docx (opt-in) Addresses #7538. The current DOCX export path shells out to LibreOffice, which means every deployment that wants a Word download either installs soffice (~500 MB) or loses that export. This PR adds a pure-JS alternative: render the HTML via the existing exporthtml pipeline, then feed it to the `html-to-docx` library in-process to produce a valid .docx buffer — no soffice required, no subprocess spawn, no temp file dance for the DOCX case. Behavior: - `settings.nativeDocxExport` (default `false`) gates the new path so existing deployments see zero behavior change. - When enabled, `type === 'docx'` requests skip the LibreOffice branch, run `html-to-docx(html)`, and return the buffer with the `application/vnd.openxmlformats-officedocument.wordprocessingml.document` content-type. - If the native converter throws, the handler falls through to the existing LibreOffice path — so flipping the flag on is safe even on a mixed-installation where soffice is still present as a backstop. - Other export formats (pdf, odt, rtf, txt, html, etherpad) are unchanged. Files: - `src/package.json`: `html-to-docx` dep (pure JS, no binary reqs) - `src/node/handler/ExportHandler.ts`: new DOCX branch gated on the setting, with fall-through on error - `src/node/utils/Settings.ts`, `settings.json.template`, `settings.json.docker`, `doc/docker.md`: wire up the new setting + env var (`NATIVE_DOCX_EXPORT`) - `src/tests/backend/specs/export.ts`: two new tests — asserts the exported buffer is a valid ZIP (PK\x03\x04 signature) and the response carries the correct content-type — both with `settings.soffice = 'false'` to prove the path doesn't need soffice at all. Out of scope for this PR: - Native PDF export (would need a PDF rendering step — separate undertaking, and the issue acknowledges the `pdfkit`/puppeteer size trade-off). Closes #7538 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7538): skip native DOCX test when html-to-docx isn't installed The upgrade-from-latest-release CI job installs deps from the previous release's package.json (before this PR adds html-to-docx) and then git-checkouts this branch's code without re-running pnpm install. Under that one workflow the new test can't find the module and fails on the LibreOffice fallback, masking that the native path actually works in every normal install. Guard the describe block with require.resolve('html-to-docx'); Mocha's this.skip() on before cascades to the sibling its. Regular backend tests (pnpm install against this branch's lockfile) still exercise it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(7538): spec for soffice-free DOCX/PDF export and DOCX import Captures the agreed scope expansion of PR #7568: replace the flag-gated native DOCX path with a soffice-first selection cascade, add native PDF export via pdfkit + a small htmlparser2-driven walker, and add native DOCX import via mammoth. Also defines a shared HTML sanitizer (stripRemoteImages) used by both export converters to close the SSRF surface that Qodo flagged on the html-to-docx path. The spec drops the nativeDocxExport setting and its env var; with soffice configured, behavior is unchanged, and with soffice null, docx/pdf export and docx import all work in-process. odt/doc/rtf (and pdf import) keep needing soffice and are documented as such. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(7538): implementation plan for native DOCX/PDF and DOCX import Bite-sized TDD task breakdown of the soffice-free export/import work: rebase, deps, sanitizer, PDF walker, mammoth wrapper, ExportHandler cascade, route guard, ImportHandler branch, UI fix, flag rollback, verification + Qodo reply. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(7538): add pdfkit, htmlparser2, mammoth deps Pure-JS, no native binaries: - pdfkit ^0.18.0 (PDF rendering) - htmlparser2 ^12 (SAX parser used by walker + sanitizer) - mammoth ^1.12 (DOCX -> HTML for native import) - @types/pdfkit ^0.17 (dev) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): add stripRemoteImages HTML sanitizer Drops <img src=> elements pointing at non-data, non-relative URLs to prevent the DOCX/PDF converters from making outbound requests via plugin-modified HTML. Closes Qodo finding #4 against the html-to-docx path; will be wired into both export branches in the cascade refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): native PDF export via pdfkit + htmlparser2 walker Renders pad HTML to a PDF Buffer in-process: headings, paragraphs, lists, links, inline emphasis, data:-URI images. Remote images are explicitly skipped at the walker (defense-in-depth on top of the shared stripRemoteImages sanitizer). PDFs are emitted with compress:false so accessibility/SEO indexers that don't FlateDecode can still extract text. Pads are small enough that the size cost is negligible. Walker is 167 LOC, well under the spec's 500-LOC bail-out threshold for switching to pdfmake+html-to-pdfmake+jsdom. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): native DOCX import via mammoth Wraps mammoth.convertToHtml so a soffice-less Etherpad can ingest .docx files. Images are coerced to data: URIs at the converter boundary so the import pipeline never sees a remote src=. Includes a tiny generated DOCX fixture (heading, paragraph, list) under tests/backend/specs/fixtures/ for both this wrapper test and the upcoming end-to-end import test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): soffice-first cascade in ExportHandler Replaces the flag-gated DOCX branch with a deterministic dispatch: soffice if configured, native DOCX/PDF otherwise, 5xx on native error. Both native paths run plugin-modified HTML through stripRemoteImages first. Test changes: - existing native DOCX block now sets soffice=null (was 'false', a truthy non-null string that sidestepped the route guard); fixes Qodo finding #3. - new native PDF integration tests assert %PDF- header and application/pdf content-type with soffice=null. - new negative test: with soffice=null, /export/odt still returns the 'not enabled' message. - the legacy 500-on-export-error test now uses /bin/false so it exercises the soffice error path explicitly (the cascade dropped the ad-hoc 'false' string; .doc has no native path so this still works as a soffice error probe). Integration tests for native DOCX/PDF currently fail because the /export route guard still treats both formats as soffice-only; the next commit fixes that. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): allow docx/pdf through export guard without soffice Tightens the no-soffice block to ['odt','doc'] only — formats with no native path. docx and pdf are handed to ExportHandler, which dispatches to the in-process converters. Closes Qodo finding #2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(7538): native DOCX import path in ImportHandler When soffice is null and the upload is .docx, run mammoth and feed the resulting HTML through setPadHTML. Other office formats (pdf/odt/doc/rtf) are explicitly rejected with uploadFailed instead of silently falling through to the ASCII-only path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): always show DOCX/PDF export links Native paths (#7538) make DOCX and PDF available regardless of soffice presence, so unconditionally render those links. ODT still gates on exportAvailable. Closes Qodo finding #2 on the UI side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(7538): drop nativeDocxExport flag Selection is now purely soffice-presence-driven (cascade in ExportHandler). The opt-in setting and its NATIVE_DOCX_EXPORT env var are no longer needed -- soffice configured means soffice path; soffice null means native path (DOCX, PDF, and DOCX import). Reverts the additive surface introduced earlier in this PR. Also updates the SOFFICE doc row to reflect that null no longer means 'plain text and HTML only' -- docx/pdf export and docx import now work natively without soffice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7538): tighten link annotation assertion CodeQL flagged the loose 'raw.includes("etherpad.org")' as 'incomplete URL substring sanitization' (a false positive in test context, but worth fixing). Match the full /URI (host) form instead -- it's both more accurate (we're verifying the PDF link annotation structure) and CodeQL-clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): cleaner DOCX/PDF output + round-trip test coverage DOCX: - New extractBody helper drops <head>/<style> and the leading newline inside <body> so html-to-docx doesn't render CSS or prefix paragraphs with empty space. - New wrapLooseLines pre-processor wraps loose pad lines in <p> before the converter sees them. html-to-docx renders <br> outside <p> as a new <w:p> (full empty line in Word); inside <p> it correctly emits <w:br/> (soft break). Etherpad's HTML uses bare <br> for every line, so this was making single Enters look like double Enters in the Word output. PDF: - Walker SKIP_TAGS rejects head/style/script/title/meta/link content -- prior version dumped CSS into the rendered PDF. - New breakLine() helper combines flushLine() with moveDown(1). pdfkit's text('', false) closes the continued run but does NOT advance the cursor, so consecutive runs were stacking at the same y-coordinate. <br>, end-of-block, and list items now use breakLine(). - ontext collapses runs of whitespace and drops pure-whitespace text nodes so pretty-printed source HTML doesn't render its formatting newlines. Round-trip: - New backend test: pad text -> DOCX export -> DOCX import -> new pad. Asserts content survives the trip. - New PDF sanity test: extracts visible text from the PDF stream and asserts the source pad text appears verbatim. - 6 new unit tests for extractBody and wrapLooseLines plus 1 for PDF walker SKIP_TAGS coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): CodeQL ReDoS + import.ts type error - BR_PARA_RE was /(?:\s*<br>\s*){2,}/ -- two adjacent \s* runs can match the same chars, so on '<br>\t<br>\t<br>...' the regex backtracks exponentially. Re-anchored to match a fixed first <br> followed by one or more additional <br>s, so each whitespace run has exactly one home. - import.ts: fetchBuffer was typed Promise<Buffer> but call sites chained .expect(200) on it, which only works on supertest's Test object. Return the Test (typed any) so the chain is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): plugin-aware HTML cleanup, PDF text-align, monospace ep_headings2/ep_align emit one heading-styled blank-line block after every styled line in the pad ('<h1 style=text-align:right></h1>'), which both html-to-docx and our pdfkit walker render as a full empty paragraph. Plus the pdfkit walker had no support for text-align or monospace, so right/center alignment and 'code' lines rendered the same as plain body text. - New dropEmptyBlocks helper strips empty h1-h6/p/code/pre/div/ blockquote wrappers in preprocessing. Iterates so nested empties collapse too. Applied before both DOCX and PDF conversion. - PDF walker now reads style='text-align:left|center|right|justify' on block elements (h1-6, p, div) and passes it as pdfkit's align option. align is applied once per continued run, then reset on flushLine so the next block can pick up its own value. - PDF walker handles <code>, <tt>, <kbd>, <samp> as inline monospace (Courier) and <pre> as block monospace (Courier + breakLine on open/close). 11 new unit tests: - 4 for dropEmptyBlocks (heading wrappers, code, nesting, pass-through) - 1 for PDF text-align (compares the BT matrix x for left vs right) - 2 for Courier in <code> and <pre> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): separate adjacent heading-style blocks on import Round-trip bug: ep_headings2 emits <h1>/<h2>/<code> in pad HTML. Mammoth round-trips them as adjacent <h1>A</h1><h2>B</h2><p>C</p> on import. Etherpad's server-side content collector has a default _blockElems set of just {div, p, pre, li}, and ep_headings2 only registers the CLIENT-side aceRegisterBlockElements -- not the server-side ccRegisterBlockElements. So h1/h2/code end up being treated as inline by the importer, and adjacent blocks merge into a single pad line. Fix: insert <br> after </h1>...</h6>/</code> when followed by another block. Server-side workaround keeps this PR self-contained regardless of plugin version. The right long-term fix is to extend ep_plugin_helpers' lineAttribute factory to register both hooks (filed as a follow-up). Tests: - 5 unit tests for separateAdjacentHeadingBlocks - New end-to-end round-trip test asserts H1+H2+P land on three separate pad lines after the import path. Plus the prior PDF text-align/Courier/code commit also included here: - code/tt/kbd/samp inherit text-align from style attribute - pre inherits text-align too Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): blank-line round-trip + DOCX code monospace + a==c tests DOCX round-trip dropping blank pad lines: - wrapLooseLines now emits an explicit <p></p> marker for each blank line in a <br> run, instead of collapsing all gaps into a single paragraph break. (N consecutive <br>s -> 1 paragraph boundary + N-2 empty <p></p> markers, mapping to N-1 blank pad lines.) - mammoth's docxBufferToHtml now passes ignoreEmptyParagraphs:false so the empty <w:p> entries survive the import side. mammoth's default of true was silently dropping them. - dropEmptyBlocks no longer strips <p></p> -- that's the meaningful marker for the round-trip. Empty <h1>/<code>/<pre>/<div>/ <blockquote> are still stripped (plugin noise). DOCX <code> rendering as monospace: - New applyMonospaceToCode wraps code/tt/kbd/samp/pre content in a <span style="font-family:'Courier New', monospace">. html-to-docx honors that and emits <w:rFonts w:ascii="Courier New".../>, which Word renders as Courier. The bare <code> tag is otherwise just a no-op for html-to-docx. - Applied only on the DOCX export path (PDF walker already handles monospace via Courier font selection). Round-trip tests: - New a==c suite: txt, etherpad, html, docx -- export from src, import to dst, re-export and compare against the meaningful invariant (line text for binary formats; trimmed body for HTML). - HTML test tolerates one trailing <br> per round-trip because setPadHTML appends a final <p> on import; this is pre-existing core behavior, not our bug. - DOCX test normalizes trailing newline run (same reason). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): preserve <w:jc> alignment through mammoth round-trip mammoth doesn't expose Word's paragraph alignment (`<w:jc>`) when it converts a docx to HTML -- there's no equivalent in its style-mapping machinery. To keep alignment through DOCX round-trips we walk the docx's document.xml directly, pull the `w:val` from each `<w:p>`'s `<w:jc>`, and inject `style="text-align:..."` onto the matching block element in mammoth's output by document order. Word's w:jc accepts more values than CSS text-align; we map left/ start, center, right/end, both/justify/distribute and skip the rest (start/end take left/right because we don't track ltr/rtl from the docx for now). Combines with the upstream ep_align PR (ether/ep_align#183) for the full round-trip: this PR makes the mammoth output carry the alignment style; ep_align#183 makes the importer pick it up. Closes the alignment side of #7538. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): preserve <a href> inside <code>/<pre> in DOCX export html-to-docx silently drops <a href> children of <code>/<pre> tags (and of styled <span>s, but the <code> wrapper is the active offender here). The pad-export HTML produced by ep_headings2 + ep_align uses <code style='text-align:right'>...<a href>...</a>...</code> for each 'Code'-style line, which lost its links on every DOCX export. Workaround: applyMonospaceToCode now drops the code/pre/tt/kbd/samp wrapper entirely. The non-anchor content gets wrapped in monospace spans; anchors are emitted unstyled so they keep their hyperlink. For block-level usage (<pre>, or <code> with an inline style attr) we emit a wrapping <p> and forward the text-align style. Run BEFORE wrapLooseLines so the <p> doesn't get double-wrapped. Tests added: - inline <code> -> just a styled span (no <code> wrapper) - <code style='text-align:right'> -> <p style> wrap - <pre> -> always block-wrapped - <tt>/<kbd>/<samp> -> inline span only - regression: <a href> inside <code> survives html-to-docx round-trip with both the URL in word/_rels/document.xml.rels AND a <w:hyperlink> in the document body Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7538): HTML import line-break drift between blocks Etherpad's HTML export wraps each pad line in <p>...</p> (or <h1>, <code>, etc.) and then appends a <br> between lines. The closing block tag already ends the line for contentcollector, so the trailing <br> is redundant -- and on import the server collector counts BOTH as line breaks, doubling every blank line between paragraphs and inserting an extra blank between adjacent headings. Two fixes, both gated on the runtime block-element registry so they don't double-trigger when the underlying plugin already handles adjacency: 1. HTML import path now runs the new collapseRedundantBrAfterBlocks helper before setPadHTML. Drops a single <br> immediately following </p>/</h1-6>/</code>/</pre>/</div>/</blockquote>/</ul>/ </ol>/</li>/</table>/</tr>/</td>/</th>. Multiple consecutive <br>s after a block keep all but the first (the rest still represent intentional blank lines). 2. The DOCX-import separateAdjacentHeadingBlocks workaround now checks whether 'h1' is in the runtime ccRegisterBlockElements set before inserting <br>s. When ep_headings2 has the new server hook (per ep_plugin_helpers#14 + the upcoming ep_headings2 PR), the workaround correctly stays out of the way -- otherwise it adds an extra blank line per heading transition. Also fixed a subtle ts-check failure on the import.ts test changes and a leftover implicit-any in ImportDocxNative's alignment preserver. Tests added: - collapseRedundantBrAfterBlocks: 5 unit tests (each block tag, whitespace tolerance, multiple <br> keeping intentional blanks) - HTML import: 'does not introduce a blank line between H1 and H2', 'preserves blank-line count between H1 and H2 (realistic shape)' reproduces the 5-blanks-where-2-expected bug from the user's round-trip pad. 1054 backend tests pass locally (the 6 failures are the pre-existing favicon/webaccess send@1.x dotfile-path issue from running under .claude/, doesn't reach CI). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(7538): skip plugin-dependent HTML import tests on no-plugin CI The two new HTML-import-adjacency tests assume ep_headings2 (or another plugin) has registered h1/h2 as server-side block elements via ccRegisterBlockElements. Without that, contentcollector treats <h1>/<h2> as inline and adjacent ones merge into a single pad line -- making the assertions inapplicable. CI's backend-tests job runs without plugins installed, so guard the describe block with a runtime hooks.callAll() check and skip when h1 isn't a registered block. Local dev with ep_headings2 (and the local plugin patch wiring ccRegisterBlockElements) still exercises both tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
afee7969dd
|
fix(7696): scrollable settings popup on short viewports (#7703)
* fix(7696): make settings popup scroll on short viewports Move max-height + overflow:auto out of the mobile-only media query and onto the base .popup-content rule so the Settings popup (and other popups) gain a scrollbar instead of cropping items off-screen when the window is short. Pad-wide Settings is the worst offender because it adds a second column of controls plus a Delete pad button. Adds a Playwright regression test that verifies the popup is scrollable and the Delete pad button is reachable at a 900x500 viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7696): float nice-select dropdowns above scrollable popups Qodo flagged that making .popup-content a scroll container clips absolutely-positioned descendants — so the Settings popup's font and language dropdowns can be truncated when their list extends past the popup's scroll bounds on short viewports. Mirror the existing toolbar workaround: when a nice-select sits inside .popup-content, switch the list to position:fixed (CSS) and place it with viewport-relative coordinates from getBoundingClientRect (JS), respecting the existing reverse class for upward-opening lists. Also relax the regression test per Qodo: drop the brittle scrollHeight > clientHeight assertion in favour of asserting the popup declares overflow-y:auto and proving Delete pad is initially off-screen, then reachable via scroll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(7696): nice-select reverse list disappeared in scrolled popup When a nice-select inside a popup-content scroll container sits in the lower half of the viewport, the JS adds the .reverse class so the list opens upward. The default .reverse rule sets bottom: calc(100% + 5px), which is fine when the list is position:absolute relative to its parent — but with the position:fixed treatment the popup branch uses, that percentage resolves against the viewport and pushes the list ~100vh above the screen, so it appears not to open at all until you scroll to the bottom of the popup (where .reverse no longer triggers). Override the rule for both .toolbar and .popup so .reverse drops back to bottom: auto and JS-set `top` controls placement, with a JS belt-and- braces also setting `bottom: auto` inline. Adds a Playwright regression test that scrolls the settings popup to the bottom, opens the Pad-wide font dropdown, and asserts the list is both visible and inside the viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
85c941fe95
|
feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings (#7698)
* feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings
Native pad-wide settings ride a single padOptions object: the server seeds
clientVars.initialOptions, the client mutates via pad.changePadOption(), and
the existing padoptions COLLABROOM message broadcasts changes. Plugins can't
use the same rail today because applyPadSettings (client) and
normalizePadSettings (server) silently drop any key not in their hardcoded
whitelist.
Add a passthrough loop that preserves keys matching /^ep_[a-z0-9_]+$/ on both
sides. Plugins can now stash their pad-wide values under their own namespace
(e.g. pad.padOptions.ep_table_of_contents = {enabled: true}) and inherit the
existing broadcast, persistence, creator-only-write enforcement, and
enforceSettings semantics for free.
A new src/node/utils/PluginCapabilities module exposes
padOptionsPluginPassthrough = true so plugins can feature-detect via
require() and fall back to per-user behavior on older cores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Qodo review on PR #7698
Four concerns raised by Qodo (qodo-free-for-open-source-projects):
1. Feature flag — AGENTS.MD §52 requires new features behind a flag,
disabled by default. Add `enablePluginPadOptions` (default false) gating
the passthrough on both server (normalizePadSettings) and client
(applyPadSettings, via clientVars). Plugins detect the runtime state
through clientVars.enablePluginPadOptions; the static
PluginCapabilities flag stays as the "core can do this" signal.
2. Documentation — add a "Plugin-namespaced pad-wide options" section to
doc/plugins.md covering capability detection, the runtime flag, the
key namespace pattern, and the validation rules. Mirror the flag
description in settings.json.template.
3. Unbounded payload — values for ep_* keys are persisted with the pad and
broadcast to every connected client, so an unvalidated path was a
reliability hazard. Validate every ep_* value:
- Must round-trip through JSON.stringify (rejects functions, symbols,
BigInt, circular refs).
- Per-key serialized size capped at 64 KB.
- Combined ep_* size capped at 256 KB per pad.
Rejects drop the value with a console.warn line; the rest of the pad
settings round-trip cleanly.
4. PadOption type — add `[k: \`ep_${string}\`]: unknown` index signature
so the SocketIO message type matches runtime behavior; TS callers no
longer need unsafe casts to read plugin-namespaced keys.
Also extends the backend test suite with cases covering the runtime flag
(off/on), JSON-serializability rejection, per-key cap, and total cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(snap-tests): assert_grep — use here-string to dodge pipefail SIGPIPE
`assert_grep` ran `printf '%s' "$out" | grep -q -F -- "$needle"` under
`set -o pipefail`. When grep matched early it closed its stdin, printf
got SIGPIPE on its next write (exit 141), and pipefail propagated the
broken-pipe failure to the pipeline — making `if` see non-zero and
falling into the FAIL branch even though grep itself succeeded.
Failure was timing-dependent: it only fired when `$out` was large enough
that printf hadn't flushed before grep exited. CI ubuntu-latest tipped
into the racy path on PR #7698 once `settings.json.template` grew by 11
lines (the new `enablePluginPadOptions` flag); the symptom was the
`Wrapper unit tests` step reporting `dbType rewritten to sqlite ✗` with
"got: /*…" output even though the seeded file did contain the needle.
Replace the pipe with a here-string so grep gets its input in one shot
with no pipe between processes — no SIGPIPE possible. The fail-message
`head -3` is converted to a here-string for the same reason.
Repro on a runner whose pipe-buffer flush is slower than grep's first
match would have hit the same flake on any PR; the bug isn't about
this particular template change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c7ac1cce6f
|
fix(a11y): localized aria-label + title on export-as links (#7697)
* fix(a11y): localized aria-label + title on export-as links The six export anchors in the import/export dialog had no aria-label or title; their accessible name relied on the inner icon span's translated text (e.g. "Etherpad"). That announces just the format name with no "export" verb context, and the icon span doubles as the visible glyph which screen readers also pick up. Add `data-l10n-id="pad.importExport.export<format>a.title"` to each anchor — html10n populates both the `title` attribute and `aria-label` from the same key, so screen readers announce e.g. "Export as Etherpad" and sighted users get a tooltip. Mark the inner icon span `aria-hidden="true"` so SR doesn't double-read the format name. Strengthens the existing `a11y_dialogs.spec.ts` "export links" test to assert aria-label/title are populated and inner spans are aria-hidden, and only checks the three formats that are present without soffice. * Address Qodo review - Add rel="noopener" to each export anchor: closes the reverse-tabnabbing window-opener gap. Other target="_blank" links in pad.html (e.g. the "Powered by Etherpad" link) already follow this hardening pattern. - Pin Playwright locale to en-US for the a11y_dialogs spec: this file already asserts specific English strings (e.g. "Close chat", "Active users on this pad"); without the pin, translatewiki updates would break those tests. Tighten the export-anchor assertions to exact-match the English aria-label/title now that the locale is stable, and assert rel="noopener" too. |
||
|
|
bcf5c91c15
|
Localisation updates from https://translatewiki.net. | ||
|
|
a356414254
|
fix(socialMeta): coerced numeric/boolean override silently dropped (#7692)
* fix(socialMeta): coerced numeric/boolean override silently dropped Qodo flagged on PR #7691: Settings.coerceValue() turns numeric-looking env vars into numbers and "true"/"false" into booleans, so e.g. SOCIAL_META_DESCRIPTION="2026" arrives at the resolver as the number 2026. The previous resolver gated on `typeof override === 'string'`, so it silently fell back to the i18n catalog with no warning — the operator's docker config would appear broken. Accept string|number|boolean and stringify before the empty-check; null / undefined / unsupported types still fall through to the catalog. Two new unit specs cover the numeric and boolean coercion paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(socialMeta): widen description type to match coerced runtime Action Qodo bug-find: SettingsType.socialMeta.description and SocialMetaSettings.description still claimed `string | null`, but Settings.coerceValue() can produce number|boolean from env-var-driven config — the previous resolver fix was correct at runtime but the type mismatch forced `as unknown as string` casts in the new tests. Widen both declared types to `string | number | boolean | null` to match runtime reality, drop the typeof string|number|boolean guards in the resolver (the union now narrows automatically) and remove the test casts. Behaviour is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
90aafb115e
|
feat(socialMeta): settings.socialMeta.description override (#7599 follow-up) (#7691)
Issue #7599 follow-up from @stffen: the OG description has no obvious settings.json knob and the i18n catalog default is English, but most preview crawlers (WhatsApp, Signal, Slack, Telegram, Facebook) don't send Accept-Language and so always hit the English fallback regardless of how many locale files translate `pad.social.description`. Keep the i18n catalog as the default source — translatable strings belong in locale files, per the original Qodo review on PR #7635 — but add an explicit `socialMeta.description` setting that wins when set as a non-empty string, regardless of negotiated language. This is the lever that fixes the crawler case for non-English instances without re-introducing per-language config in settings.json (operators who want that still use customLocaleStrings). - Empty/whitespace overrides are treated as unset (would otherwise silently blank the preview). - Override is HTML-escaped via the same path as every other value. - og:locale stays language-negotiated; only the description is forced. - Documented next to publicURL in settings.json.template and settings.json.docker (env var SOCIAL_META_DESCRIPTION). The customLocaleStrings example now spells out pad.social.description so operators discover both routes. 5 new unit specs + 4 new integration specs cover override-wins, null/missing fallback, blank-treated-as-unset, and HTML-escaping. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ab0cff4d95
|
fix(7606): sync theme-color meta with client-side dark-mode switch (#7690)
* fix(7606): sync theme-color meta with client-side dark-mode switch PR #7636 emits <meta name="theme-color"> server-side from settings.skinVariants. That covers operators who hard-code a dark toolbar in settings.json, but not the runtime path: pad.ts auto-flips the toolbar to super-dark when enableDarkMode is on, the browser reports prefers-color-scheme: dark, and no localStorage white-mode override is set, plus the user can flip it via #options-darkmode. Both paths run skinVariants.updateSkinVariantsClasses(), which until now never touched the meta — so dark-mode users kept the light #ffffff baseline and saw a white address bar above a dark toolbar (stffen on #7606 after 2.7.3). Push the toolbar-color lookup into updateSkinVariantsClasses so the meta tracks every class change: the auto-switch on init, the user toggle, and the skinVariants builder. Mirrors the CSS-source-order table from src/node/utils/SkinColors.ts (last matching *-toolbar token wins). When no meta is present (non-colibris skin, server omits it) the helper is a no-op. Adds Playwright coverage for both paths under colorScheme: 'light' (manual toggle) and 'dark' (auto-switch on dark-OS clients — the case stffen reported). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(theme-color): action Qodo PR review (1) Bug — duplicated toolbar→color table: extract the CSS-source-order mapping and the default-color constant into src/static/js/skin_toolbar_colors, re-imported by both src/node/utils/SkinColors.ts (server, EJS template helper) and src/static/js/skin_variants.ts (client, runtime updates). Lives under static/js so the browser bundle can resolve it; server-side imports of static/js modules already exist (Changeset, AttributeMap, ImportHtml, hooks). One source of truth means a future palette change can no longer silently desync the server-rendered baseline meta from the client updates, which was the exact regression that brought us here. (2) Rule violation — 4-space continuation indentation in the new Playwright spec: re-indent the themeColor helper and the multiline test(...) call to the repo's 2-space rule (.editorconfig). Existing backend coverage (configuredToolbarColor unit tests + the specialpages server-render checks for both pad and timeslider) still passes against the refactored helper, so it's regression-locked end to end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fe9727b31e
|
fix(docker): share corepack cache so etherpad user can resolve pnpm (#7689)
* fix(docker): share corepack cache so etherpad user can resolve pnpm (#7687) PR #7674 switched the Dockerfile from `npm install -g pnpm` to corepack and `corepack prepare pnpm@${PnpmVersion} --activate`. The activate step runs as root and writes its lastKnownGood pin into `$COREPACK_HOME`, which defaults to `~/.cache/node/corepack` — i.e. a per-user path. The Dockerfile then drops to `USER etherpad` and later runs `bin/installLocalPlugins.sh`, which invokes `pnpm` as etherpad. With an empty per-user corepack cache and no shared activation file, corepack re-resolves pnpm and (for forks/configs without a `packageManager` pin matching the activated version) can fall back to "latest" from the registry — pulling `pnpm@10.33.4` instead of the requested 11.x and failing the workspace's `engines.pnpm` check. Pin `COREPACK_HOME=/opt/corepack` and chown it to etherpad after the prepare step. Both root and etherpad now share the same lastKnownGood file and tarball cache, so etherpad inherits the activated pnpm without hitting the registry again. Verified end-to-end: - `docker build --target development --build-arg ETHERPAD_LOCAL_PLUGINS=ep_test` with a stub local plugin runs `installLocalPlugins.sh` cleanly: `Done in 16.6s using pnpm v11.0.6`. - `docker run ... pnpm --version` as etherpad reports 11.0.6 from the shared cache — no "Unsupported environment" error. Note: corepack still emits a one-time "about to download" line at runtime because `corepack prepare pnpm@11.0.6` resolves to the highest matching patch (11.0.8) at build time while the project's `packageManager` field pins exactly 11.0.6. That's a follow-up — the download succeeds non-interactively and the engine check passes. Fixes #7687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(docker): action Qodo PR review (#7687 follow-up) - Replace hard-coded /opt/corepack with ${COREPACK_HOME} in mkdir/chown so the env var stays the single source of truth (Qodo: "COREPACK_HOME path duplication"). - Add a build-test-local-plugin job to .github/workflows/docker.yml that builds the development target with a stub ETHERPAD_LOCAL_PLUGINS so the original failure mode (corepack/pnpm cache invisible across the USER switch) cannot silently regress (Qodo: "COREPACK_HOME fix lacks test"). The job is small — `docker build` only, no run — and uses the shared GHA buildx cache. Verified: same docker build + `docker run pnpm --version` flow on the variable form gives identical output (pnpm 11.0.6 from the etherpad-owned cache). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b751fd7b9e
|
feat(settings): enable Pad-wide Settings by default; fix misleading modal title (#7679)
* feat(settings): enable Pad-wide Settings by default; fix misleading modal title The creator-owned Pad-wide Settings feature (#7545) shipped behind a flag that defaulted to false. With the flag off the modal still rendered an H1 of `pad.settings.padSettings` ("Pad-wide Settings") for *every* user, even though no pad-wide controls were ever shown. Two readers in different browsers both saw "Pad-wide Settings" as the modal title, which looked like a creator-gate regression but was just a copy bug. Two changes: 1. Flip the default of `enablePadWideSettings` to `true` (Settings.ts plus both settings templates). With the feature on, the creator (revision-0 author) gets a real "Pad-wide Settings" section gated by `clientVars.canEditPadSettings`, while every other user sees only "User Settings" — matching the design intent of #7545. This is a behavior change, so the settings comments are expanded to describe what the toggle now does. 2. Drop the conditional H1 in `src/templates/pad.html` and always use `pad.settings.title` ("Settings"). Operators who explicitly disable the feature shouldn't see a label that lies about a section that isn't rendered. Adds backend regression coverage in `tests/backend/specs/socketio.ts`: - Different browsers (different cookie jars => different authorIDs): only the first joiner gets `canEditPadSettings: true`. - Same browser, two tabs (shared HttpOnly token cookie => same authorID): both connections are the same identity, both correctly land on the creator path. * test(settings): regression coverage for the settings modal H1 Asserts the rendered `/p/<id>` HTML always uses `data-l10n-id="pad.settings.title"` for the modal heading, regardless of `enablePadWideSettings`. Catches a re-introduction of the old conditional that printed "Pad-wide Settings" for every user when the feature was off. Action of Qodo review feedback on PR #7679. |
||
|
|
da87a4fca2
|
fix(7686): username 'false' / 'malformed color: false' for legacy settings.json (#7688)
* fix(7686): legacy padOptions.userName/userColor=false breaks pad
Settings.json files generated before December 2021 used `false` as the
default for these two string options (commit
|
||
|
|
45b000d533 | Merge branch 'master' into develop | ||
|
|
07902d3da1 | Merge branch 'develop' | ||
|
|
db602bcb70 | bump version | ||
|
|
5e74928317 | chore: use jq for version replacement [skip ci] | ||
|
|
ad9f424eef | chore: use --no-git-tag-version for pnpm version to support dirty trees during release | ||
|
|
0b0883c02b | chore: allow installs on release | ||
|
|
484d48ad46 | chore: update changelog for 2.7.3 release | ||
|
|
bfdbd2bb91
|
chore: removed axios (#7685)
* chore: removed axios * chore: pnpm |
||
|
|
6c7598d88b | chore: fixed deb package | ||
|
|
958590d1c8
|
chore(docker): clear most CVEs in published image (npm/pnpm/uuid + drop curl) (#7674)
* chore(docker): clear most CVEs in published image — npm/pnpm/uuid + drop curl Cuts published-image vulnerabilities from 18 (4H/13M/1L) across 8 packages to 12 (2H/9M/1L) across 3 packages. The remaining three (curl/libcurl, git, busybox) are all upstream Alpine 3.23 packages with "not fixed" status — libcurl is pulled in transitively by git and cannot be removed independently. Changes: - Provision pnpm via corepack instead of `npm install -g pnpm`, then remove the bundled npm. The base image's npm@10.9.7 ships old transitives (picomatch 4.0.3 → CVE-2026-33671/33672, brace-expansion 2.0.2 → CVE-2026-33750) that we don't otherwise need at runtime; corepack handles pnpm directly without npm. Fixes 1H + 1M. - Bump PnpmVersion 10.28.2 → 10.33.2 to align with the rest of the workflow and pull in pnpm's patched bundled brace-expansion (5.0.5 vs 5.0.4). Fixes 1M. - Add `uuid@<14.0.0` → `>=14.0.0` to pnpm.overrides (GHSA-w5hq-g745-h8pq). Fixes 1M. - Drop `curl` from the runtime apk add list and switch HEALTHCHECK to wget (busybox built-in). curl was only invoked by the healthcheck and by dev/CI scripts that don't run in the container. Removes the curl CLI binary; libcurl remains as a git transitive dep, so the `apk/alpine/curl` advisories scout reports against libcurl persist but aren't reachable from any code we ship. As a side-effect this also clears nghttp2 (CVE-2026-27135) which was a curl-CLI dep. - Switch HEALTHCHECK URL from `localhost` to `127.0.0.1` — alpine/musl resolves localhost to ::1 first and Etherpad only binds IPv4. Verified locally: docker build → docker run → healthy → docker scout cves shows 12 CVEs / 3 packages. * fix(docker): refresh corepack before preparing pnpm (Qodo) Node 22's bundled corepack ships a stale signing-key list and can reject newer pnpm releases (nodejs/corepack#612), which would fail the image build at `corepack prepare`. Mirror the snap/snapcraft.yaml workaround: `npm install -g corepack@latest` before activating pnpm, in both adminbuild and build stages. npm is still removed afterwards. * docs(changelog): note docker image dropping curl/npm/npx (Qodo) Address Qodo's "backwards-incompatible change without mitigation" rule violations by documenting the removal in the 2.7.3 breaking-changes section. Operators who exec into the container can apk add curl on demand or use the busybox wget / pnpm already present. * chore: pnpm * chore: pnpm * chore: pnpm * chore: pnpm * chore: pnpm * chore: pnpm * chore: pnpm * chore: pnpm * chore: pnpm --------- Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com> |
||
|
|
fb09b11ff5
|
build(deps): bump lru-cache from 11.3.5 to 11.3.6 (#7671)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.3.5 to 11.3.6. - [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.3.5...v11.3.6) --- updated-dependencies: - dependency-name: lru-cache dependency-version: 11.3.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> |
||
|
|
045505b0e9
|
build(deps): bump axios from 1.15.2 to 1.16.0 (#7672)
Bumps [axios](https://github.com/axios/axios) from 1.15.2 to 1.16.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.15.2...v1.16.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.16.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> |
||
|
|
4accea429b
|
build(deps): bump actions/download-artifact from 4 to 8 (#7668)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' 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> |
||
|
|
25dd8e492e
|
build(deps): bump actions/upload-artifact from 4 to 7 (#7669)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact 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> |
||
|
|
70415714e6
|
fix(socketio): don't kick authenticated duplicate-author sessions (#7656) (#7678)
* fix(socketio): don't kick authenticated duplicate-author sessions (#7656) The CLIENT_READY handler kicks any prior socket whose authorID matches the joining socket's, originally as a workaround for stale tabs in the same browser (cookie-derived authorIDs were per-browser, so "same authorID, same pad" reliably meant "page refresh / second tab in this browser"). With stable identities (basic auth, SSO, apikey, getAuthorId hook) the same authorID can legitimately appear across windows or devices, so the kick disconnects real concurrent sessions. Skip the kick when the joining socket has req.session.user set; cookie-only sessions keep the existing behavior so the userdup modal and the xxauto_reconnect path still work. * fix(socketio): suppress USER_LEAVE when other same-author sockets remain With the duplicate-author kick disabled for authenticated sessions, a single authorID can legitimately span multiple sockets in one pad. handleDisconnect was emitting USER_LEAVE on every socket close, which made clients (whose presence is keyed by authorID) drop the author entirely even when another socket of theirs was still online. Only broadcast USER_LEAVE — and only run the userLeave hook — when the disconnecting socket is the last one in the pad for that author. Adds two backend tests: - authenticated identity: closing one of two same-author sockets does NOT emit USER_LEAVE on the other. - different authors (regression): closing socket A still emits USER_LEAVE for socket B. Action of Qodo review feedback on PR #7678. |
||
|
|
d9e1be04f4
|
build(deps): bump mssql from 12.5.0 to 12.5.2 (#7675)
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.0 to 12.5.2. - [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.0...v12.5.2) --- updated-dependencies: - dependency-name: mssql dependency-version: 12.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> |
||
|
|
96c032ae3e
|
build(deps): bump rate-limiter-flexible from 11.0.1 to 11.1.0 (#7683)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 11.0.1 to 11.1.0. - [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases) - [Commits](https://github.com/animir/node-rate-limiter-flexible/compare/v11.0.1...v11.1.0) --- updated-dependencies: - dependency-name: rate-limiter-flexible dependency-version: 11.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> |
||
|
|
1e156660e9
|
build(deps): bump @elastic/elasticsearch from 9.3.4 to 9.4.0 (#7682)
Bumps [@elastic/elasticsearch](https://github.com/elastic/elasticsearch-js) from 9.3.4 to 9.4.0. - [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.3.4...v9.4.0) --- updated-dependencies: - dependency-name: "@elastic/elasticsearch" dependency-version: 9.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> |
||
|
|
74e769a182
|
build(deps): bump express-rate-limit from 8.4.1 to 8.5.1 (#7681)
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.4.1 to 8.5.1. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.4.1...v8.5.1) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.5.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> |