Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
26adb99
docs: design spec for #7799 outdated-notice redesign
JohnMcLear May 18, 2026
8abd04a
docs: implementation plan for #7799 outdated-notice redesign
JohnMcLear May 18, 2026
e418343
feat(updater): add isMinorOrMoreBehind, drop major/vulnerable helpers
JohnMcLear May 18, 2026
feb4bfb
refactor(updater): drop vulnerable-below directive and state field
JohnMcLear May 18, 2026
35b02d0
refactor(updater): drop residual EmailSendLog vulnerable fields
JohnMcLear May 18, 2026
161f0d6
feat(updater): add firstAuthorOf helper
JohnMcLear May 18, 2026
1c96590
feat(updater): add resolveRequestAuthor helper for HTTP GET
JohnMcLear May 18, 2026
790350a
feat(updater): pad-aware /api/version-status with first-author gating
JohnMcLear May 18, 2026
6660048
fix(updater): switch isSevere signal from major-only to minor-or-more…
JohnMcLear May 18, 2026
7314aae
test(updater): end-to-end coverage for /api/version-status
JohnMcLear May 18, 2026
f0690c2
docs(openapi): /api/version-status pad-aware shape and gating
JohnMcLear May 18, 2026
c1e8123
chore(pad): remove unused #version-badge template and CSS
JohnMcLear May 18, 2026
e534016
feat(pad): replace persistent badge with first-author outdated gritter
JohnMcLear May 18, 2026
3d2b624
test(pad): playwright coverage for outdated notice gritter
JohnMcLear May 18, 2026
58d3619
docs(pad): outdated-notice redesign + drop vulnerable-below docs
JohnMcLear May 18, 2026
cb3e194
chore(test): remove stale specs for deleted #version-badge surface
JohnMcLear May 18, 2026
718f723
chore: clean stale references to vulnerable/severe in types, emails, …
JohnMcLear May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Notable enhancements

- **pad: Outdated-version notice redesigned (#7799).** The persistent "severely outdated" banner is replaced by a dismissable gritter notification (auto-fades after 8 seconds), shown only to a pad's first author and only when the server is at least one minor version behind the latest released version. Patch-only deltas no longer fire the notice. The `vulnerable-below` directive scraping, the `severe` and `vulnerable` enum values, and the `vulnerableBelow` state field have been removed.
- **API: `GET /api/version-status` updated (#7799).** Now accepts an optional `?padId=<id>` query parameter and returns `{outdated: "minor" | null, isFirstAuthor: boolean}`. The `severe` and `vulnerable` enum values are gone. Results are cached per `(padId, authorId)` for 60 seconds.

- **Self-update — Tier 4 (autonomous in a maintenance window).** Set `updates.tier: "autonomous"` together with `updates.maintenanceWindow: {"start":"HH:MM","end":"HH:MM","tz":"local"|"utc"}` to constrain autonomous updates to a nightly window. The scheduler snaps `scheduledFor` forward to the next window opening when grace would otherwise land outside the window, and defers the fire when the window has closed by the timer callback. Cross-midnight windows (`end < start`) are supported; DST transitions are absorbed by host wall-clock arithmetic. A missing or malformed window degrades the policy to Tier 3 with an explicit `policy.reason` of `maintenance-window-missing` / `maintenance-window-invalid`; an admin banner surfaces the misconfiguration so autonomous behaviour is not silently disabled. The admin update page shows a "Maintenance window" section with the parsed window summary, the next opening, and a "deferred until <iso>" subtitle on the scheduled panel when the timer has been snapped forward. Closes #7607 (#7753).
- **Updater — real SMTP via nodemailer (new top-level `mail.*` block).** Replaces the "(would send email)" stub. New settings: `mail.host`, `mail.port`, `mail.secure`, `mail.from`, `mail.auth.{user,pass}`. `mail.host=null` keeps the legacy log-only behaviour. The `nodemailer` dependency is lazy-imported on first send so installs that don't configure mail pay no runtime cost; the transport is cached on the full SMTP options tuple so a `reloadSettings()` change to host/port/credentials invalidates the cache. `settings.json.docker` reads `MAIL_HOST` / `MAIL_FROM` / `MAIL_PORT` / `MAIL_SECURE` from env. Send errors are logged warn and swallowed so a transient SMTP failure can never poison the updater state machine.
- **Updater — preflight against the target tag's `engines.node`.** Before mutating the working tree, `runPreflight` now runs `git show <tag>:package.json` and verifies `process.versions.node` satisfies the target's `engines.node`. A mismatch fails cleanly at `preflight-failed` with the detail `target requires Node >=X, running Y` — no drain, no restart, no rollback. The check runs *after* signature verification so we only trust signed `package.json`. New `PreflightReason: 'node-engine-mismatch'`.
Expand Down
1 change: 0 additions & 1 deletion admin/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export interface UpdateStatusPayload {
installMethod: string;
tier: string;
policy: null | {canNotify: boolean; canManual: boolean; canAuto: boolean; canAutonomous: boolean; reason: string};
vulnerableBelow: Array<{announcedBy: string; threshold: string}>;
// Tier 2 additions:
execution: Execution;
lastResult: LastResult;
Expand Down
21 changes: 9 additions & 12 deletions doc/admin/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Etherpad ships with a built-in update subsystem.

- **Tier 1 (notify)** — default. A banner appears in the admin UI when a new release is available, and pad users see a discreet badge if the running version is severely outdated or flagged as vulnerable. No execution.
- **Tier 1 (notify)** — default. A banner appears in the admin UI when a new release is available, and pad users see a dismissable gritter notification if the running version is at least one minor version behind the latest release. No execution.
- **Tier 2 (manual click)** — admins on a git install can click "Apply update" at `/admin/update`. Etherpad drains active sessions, runs `git fetch / checkout / pnpm install / pnpm run build:ui`, and exits with code 75 so a process supervisor restarts it on the new version. Auto-rolls back on failure.
- **Tier 3 (auto with grace window)** — opt-in. On a git install, a newly detected release transitions execution state to `scheduled` and is applied after `preApplyGraceMinutes`. During the grace window, `/admin/update` shows a live countdown plus Cancel and Apply now buttons; an admin email (if `adminEmail` is set) fires once per scheduled tag.
- **Tier 4 (autonomous in maintenance window)** — opt-in. Tier 3 + `updates.maintenanceWindow` is required; the scheduler only fires while the wall clock is inside the configured window. Updates detected outside the window queue for the next opening.
Expand Down Expand Up @@ -52,30 +52,27 @@ In `settings.json`:

## What "outdated" means

- **`severe`** — running at least one major version behind the latest release.
- **`vulnerable`** — the running version is below a `vulnerable-below` threshold announced in a recent release. Releases declare these via a `<!-- updater: vulnerable-below X.Y.Z -->` HTML comment in their body. The newest such directive wins.
- **`minor`** — the running server is at least one minor version behind the latest published release. Patch-only deltas (same major and minor, higher patch) do not fire the notice.

## Email cadence (when `adminEmail` is set)

| Trigger | First send | Repeat |
| --- | --- | --- |
| Vulnerable status detected | Immediate | Weekly while still vulnerable |
| New release announced while still vulnerable | Immediate | n/a (one event per tag change) |
| Severely outdated detected | Immediate | Monthly while still severely outdated |
| Outdated (minor or more behind) detected | Immediate | Monthly while still outdated |
| Up to date | No email | — |

If `adminEmail` is unset, the updater never sends mail. The admin UI banner and the pad-side badge still work without it.
If `adminEmail` is unset, the updater never sends mail. The admin UI banner and the pad-side notice still work without it.

PR 1 ships the cadence machinery but does not yet wire a real SMTP transport — emails are logged with `(would send email)` until a future PR adds the transport. The dedupe state still advances correctly so admins are not bombarded once SMTP is wired.

## Pad-side badge
## Pad-side notice

Pad users see no version information by default. A small badge appears in the bottom-right corner only when:
Pad users see no version information by default. A dismissable gritter notification appears only when:

- The instance is `severe` (one or more major versions behind), or
- The instance is `vulnerable` (running below an announced threshold).
- The running server is at least one minor version behind the latest published release (patch-only deltas do not fire), **and**
- The requesting user is the first author of the pad.

The public endpoint `/api/version-status` returns only `{outdated: null|"severe"|"vulnerable"}` — it never leaks the running version, so attackers do not gain a fingerprint vector.
The notice auto-fades after 8 seconds and can be dismissed immediately. The public endpoint `/api/version-status` accepts an optional `?padId=<id>` query parameter and returns `{outdated: "minor" | null, isFirstAuthor: boolean}` — it never leaks the running version, so attackers do not gain a fingerprint vector. Results are cached per `(padId, authorId)` for 60 seconds.

## Disabling everything

Expand Down
28 changes: 28 additions & 0 deletions doc/api/http_api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,31 @@ get stats of the etherpad instance
_Example returns_:

* `{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}`

===== `GET /api/version-status`

Returns an outdated-version signal intended for the pad-side gritter.

*Query parameters:*

[cols="1,1,1,3"]
|===
| name | type | required | description

| `padId`
| string
| no
| Pad whose first-author membership is being checked.
|===

*Response 200 (`application/json`):*

[source,json]
----
{
"outdated": "minor",
"isFirstAuthor": true
}
----

`outdated` is `"minor"` only when the running server is at least one minor version behind the latest published release AND the request resolves to the pad's first author. Otherwise it is `null`. Result is cached per `(padId, authorId)` for 60s. The endpoint is disabled entirely when `updates.tier = 'off'`.
21 changes: 21 additions & 0 deletions doc/api/http_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,3 +764,24 @@ get stats of the etherpad instance
{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}
```

#### `GET /api/version-status`

Returns an outdated-version signal intended for the pad-side gritter.

**Query parameters:**

| name | type | required | description |
| ------- | ------ | -------- | --------------------------------------------------------------------------- |
| `padId` | string | no | Pad whose first-author membership is being checked. |

**Response 200 (`application/json`):**

```json
{
"outdated": "minor",
"isFirstAuthor": true
}
```

`outdated` is `"minor"` only when the running server is at least one minor version behind the latest published release AND the request resolves to the pad's first author. Otherwise it is `null`. Result is cached per `(padId, authorId)` for 60s. The endpoint is disabled entirely when `updates.tier = 'off'`.

Loading
Loading