Skip to content

fix(pad): redesign outdated-version notice (#7799)#7804

Merged
JohnMcLear merged 17 commits into
developfrom
fix/7799-outdated-notice-redesign
May 18, 2026
Merged

fix(pad): redesign outdated-version notice (#7799)#7804
JohnMcLear merged 17 commits into
developfrom
fix/7799-outdated-notice-redesign

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Closes #7799.

Replaces the persistent, all-visitor "Etherpad on this server is severely outdated" banner with a dismissable gritter toast, shown only to a pad's first author (pool position 0 in the attribute pool), only when the running server is at least one minor version behind the latest published release.

Behaviour changes

  • The persistent #version-badge bottom-right banner is gone. A gritter (auto-fades after 8s, X-dismissable) takes its place.
  • The notice fires only on minor-or-more deltas. Patch-only deltas (e.g. 3.0.1 → 3.0.2) no longer fire.
  • Non-creators of a pad never see the notice.
  • updates.tier = 'off' remains the full kill-switch and is unchanged.

API changes

  • GET /api/version-status now takes an optional ?padId=<id> query parameter and returns {outdated: 'minor' | null, isFirstAuthor: boolean}.
  • The severe and vulnerable enum values, the vulnerable-below directive scraping, the vulnerableBelow state field, and the vulnerable/vulnerable-new-release email kinds have all been removed.
  • isSevere (the Notifier signal that triggers the "outdated" admin email) now fires on minor-or-more-behind, matching the new gritter signal. The email copy is updated to match.

Implementation notes

  • The route resolves the requester's author via the express session cookie (resolveRequestAuthor) and only computes the per-pad first-author match when both state.latest exists AND the current version is minor-or-more behind. The padId is validated via padManager.isValidPadId before any DB call.
  • Results are cached per (padId, authorId) for 60s in a 1000-entry LRU with single-flight on misses.

Design spec + implementation plan committed under docs/superpowers/specs / docs/superpowers/plans.

Test plan

  • Backend vitest suite green (702 / 703; the one pre-existing failure is the unrelated backend-tests-glob.test.ts which expects ../node_modules/ep_*/static/tests/backend/specs to match — a fresh-clone harness limitation, present on develop)
  • New backend coverage: tests/backend-new/specs/updater/versionCompare.test.ts (semver helper, 18 cases), tests/backend-new/specs/hooks/express/firstAuthorOf.test.ts (6 cases), tests/backend-new/specs/hooks/express/updateStatus.test.ts (9 cases including LRU eviction, single-flight, first-author gating)
  • New Playwright coverage: src/tests/frontend-new/specs/outdated_notice.spec.ts (6 cases incl. auto-fade, X-dismiss, server-500, client-side isFirstAuthor=false guard)
  • tsc --noEmit clean
  • Manual: dev server with var/update.state.json pinned to a higher latest.version — verify gritter appears once for the pad's first author, absent for second visitor

🤖 Generated with Claude Code

JohnMcLear and others added 17 commits May 18, 2026 11:09
Per-pad first-author gating, dismissable gritter, minor-or-more rule, drop vulnerable UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 bite-sized tasks, TDD-first where applicable; closes the spec end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds isMinorOrMoreBehind(current, latest) which returns true only when
the latest release is at least one minor version ahead (patch-only deltas
return false). Removes isMajorBehind, parseVulnerableBelow, and
isVulnerable from versionCompare.ts — callers in updateStatus.ts,
VersionChecker.ts, and index.ts will be updated in subsequent tasks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove VulnerableBelowDirective type, UpdateState.vulnerableBelow field, and
all related scraping/checking logic (parseVulnerableBelow, isVulnerable imports).
Clean up Notifier, OpenAPI schema, and all test fixtures to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove `vulnerableAt` and `vulnerableNewReleaseTag` from the
`EmailSendLog` interface, `EMPTY_STATE`, and the `isValidEmail`
validator — these backed the removed `vulnerable`/`vulnerable-new-release`
email kinds and are now dead code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Export firstAuthorOf() from updateStatus.ts — finds the lowest-numbered
author attrib in a pad's pool, skipping empty-string placeholders.
Covered by 6 vitest cases in tests/backend-new/specs/hooks/express/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace global badge cache with a per-(padId, authorId) LRU cache. The
new response shape is {outdated: 'minor' | null, isFirstAuthor: boolean};
the old 'severe'/'vulnerable' enum is dropped entirely. computeOutdated
now resolves the pad's first author and compares it against the session
author before returning outdated:'minor', so the notice is only shown to
the person who created the pad.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… behind

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the /api/version-status GET operation to the admin OpenAPI spec with
the new pad-aware response shape: outdated enum reduced to [minor]|null,
isFirstAuthor boolean, and an optional padId query param.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renames pad_version_badge.ts → pad_outdated_notice.ts and rewrites it
as a fire-and-forget gritter notice that only shows when the API reports
outdated=minor AND the current user is the pad's first author.  Wires
the new maybeShowOutdatedNotice() call into pad.ts immediately after
showPrivacyBannerIfEnabled().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Six Playwright specs exercise maybeShowOutdatedNotice: null response,
isFirstAuthor:false guard, positive appearance + text, X-dismiss,
500 server error tolerance, and 8 s auto-fade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete the GET /api/version-status describe block from the legacy mocha
spec (asserted outdated:null and outdated:'severe' — both no longer match
the new response shape). The new vitest spec at
tests/backend-new/specs/hooks/express/updateStatus.test.ts covers this
surface comprehensively.

Delete src/tests/frontend-new/specs/pad-version-badge.spec.ts entirely:
all three tests reference the #version-badge DOM element removed in Task 8
and stub 'severe'/'vulnerable' enum values that no longer exist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…docs

- Remove OutdatedLevel type (null|'severe') from types.ts — no consumers
  remain after the badge redesign removed the severe tier.
- Fix Notifier severe-email body: was "more than one major release behind"
  but isSevere now fires on minor-or-more, so update to "at least one
  minor release behind the latest published version".
- Drop "vulnerability directives" from the /admin/update/status OpenAPI
  description; replace with the actual response fields.
- Remove stale vulnerableBelow field from UpdateStatusPayload in
  admin/src/store/store.ts — server no longer sends it.
- Fix docs/admin/updates.md: "pad-side badge" → "pad-side notice".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Redesign outdated-version notice from persistent banner to dismissable first-author-only gritter

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• Redesigned the outdated-version notice from a persistent bottom-right banner to a dismissable
  gritter toast notification
• Changed visibility to first-author-only (pad creator) instead of showing to all visitors
• Updated version comparison logic from major-behind to minor-or-more-behind, excluding patch-only
  updates
• Rewrote /api/version-status endpoint to be pad-aware with optional ?padId query parameter
• Added per-(padId, authorId) LRU cache (1000 entries, 60s TTL) with single-flight pattern for
  concurrent misses
• Updated response schema from {outdated: enum} to `{outdated: 'minor'|null, isFirstAuthor:
  boolean}`
• Removed all vulnerability-related code: vulnerable enum values, vulnerable-below directive
  scraping, vulnerableBelow state field, and vulnerable email kinds
• Simplified isSevere notifier signal to fire on minor-or-more-behind instead of major-behind
• Added comprehensive test coverage: 18 semver comparison cases, 6 first-author extraction cases, 9
  endpoint e2e cases, 6 frontend Playwright cases
• Updated documentation (AsciiDoc, Markdown, admin guide) and CHANGELOG to reflect new behavior
• Deleted obsolete pad_version_badge.ts and related tests
Diagram
flowchart LR
  A["Persistent<br/>Version Badge"] -->|Redesign| B["Dismissable<br/>Gritter Toast"]
  C["All Visitors"] -->|Gating| D["First Author Only"]
  E["Major-Behind<br/>Logic"] -->|Update| F["Minor-or-More<br/>Behind Logic"]
  G["Simple Boolean<br/>Cache"] -->|Replace| H["Per-padId-authorId<br/>LRU Cache"]
  I["Vulnerable<br/>Email Logic"] -->|Remove| J["Simplified<br/>Notifier"]
  B --> K["8s Auto-fade<br/>+ X-Dismiss"]
  D --> K
  F --> K
  H --> K
Loading

Grey Divider

File Changes

1. src/tests/backend-new/specs/hooks/express/updateStatus.test.ts 🧪 Tests +315/-0

End-to-end vitest coverage for version-status route

• New comprehensive end-to-end test suite for /api/version-status route with 9 test cases
• Tests cover cache behavior, LRU eviction, first-author gating, and version comparison logic
• Uses mocked loadState, PadManager, and fake session middleware for isolated testing
• Validates single-flight pattern and per-(padId, authorId) cache key isolation

src/tests/backend-new/specs/hooks/express/updateStatus.test.ts


2. src/node/hooks/express/updateStatus.ts ✨ Enhancement +101/-31

Pad-aware version-status route with first-author gating

• Rewrote /api/version-status route to be pad-aware with optional ?padId query parameter
• Added firstAuthorOf helper to extract first author from pad's attribute pool
• Added resolveRequestAuthor helper to resolve session author from express-session
• Replaced simple boolean cache with per-(padId, authorId) LRU cache (1000 entries, 60s TTL)
• Changed response from {outdated: enum} to {outdated: 'minor'|null, isFirstAuthor: boolean}
• Implemented single-flight pattern for concurrent cache misses
• Added test-only helpers _setBadgeCacheCapForTests and _resetBadgeCacheForTests

src/node/hooks/express/updateStatus.ts


3. src/tests/backend-new/specs/updater/versionCompare.test.ts 🧪 Tests +34/-47

Update version comparison tests for minor-behind logic

• Replaced isMajorBehind, parseVulnerableBelow, and isVulnerable tests with new
 isMinorOrMoreBehind tests
• Added 6 new test cases for the minor-or-more-behind logic covering equal, patch-only, minor, and
 major deltas
• Reorganized test structure with improved test descriptions and edge case coverage

src/tests/backend-new/specs/updater/versionCompare.test.ts


View more (30)
4. src/tests/frontend-new/specs/outdated_notice.spec.ts 🧪 Tests +124/-0

Playwright frontend tests for outdated notice gritter

• New Playwright test suite with 6 test cases for the outdated notice gritter
• Tests cover null response, non-first-author guard, gritter visibility, dismissal, server errors,
 and auto-fade
• Uses route interception to mock /api/version-status responses
• Validates gritter text content and auto-fade timing (8 seconds)

src/tests/frontend-new/specs/outdated_notice.spec.ts


5. src/node/updater/Notifier.ts Refactor +5/-36

Simplify notifier to remove vulnerable email logic

• Removed isVulnerable and vulnerable-new-release email kinds from EmailKind type
• Removed vulnerableBelow and vulnerableNewReleaseTag fields from NotifierInput interface
• Simplified decideEmails logic to only handle isSevere (no longer checks isVulnerable)
• Updated severe email subject and body to reflect minor-or-more-behind instead of major-behind
• Removed VULNERABLE_INTERVAL constant and all vulnerability-related email cadence logic

src/node/updater/Notifier.ts


6. src/node/hooks/express/openapi-admin.ts 📝 Documentation +40/-14

OpenAPI documentation for pad-aware version-status endpoint

• Added OpenAPI documentation for /api/version-status endpoint with query parameter and response
 schema
• Updated /admin/update/status description to remove reference to vulnerability directives
• Removed VulnerableBelowDirective schema definition
• Updated UpdateStatus schema to remove vulnerableBelow field requirement

src/node/hooks/express/openapi-admin.ts


7. src/tests/backend/specs/openapi-admin.ts 🧪 Tests +1/-7

Update OpenAPI admin tests to remove vulnerable schema

• Removed assertion for vulnerableBelow field in UpdateStatus schema
• Removed test case checking for VulnerableBelowDirective sub-schema
• Updated test description to reflect removal of vulnerability directive schema

src/tests/backend/specs/openapi-admin.ts


8. src/tests/backend-new/specs/updater/VersionChecker.test.ts 🧪 Tests +2/-3

Remove vulnerable-below directive from version checker tests

• Removed vulnerable-below directive from test release body
• Removed assertion checking for vulnerableBelow array in the result
• Simplified test expectations to only validate release and etag fields

src/tests/backend-new/specs/updater/VersionChecker.test.ts


9. src/tests/backend-new/specs/hooks/express/firstAuthorOf.test.ts 🧪 Tests +42/-0

Unit tests for firstAuthorOf helper function

• New test suite with 6 test cases for the firstAuthorOf helper function
• Tests cover empty pads, non-author attributes, single/multiple authors, empty-string placeholders,
 and numeric key ordering
• Validates correct extraction of first author from attribute pool

src/tests/backend-new/specs/hooks/express/firstAuthorOf.test.ts


10. src/node/updater/VersionChecker.ts Refactor +3/-9

Remove vulnerable-below directive scraping from version checker

• Removed import of parseVulnerableBelow from versionCompare module
• Removed import of VulnerableBelowDirective type
• Removed code that scraped vulnerable-below directive from release body
• Removed vulnerableBelow array from CheckResult discriminated union variant
• Simplified return statement to only include release and etag

src/node/updater/VersionChecker.ts


11. src/static/js/pad_outdated_notice.ts ✨ Enhancement +43/-0

New gritter-based outdated notice client module

• New module replacing pad_version_badge.ts with gritter-based implementation
• Exports maybeShowOutdatedNotice async function that fetches /api/version-status with current
 padId
• Shows dismissable gritter only when outdated === 'minor' AND isFirstAuthor === true
• Gritter auto-fades after 8 seconds with class name outdated-notice for test targeting
• Gracefully handles fetch errors and missing jQuery/gritter without blocking pad load

src/static/js/pad_outdated_notice.ts


12. src/tests/backend-new/specs/updater/state.test.ts 🧪 Tests +1/-15

Remove vulnerable-below state validation tests

• Removed test cases validating vulnerableBelow field validation and error handling
• Removed test for missing threshold in vulnerableBelow entries
• Updated test for email subfield validation to remove vulnerableAt and vulnerableNewReleaseTag
 fields

src/tests/backend-new/specs/updater/state.test.ts


13. src/tests/frontend-new/admin-spec/update-banner.spec.ts 🧪 Tests +2/-2

Remove vulnerableBelow from admin update banner tests

• Removed vulnerableBelow: [] field from mocked /admin/update/status response payloads
• Updated two test cases to reflect the simplified response schema

src/tests/frontend-new/admin-spec/update-banner.spec.ts


14. src/static/js/pad.ts ✨ Enhancement +2/-2

Wire outdated notice into pad initialization flow

• Replaced import of pad_version_badge with import of maybeShowOutdatedNotice from
 pad_outdated_notice
• Added explicit call to maybeShowOutdatedNotice() in postAceInit after privacy banner check
• Removed self-bootstrapping side-effect import pattern in favor of explicit invocation

src/static/js/pad.ts


15. docs/superpowers/plans/2026-05-18-outdated-notice-redesign.md 📝 Documentation +1200/-0

Implementation plan for outdated-notice redesign

• Comprehensive 12-task implementation plan for the outdated-notice redesign
• Covers backend refactoring (version comparison, types, state), route rewrite, caching, testing
• Includes frontend module rewrite, Playwright tests, documentation updates, and verification gates
• Provides detailed step-by-step instructions with code examples and commit messages

docs/superpowers/plans/2026-05-18-outdated-notice-redesign.md


16. src/node/updater/index.ts Refactor +2/-8

Update updater index to use minor-behind logic

• Changed import from isMajorBehind, isVulnerable to isMinorOrMoreBehind
• Removed code that unions new vulnerableBelow directives with existing state
• Updated isSevere calculation to use isMinorOrMoreBehind instead of isMajorBehind
• Removed isVulnerable check from notifier input

src/node/updater/index.ts


17. src/node/updater/versionCompare.ts Refactor +7/-23

Replace major-behind with minor-or-more-behind version logic

• Added new isMinorOrMoreBehind function that returns true when current is at least one minor
 version behind latest
• Removed isMajorBehind function (replaced by new logic)
• Removed parseVulnerableBelow function and VULN_RE regex
• Removed isVulnerable function and VulnerableBelowDirective type import
• Kept parseSemver and compareSemver unchanged

src/node/updater/versionCompare.ts


18. doc/admin/updates.md 📝 Documentation +9/-12

Documentation updates for outdated-notice redesign

• Updated tier 1 description to reflect dismissable gritter instead of persistent badge
• Changed "severely outdated or vulnerable" to "at least one minor version behind"
• Removed severe and vulnerable definitions, replaced with single minor definition
• Simplified email cadence table to remove vulnerable triggers and weekly repeat
• Updated pad-side notice section with new gritter behavior and first-author gating
• Updated /api/version-status description to reflect new response shape and caching

doc/admin/updates.md


19. doc/api/http_api.adoc 📝 Documentation +28/-0

AsciiDoc documentation for version-status endpoint

• Added new /api/version-status endpoint documentation in AsciiDoc format
• Documents optional padId query parameter and response schema with outdated and isFirstAuthor
 fields
• Explains caching behavior (60s per padId/authorId) and disabling via updates.tier = 'off'

doc/api/http_api.adoc


20. doc/api/http_api.md 📝 Documentation +21/-0

Markdown documentation for version-status endpoint

• Added new /api/version-status endpoint documentation in Markdown format
• Documents optional padId query parameter and response schema with outdated and isFirstAuthor
 fields
• Explains caching behavior (60s per padId/authorId) and disabling via updates.tier = 'off'

doc/api/http_api.md


21. docs/superpowers/specs/2026-05-18-outdated-notice-redesign-design.md 📝 Documentation +278/-0

Design specification for outdated notice redesign

• Comprehensive design specification for redesigning the outdated-version notice from a persistent
 banner to a dismissable gritter toast
• Specifies that the notice is shown only to the pad's first author and only on minor-or-more
 version deltas (not patch-only)
• Details server-side changes including new isMinorOrMoreBehind() helper, removal of
 vulnerable-related code, and LRU-cached /api/version-status endpoint with per-pad first-author
 gating
• Outlines client-side changes including renaming pad_version_badge.ts to
 pad_outdated_notice.ts, removal of persistent badge DOM/CSS, and integration with gritter for
 8-second auto-fade dismissable notifications
• Includes comprehensive test plan covering backend semver comparison, first-author resolution,
 endpoint caching/single-flight, and frontend gritter behavior

docs/superpowers/specs/2026-05-18-outdated-notice-redesign-design.md


22. CHANGELOG.md 📝 Documentation +3/-0

Changelog entries for outdated notice redesign

• Added two new entries under "Notable enhancements" documenting the outdated-version notice
 redesign (#7799)
• First entry describes the UI change from persistent banner to dismissable gritter,
 first-author-only visibility, and removal of vulnerable-related features
• Second entry documents the /api/version-status API changes including new padId query
 parameter, updated response schema, and 60-second per-(padId, authorId) caching

CHANGELOG.md


23. admin/src/store/store.ts Additional files +0/-1

...

admin/src/store/store.ts


24. src/node/updater/state.ts Additional files +0/-11

...

src/node/updater/state.ts


25. src/node/updater/types.ts Additional files +0/-19

...

src/node/updater/types.ts


26. src/static/css/pad.css Additional files +0/-14

...

src/static/css/pad.css


27. src/static/js/pad_version_badge.ts Additional files +0/-46

...

src/static/js/pad_version_badge.ts


28. src/templates/pad.html Additional files +0/-1

...

src/templates/pad.html


29. src/tests/backend-new/specs/updater/Notifier.test.ts Additional files +2/-48

...

src/tests/backend-new/specs/updater/Notifier.test.ts


30. src/tests/backend/specs/updateStatus.ts Additional files +0/-30

...

src/tests/backend/specs/updateStatus.ts


31. src/tests/frontend-new/admin-spec/update-page-actions.spec.ts Additional files +0/-1

...

src/tests/frontend-new/admin-spec/update-page-actions.spec.ts


32. src/tests/frontend-new/admin-spec/update-scheduled.spec.ts Additional files +0/-1

...

src/tests/frontend-new/admin-spec/update-scheduled.spec.ts


33. src/tests/frontend-new/specs/pad-version-badge.spec.ts Additional files +0/-47

...

src/tests/frontend-new/specs/pad-version-badge.spec.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 18, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. vulnerable-below removed without deprecation 📘 Rule violation ☼ Reliability
Description
This PR removes the vulnerable-below directive parsing and the severe/vulnerable outdated
signals without any deprecation-period WARN log, which can break deployments and workflows that
relied on those signals. Per policy, the feature should emit a WARN and remain supported until the
next version before being removed.
Code

src/node/hooks/express/updateStatus.ts[R61-100]

+interface OutdatedResponse {
+  outdated: 'minor' | null;
+  isFirstAuthor: boolean;
+}
+
+const EMPTY: OutdatedResponse = {outdated: null, isFirstAuthor: false};
+
+const TTL_MS = 60 * 1000;
+let cache = new LRUCache<string, {value: OutdatedResponse; at: number}>({max: 1000});
+const inFlight = new Map<string, Promise<OutdatedResponse>>();
+
+/** Test-only setter: rebuild the LRU with a smaller cap so eviction can be asserted. */
+export const _setBadgeCacheCapForTests = (max: number): void => {
+  cache = new LRUCache<string, {value: OutdatedResponse; at: number}>({max});
+};
+
/** Test-only: clear the in-memory badge cache so integration tests see fresh state. */
export const _resetBadgeCacheForTests = (): void => {
-  badgeCache = {value: null, at: 0};
-  badgeInFlight = null;
+  cache.clear();
+  inFlight.clear();
+};
+
+const computeOutdated = async (
+  padId: string | null,
+  authorId: string | null,
+): Promise<OutdatedResponse> => {
+  const state = await loadState(stateFilePath());
+  if (!state.latest) return EMPTY;
+  const current = getEpVersion();
+  if (!isMinorOrMoreBehind(current, state.latest.version)) return EMPTY;
+  if (!padId || !authorId) return EMPTY;
+  // padManager is loaded via dynamic import to avoid circular-init w/ updater.
+  const padManagerMod: any = await import('../../db/PadManager');
+  const padManager = padManagerMod.default ?? padManagerMod;
+  if (typeof padManager.isValidPadId !== 'function' || !padManager.isValidPadId(padId)) return EMPTY;
+  if (!(await padManager.doesPadExist(padId))) return EMPTY;
+  const pad = await padManager.getPad(padId);
+  if (firstAuthorOf(pad) !== authorId) return EMPTY;
+  return {outdated: 'minor', isFirstAuthor: true};
};
Evidence
PR Compliance ID 4 requires feature removals to be preceded by a WARN deprecation and delayed until
a subsequent version. The CHANGELOG and updated route implementation show that vulnerable-below
and the severe/vulnerable signals were removed in this PR (no deprecation path is present in the
changed implementation).

CHANGELOG.md[7-8]
src/node/hooks/express/updateStatus.ts[61-100]
src/node/updater/VersionChecker.ts[14-75]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR removes the `vulnerable-below` functionality and the `severe`/`vulnerable` signals immediately, without emitting a deprecation WARN and without a deprecation period.

## Issue Context
- `CHANGELOG.md` explicitly states that `vulnerable-below`, `severe`, and `vulnerable` were removed.
- The `/api/version-status` implementation now only supports `{outdated: 'minor' | null, ...}`.
- The release-notes directive scraping that powered vulnerability signalling has been removed.

## Fix Focus Areas
- src/node/hooks/express/updateStatus.ts[61-100]
- src/node/updater/VersionChecker.ts[14-75]
- src/node/updater/Notifier.ts[13-64]
- src/node/updater/index.ts[159-165]
- doc/admin/updates.md[52-76]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Author lookup always null 🐞 Bug ≡ Correctness
Description
resolveRequestAuthor() reads req.session.user.author, but Etherpad does not populate an authorId
into the express-session user object for pad visitors, so /api/version-status will return the EMPTY
response and the outdated notice will never show. In Etherpad, the pad author identity is resolved
from the HttpOnly token/sessionID cookies via securityManager.checkAccess(), not from
express-session.
Code

src/node/hooks/express/updateStatus.ts[R41-59]

+export const resolveRequestAuthor = async (req: any): Promise<string | null> => {
+  const readAuthor = (): string | null => {
+    const a = req?.session?.user?.author;
+    return typeof a === 'string' && a !== '' ? a : null;
+  };
+  const fromSession = readAuthor();
+  if (fromSession !== null) return fromSession;
+  try {
+    const expressModule = await import('../express');
+    const mw = (expressModule as any).sessionMiddleware;
+    if (typeof mw !== 'function') return null;
+    await new Promise<void>((resolve, reject) => {
+      mw(req, {} as any, (err?: unknown) => (err ? reject(err) : resolve()));
+    });
+  } catch {
+    return null;
+  }
+  return readAuthor();
+};
Evidence
The new endpoint relies on express-session for author identity, but the server’s own pad
session/auth flow derives authorID from the token/sessionID cookies via
SecurityManager.checkAccess(), and socket.io’s use of express-session only increments a connection
counter. There is no code path shown that populates req.session.user.author for pad visitors, so
the route will not be able to recognize the first author in production.

src/node/hooks/express/updateStatus.ts[41-59]
src/node/hooks/express/updateStatus.ts[83-99]
src/node/handler/PadMessageHandler.ts[410-522]
src/node/db/SecurityManager.ts[60-126]
src/node/utils/ensureAuthorTokenCookie.ts[17-35]
src/node/hooks/express/socketio.ts[83-90]
src/node/hooks/express/webaccess.ts[199-204]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /api/version-status` currently derives the requester identity from `req.session.user.author`, but Etherpad’s pad-user authorID is not stored on the express-session `user` object. This causes `authorId` to be `null` in real pad requests, making `computeOutdated()` always return `EMPTY` and preventing the pad-side outdated notice from ever firing.

## Issue Context
Pad authorship is determined via the HttpOnly `${prefix}token` (and optionally `${prefix}sessionID`) cookies and enforced via `securityManager.checkAccess(...)` during the socket.io handshake.

## Fix Focus Areas
- src/node/hooks/express/updateStatus.ts[33-100]
 - Replace `resolveRequestAuthor()` + `req.session.user.author` usage with a cookie-based resolution.
 - Recommended approach: in the route handler or `computeOutdated()`, call `securityManager.checkAccess(padId, sessionIDCookie, tokenCookie, req.session?.user)` and use the returned `authorID` when `accessStatus === 'grant'`.
 - Read cookies from `req.cookies` using `settings.cookie.prefix` (mirrors `ensureAuthorTokenCookie`).
 - Keep the “quiet” behavior: on any failure/deny, return `EMPTY` (don’t throw).
 - Update cache key to incorporate the resolved `authorID` (or the token if you prefer), not `req.session.user.author`.
- src/tests/backend-new/specs/hooks/express/updateStatus.test.ts[1-315]
 - Adjust tests to match the new identity mechanism (e.g., mock `securityManager.checkAccess()` or inject token/sessionID cookies and mock AuthorManager).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Public route in admin spec 🐞 Bug ⚙ Maintainability
Description
The Admin OpenAPI document now includes the public pad endpoint /api/version-status, which
contradicts the document’s stated purpose (admin endpoints) and can confuse codegen/tooling
consumers. The endpoint is called from the pad UI and is not an admin-only route.
Code

src/node/hooks/express/openapi-admin.ts[R48-85]

+    '/api/version-status': {
+      get: {
+        operationId: 'getVersionStatus',
+        summary: 'Outdated-version notice signal for the pad UI',
+        description:
+          'Returns a non-null `outdated` value only to the first author of the supplied pad, ' +
+          'and only when the running server is at least one minor version behind the latest ' +
+          'published release. Result is cached per (padId, authorId) for 60 s.',
+        parameters: [
+          {
+            name: 'padId',
+            in: 'query',
+            required: false,
+            schema: {type: 'string'},
+            description:
+              'Pad whose first-author membership is being checked. ' +
+              'Omitted padId always yields a null result.',
+          },
+        ],
+        responses: {
+          '200': {
+            description: 'Outdated-notice signal.',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  required: ['outdated', 'isFirstAuthor'],
+                  properties: {
+                    outdated: {type: 'string', enum: ['minor'], nullable: true},
+                    isFirstAuthor: {type: 'boolean'},
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
Evidence
The Admin OpenAPI file explicitly claims to document admin endpoints and be distinct from the public
API surface, yet it now includes /api/version-status, which is fetched by the pad UI client code
during pad load.

src/node/hooks/express/openapi-admin.ts[8-16]
src/node/hooks/express/openapi-admin.ts[48-85]
src/static/js/pad_outdated_notice.ts[18-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`openapi-admin.ts` is described as the OpenAPI document for admin endpoints, but it now documents `/api/version-status`, a public endpoint used by the pad UI. This mixes public/non-versioned endpoints into the admin spec and can mislead downstream tooling.

## Issue Context
The pad-side code fetches `/api/version-status` directly during pad load.

## Fix Focus Areas
- src/node/hooks/express/openapi-admin.ts[8-16]
- src/node/hooks/express/openapi-admin.ts[48-85]
 - Either:
   - Move `/api/version-status` to the public OpenAPI document generator (if one exists for non-versioned public endpoints), OR
   - Update the admin spec’s description/title and/or add an explicit note that it intentionally includes select public non-versioned endpoints.
 - Optionally add a small test assertion to prevent future spec-scope drift.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@JohnMcLear JohnMcLear merged commit 29dac6b into develop May 18, 2026
33 checks passed
@JohnMcLear JohnMcLear deleted the fix/7799-outdated-notice-redesign branch May 18, 2026 11:23
Comment on lines +61 to 100
interface OutdatedResponse {
outdated: 'minor' | null;
isFirstAuthor: boolean;
}

const EMPTY: OutdatedResponse = {outdated: null, isFirstAuthor: false};

const TTL_MS = 60 * 1000;
let cache = new LRUCache<string, {value: OutdatedResponse; at: number}>({max: 1000});
const inFlight = new Map<string, Promise<OutdatedResponse>>();

/** Test-only setter: rebuild the LRU with a smaller cap so eviction can be asserted. */
export const _setBadgeCacheCapForTests = (max: number): void => {
cache = new LRUCache<string, {value: OutdatedResponse; at: number}>({max});
};

/** Test-only: clear the in-memory badge cache so integration tests see fresh state. */
export const _resetBadgeCacheForTests = (): void => {
badgeCache = {value: null, at: 0};
badgeInFlight = null;
cache.clear();
inFlight.clear();
};

const computeOutdated = async (
padId: string | null,
authorId: string | null,
): Promise<OutdatedResponse> => {
const state = await loadState(stateFilePath());
if (!state.latest) return EMPTY;
const current = getEpVersion();
if (!isMinorOrMoreBehind(current, state.latest.version)) return EMPTY;
if (!padId || !authorId) return EMPTY;
// padManager is loaded via dynamic import to avoid circular-init w/ updater.
const padManagerMod: any = await import('../../db/PadManager');
const padManager = padManagerMod.default ?? padManagerMod;
if (typeof padManager.isValidPadId !== 'function' || !padManager.isValidPadId(padId)) return EMPTY;
if (!(await padManager.doesPadExist(padId))) return EMPTY;
const pad = await padManager.getPad(padId);
if (firstAuthorOf(pad) !== authorId) return EMPTY;
return {outdated: 'minor', isFirstAuthor: true};
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. vulnerable-below removed without deprecation 📘 Rule violation ☼ Reliability

This PR removes the vulnerable-below directive parsing and the severe/vulnerable outdated
signals without any deprecation-period WARN log, which can break deployments and workflows that
relied on those signals. Per policy, the feature should emit a WARN and remain supported until the
next version before being removed.
Agent Prompt
## Issue description
The PR removes the `vulnerable-below` functionality and the `severe`/`vulnerable` signals immediately, without emitting a deprecation WARN and without a deprecation period.

## Issue Context
- `CHANGELOG.md` explicitly states that `vulnerable-below`, `severe`, and `vulnerable` were removed.
- The `/api/version-status` implementation now only supports `{outdated: 'minor' | null, ...}`.
- The release-notes directive scraping that powered vulnerability signalling has been removed.

## Fix Focus Areas
- src/node/hooks/express/updateStatus.ts[61-100]
- src/node/updater/VersionChecker.ts[14-75]
- src/node/updater/Notifier.ts[13-64]
- src/node/updater/index.ts[159-165]
- doc/admin/updates.md[52-76]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +41 to +59
export const resolveRequestAuthor = async (req: any): Promise<string | null> => {
const readAuthor = (): string | null => {
const a = req?.session?.user?.author;
return typeof a === 'string' && a !== '' ? a : null;
};
const fromSession = readAuthor();
if (fromSession !== null) return fromSession;
try {
const expressModule = await import('../express');
const mw = (expressModule as any).sessionMiddleware;
if (typeof mw !== 'function') return null;
await new Promise<void>((resolve, reject) => {
mw(req, {} as any, (err?: unknown) => (err ? reject(err) : resolve()));
});
} catch {
return null;
}
return readAuthor();
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Author lookup always null 🐞 Bug ≡ Correctness

resolveRequestAuthor() reads req.session.user.author, but Etherpad does not populate an authorId
into the express-session user object for pad visitors, so /api/version-status will return the EMPTY
response and the outdated notice will never show. In Etherpad, the pad author identity is resolved
from the HttpOnly token/sessionID cookies via securityManager.checkAccess(), not from
express-session.
Agent Prompt
## Issue description
`GET /api/version-status` currently derives the requester identity from `req.session.user.author`, but Etherpad’s pad-user authorID is not stored on the express-session `user` object. This causes `authorId` to be `null` in real pad requests, making `computeOutdated()` always return `EMPTY` and preventing the pad-side outdated notice from ever firing.

## Issue Context
Pad authorship is determined via the HttpOnly `${prefix}token` (and optionally `${prefix}sessionID`) cookies and enforced via `securityManager.checkAccess(...)` during the socket.io handshake.

## Fix Focus Areas
- src/node/hooks/express/updateStatus.ts[33-100]
  - Replace `resolveRequestAuthor()` + `req.session.user.author` usage with a cookie-based resolution.
  - Recommended approach: in the route handler or `computeOutdated()`, call `securityManager.checkAccess(padId, sessionIDCookie, tokenCookie, req.session?.user)` and use the returned `authorID` when `accessStatus === 'grant'`.
  - Read cookies from `req.cookies` using `settings.cookie.prefix` (mirrors `ensureAuthorTokenCookie`).
  - Keep the “quiet” behavior: on any failure/deny, return `EMPTY` (don’t throw).
  - Update cache key to incorporate the resolved `authorID` (or the token if you prefer), not `req.session.user.author`.
- src/tests/backend-new/specs/hooks/express/updateStatus.test.ts[1-315]
  - Adjust tests to match the new identity mechanism (e.g., mock `securityManager.checkAccess()` or inject token/sessionID cookies and mock AuthorManager).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@JohnMcLear
Copy link
Copy Markdown
Member Author

Qodo follow-up — addressed in #7805:

  • 🐞 Bug: author lookup always null — confirmed real bug. resolveRequestAuthor read req.session.user.author, which Etherpad never populates for pad visitors. Fixed in fix(pad): outdated notice — resolve author from token cookie (Qodo #7804) #7805 by switching to the same token-cookie path Etherpad's own socket.io handshake uses (authorManager.getAuthorId(token, user)).
  • 📘 Public route in admin spec — clarified the admin spec's info.description to note pad-side endpoints are included for completeness. Larger structural split deferred.
  • ⚠️ Rule violation: vulnerable-below removed without deprecation WARN — intentional. Per Inappropriate update message #7799 the maintainer asked to "drop it"; the directive was an internal release-notes scrape (no plugin or external system consumed it), so the deprecation rule doesn't usefully apply here.

JohnMcLear added a commit that referenced this pull request May 18, 2026
) (#7805)

* fix(updater): resolve pad author from token cookie, not session.user

Etherpad does not populate an authorID into the express-session user
object for pad visitors, so resolveRequestAuthor() always returned null
in production, causing computeOutdated() to return EMPTY and the
pad-side gritter to never fire.

Replace the session-based lookup with a cookie-based path that mirrors
how the socket.io handshake resolves pad-visitor identity: read the
HttpOnly `token` (or `<prefix>token`) cookie and call
authorManager.getAuthorId(token, user) via dynamic import (same
circular-init guard pattern as the PadManager import).

Update the test harness to mock AuthorManager instead of injecting a
fake req.session.user.author, and to set req.cookies.token directly.
All 9 cases continue to pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(openapi): clarify admin spec scope includes pad-side endpoints

/api/version-status is a public pad-side endpoint but lives in the
admin OpenAPI document because it shares the same internal route
registration. Add a note to info.description so downstream tooling
consumers are not misled into treating it as an admin-only route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inappropriate update message

1 participant