feat(shell): give every Dashboard page a real URL
CI / verify (pull_request) Successful in 1m7s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 49s
CI / migration-smoke (pull_request) Successful in 2m17s
CI / image-build (pull_request) Successful in 1m10s
CI / deploy-smoke (same-origin) (pull_request) Successful in 45s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 2m37s
CI / verify (pull_request) Successful in 1m7s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 49s
CI / migration-smoke (pull_request) Successful in 2m17s
CI / image-build (pull_request) Successful in 1m10s
CI / deploy-smoke (same-origin) (pull_request) Successful in 45s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 2m37s
The Dashboard had one address. The API served the page only at `/`, both reference proxies forwarded only `/`, and the open module lived in React state, so a refresh went back to the first module, nothing could be bookmarked, and Back left the Dashboard (DESIGN.md §2.7). ADR 0057 makes the address bar the source of truth: - `/` is a host-owned Overview, now the landing page; `/modules/<id>` mounts that module. An id the server's navigation does not list shows "This page isn't available" and loads nothing; any other Dashboard address is a host-owned "Page not found". - `/modules`, `/settings`, and `/account` are reserved whole, from one list (`@ekklesia/shell/dashboard-routes`) that the shell's router and the API's page routes both read. The API serves the same frozen page, ETag, and security headers anywhere inside them; look-alikes and other methods stay the sanitized 404. - A small host-owned history replaces the selected-module state, built in the entry point so it stays the only file touching `window`. Navigation items are real links; modified clicks are left to the browser. Focus moves to the page after an in-place change, and the tab is named after the page. - Sign-in still happens in place with no return value (ADR 0034), so a deep link opens its own page after sign-in. - Both reference Caddyfiles forward the new paths; the deploy smoke now reloads a module's address through the real proxy and requires it to mount again. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
44430b5d59
commit
9afe9376c4
@@ -41,9 +41,11 @@
|
||||
}
|
||||
|
||||
{$EKKLESIA_HOSTNAME} {
|
||||
# Everything the API serves: the Dashboard at /, the API, display pages and their service
|
||||
# worker, and the Dashboard and display host scripts. Any other path is refused here.
|
||||
@ekklesia path / /api/* /display/* /assets/*
|
||||
# Everything the API serves: the Dashboard at / and at its own pages (/modules, /settings, and
|
||||
# /account, each with everything below it, so a refreshed deep link stays put), the API, display
|
||||
# pages and their service worker, and the Dashboard and display host scripts. Any other path is
|
||||
# refused here.
|
||||
@ekklesia path / /api/* /display/* /assets/* /modules /modules/* /settings /settings/* /account /account/*
|
||||
|
||||
# Module realtime is Server-Sent Events: flush every event immediately.
|
||||
@realtime path /api/v1/module-realtime/*
|
||||
|
||||
@@ -45,11 +45,13 @@
|
||||
header_up -True-Client-IP
|
||||
}
|
||||
|
||||
# The Dashboard: its page and its one script, nothing else. Emailed verification and reset links land
|
||||
# on `/` with a query. `/api/*` is refused here on purpose, so the session cookie has exactly one
|
||||
# home, the API hostname, and the Dashboard can never fall back to a same-origin API.
|
||||
# The Dashboard: its page — at `/` and at every Dashboard page (/modules, /settings, and /account,
|
||||
# each with everything below it), so a refreshed deep link stays put — and its one script, nothing
|
||||
# else. Emailed verification and reset links land on `/` with a query. `/api/*` is refused here on
|
||||
# purpose, so the session cookie has exactly one home, the API hostname, and the Dashboard can never
|
||||
# fall back to a same-origin API.
|
||||
{$EKKLESIA_DASHBOARD_HOSTNAME} {
|
||||
@dashboard path / /assets/ekklesia-dashboard/*
|
||||
@dashboard path / /assets/ekklesia-dashboard/* /modules /modules/* /settings /settings/* /account /account/*
|
||||
|
||||
handle @dashboard {
|
||||
reverse_proxy api:3000 {
|
||||
|
||||
+12
-3
@@ -241,6 +241,12 @@ Module storage is installation state: back it up with the database and the bucke
|
||||
`docker compose run --rm migrate node core/dist/cli/apply-migrations.js`.
|
||||
5. **Restart on the new image:** `docker compose -f compose.yaml -f compose.proxy.yaml up -d --wait`.
|
||||
|
||||
**Upgrading to a release with Dashboard page addresses (ADR 0057):** compare your `Caddyfile` with
|
||||
the new `Caddyfile.example` (or `Caddyfile.split.example`). The Dashboard's matcher gains `/modules`,
|
||||
`/settings`, `/account` and everything below each; without them the Dashboard still opens at `/`,
|
||||
but refreshing or bookmarking any other Dashboard page is the proxy's `404`. Reload the proxy after
|
||||
editing it.
|
||||
|
||||
### Rolling back
|
||||
|
||||
Set `EKKLESIA_IMAGE` back to the previous tag and run `up -d --wait` again. That is enough only when
|
||||
@@ -263,7 +269,7 @@ The same image serves both; only configuration differs.
|
||||
|
||||
| Hostname | Serves | Refuses (`404`) |
|
||||
| --- | --- | --- |
|
||||
| Dashboard (`EKKLESIA_DASHBOARD_HOSTNAME`) | `/` and `/assets/ekklesia-dashboard/*` | everything else, including `/api/*` |
|
||||
| Dashboard (`EKKLESIA_DASHBOARD_HOSTNAME`) | `/`, the Dashboard's pages (`/modules`, `/settings`, `/account`, each with everything below it), and `/assets/ekklesia-dashboard/*` | everything else, including `/api/*` |
|
||||
| API (`EKKLESIA_HOSTNAME`) | `/api/*`, `/display/*`, `/assets/ekklesia-display/*` | everything else, including `/` |
|
||||
| Storage (`EKKLESIA_STORAGE_HOSTNAME`) | signed object URLs, unchanged | |
|
||||
|
||||
@@ -339,7 +345,10 @@ networks:
|
||||
|
||||
Then make your proxy do what `Caddyfile.example` does:
|
||||
|
||||
- forward `/`, `/api/*`, `/display/*`, and `/assets/*` on the Ekklesia hostname to `api:3000`;
|
||||
- forward `/`, `/api/*`, `/display/*`, and `/assets/*` on the Ekklesia hostname to `api:3000`,
|
||||
and the Dashboard's pages: `/modules`, `/settings`, and `/account`, each bare and with everything
|
||||
below it (ADR 0057) — without them, refreshing any Dashboard page other than `/` is your proxy's
|
||||
`404`;
|
||||
- **overwrite** `X-Forwarded-For` with the one client address it resolved, and strip
|
||||
`CF-Connecting-IP`, `X-Real-IP`, `Forwarded`, and `True-Client-IP`;
|
||||
- stream responses on `/api/v1/module-realtime/*` without buffering (Server-Sent Events);
|
||||
@@ -405,7 +414,7 @@ deployment:
|
||||
|
||||
| # | Item | Where it is met |
|
||||
| --- | --- | --- |
|
||||
| 1 | The proxy forwards the Dashboard and `/api` on the intended origin | `Caddyfile.example` routes `/`, `/api/*`, `/display/*`, `/assets/*` on one hostname to `api`; the API serves the Dashboard itself. In the split topology, `Caddyfile.split.example` gives each hostname exactly its own paths and the exact trusted-origin CORS of ADR 0045 covers the rest. |
|
||||
| 1 | The proxy forwards the Dashboard and `/api` on the intended origin | `Caddyfile.example` routes `/`, the Dashboard's pages (`/modules`, `/settings`, `/account` and below), `/api/*`, `/display/*`, `/assets/*` on one hostname to `api`; the API serves the Dashboard itself. In the split topology, `Caddyfile.split.example` gives each hostname exactly its own paths and the exact trusted-origin CORS of ADR 0045 covers the rest. |
|
||||
| 2 | The origin port is unreachable from the Internet | The base publishes no port; only `proxy` publishes 80 and 443. |
|
||||
| 3 | The origin port is unreachable from untrusted networks | `api` is reachable only on Docker networks; `EKKLESIA_PUBLISH_ADDRESS` limits the proxy to one interface. Firewall the host itself. |
|
||||
| 4 | The proxy overwrites the trusted client-IP header | `header_up X-Forwarded-For {client_ip}` in `Caddyfile.example`. |
|
||||
|
||||
Reference in New Issue
Block a user