diff --git a/docs/prs/2026-09-03-PR#17-maintenance-page.md b/docs/prs/2026-09-03-PR#17-maintenance-page.md new file mode 100644 index 0000000..961f423 --- /dev/null +++ b/docs/prs/2026-09-03-PR#17-maintenance-page.md @@ -0,0 +1,86 @@ +> **WARNING:** This document describes only the change applied in this PR. +> It may already be outdated once the next PR is merged. +> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation. + +## The Problem + +`instance-update` restarts the systemd unit: for a few seconds nothing listens on Werkator's port at all. +On the Hostsharing Managed Webspace deployment (see [deployment.md](../deployment.md#hostsharing-managed-webspace)) the platform's Apache sits in front and proxies via `.htaccess` (`SystemdServiceFiles.htaccessContent`); with the backend refusing connections, Apache answers with its own bare 502 error page, or the request just hangs until it times out. +Either way the instance looks dead rather than "back in a moment", which is confusing during a deploy that is otherwise routine and fast (the restart itself is under a second; the rest of `instance-update`'s time is upload). + +## Non-Goals + +- A live progress indicator ("2 of 10 seconds left") — the maintenance page is a static file with no way to know how far the restart has gotten. +- Zero-downtime / blue-green deployment — the restart gap itself is not eliminated, only made to look intentional instead of broken. +- The Docker-host and bwrap-webspace-without-a-domain deployment variants, which do not go through Apache/`.htaccess` at all. + +## The Scenarios + +### Feature: a refused connection shows a maintenance page instead of a bare error + +#### Background + +- `init --systemd` writes the `.htaccess` only when `server.publicBaseUrl` is set — unchanged; the maintenance page is generated under the same condition, next to it. +- The generated `.htaccess` already proxies every request to `http://127.0.0.1:` via `mod_rewrite [P]`; a refused connection surfaces as Apache's own 502 (and, depending on Apache's timeout handling, 503/504). + +#### Scenario#17.01: A refused backend connection serves the maintenance page + +So that a deployment's restart window reads "please retry" instead of a bare error or a hang. + +- **Given** the generated `.htaccess` and `werkator-maintenance.html` are in a domain's docroot +- **When** the proxied backend refuses the connection (Apache's 502/503/504) +- **Then** Apache serves `werkator-maintenance.html` instead of its default error page + +##### Verified by + +- [SystemdServiceFilesTest — "the htaccess maps a refused connection to the static maintenance page"](../../src/test/kotlin/de/hoennig/werkator/commands/SystemdServiceFilesTest.kt) +- Manual, pending: an `instance-update` deploy on `mih09` watched live during the restart window, to confirm Apache actually reaches 502 there rather than timing out first (see Open Questions). + +#### Scenario#17.02: The maintenance page itself is never proxied + +So that `ErrorDocument`'s internal sub-request for the page does not loop back into the same rewrite rule (which would proxy it to the — still down — backend and fail again). + +- **Given** the generated `.htaccess` +- **When** Apache serves `werkator-maintenance.html` as an `ErrorDocument` +- **Then** the `RewriteCond` excludes exactly that path from the proxy `RewriteRule` + +##### Verified by + +- [SystemdServiceFilesTest — "the htaccess maps a refused connection to the static maintenance page"](../../src/test/kotlin/de/hoennig/werkator/commands/SystemdServiceFilesTest.kt) + +#### Scenario#17.03: A host without a configured public base URL is unaffected + +So that a developer machine or a Docker-host deployment, which never went through `.htaccess`, sees no new file and no behavior change. + +- **Given** `server.publicBaseUrl` is blank +- **When** `init --systemd` runs +- **Then** neither `.htaccess` nor `werkator-maintenance.html` is written — unchanged from before this PR + +##### Verified by + +- Existing `InitCommand` behavior (`server.publicBaseUrl.isNotBlank()` gate); not separately tested, as before this PR. + +## The Solution + +`SystemdServiceFiles.htaccessContent` gains three `ErrorDocument` lines (502/503/504) pointing at a new static `werkator-maintenance.html`, plus one `RewriteCond` excluding that file from the catch-all proxy rule. +`SystemdServiceFiles.maintenancePageContent()` is a small, self-contained HTML page ("Werkator is restarting — please retry in a few minutes") — no external CSS/fonts/images, since nothing would be there to serve them while Werkator itself is the thing that is down. +`InitCommand.createSystemdFiles` writes it under the same `server.publicBaseUrl.isNotBlank()` gate as the `.htaccess`, next to it in `.git/werkator/`. +`tools/remote instance-start` copies both files into the domain docroot in the same step (it already copied the `.htaccess` there; the maintenance page rides along). + +No config key was added: like the `.htaccess` itself, the maintenance page is generated whenever a `publicBaseUrl` is configured, not behind a separate toggle nobody is expected to turn off. + +## Open Questions + +- **Does Apache actually reach 502, or does the client just time out first?** Depends on Apache's `ProxyTimeout`/connect-timeout configuration on the managed webspace, which this PR does not change or configure — where Apache's own request timeout fires before the backend's refused-connection error would, the client still sees a hang, not the maintenance page, for that fraction of the restart window. Not addressed here; a follow-up could set an explicit short `ProxyTimeout` if this turns out to matter in practice. + +## Additional Changes + +- None beyond the feature itself. + +## Prerequisite PRs + +- None; builds on the existing `.htaccess` generation (`SystemdServiceFiles`, `InitCommand`), unchanged in shape. + +## Follow-up PRs + +- An explicit Apache `ProxyTimeout` if the client-side hang (see Open Questions) turns out to matter in practice.