Commit Graph
4 Commits
Author SHA1 Message Date
ekklesiaandClaude Opus 5.5 ba60f70835 feat(shell): make module pages first-class top-level routes
CI / verify (pull_request) Successful in 1m10s
CI / infrastructure-smoke (pull_request) Successful in 43s
CI / package-release (pull_request) Successful in 49s
CI / migration-smoke (pull_request) Successful in 2m12s
CI / image-build (pull_request) Successful in 1m0s
CI / deploy-smoke (same-origin) (pull_request) Successful in 56s
CI / package-publication (pull_request) Successful in 56s
CI / deploy-smoke (split) (pull_request) Successful in 2m37s
Module pages move from `/modules/<id>` to `/<module-id>` (`/signage`),
beside `/settings` and `/account`, so the implementation concept
"module" never appears in a user-facing address. ADR 0057 is rewritten
to match.

- Core reserves the top-level names the platform actually serves today
  (`account`, `api`, `assets`, `display`, `settings`), and discovery
  refuses a module that declares one with `MODULE_ID_RESERVED`. That
  covers startup, `modules:validate`, and package verification. Tests in
  `apps/api` hold the set to every top-level route the API registers and
  every page the shell interprets.
- The API serves the Dashboard page at `/<module-id>` and below only for
  modules discovered with a Dashboard UI, enabled or not: a disabled
  module's bookmark still opens the Dashboard, which says it is
  unavailable. An arbitrary root path such as
  `/definitely-not-a-real-module` stays the sanitized 404.
- Both reference proxies forward every top-level path shaped like a
  module id and let the API decide; the split Dashboard hostname still
  refuses `/api`, `/display`, and `/assets`. Installing a module needs no
  proxy change.
- `/<module-id>/<page>` is already served and forwarded, so a later
  module sub-page contract needs no routing or deployment change; the
  shell shows "Page not found" there until it exists.
- The deploy smoke checks a module's address before install, while
  installed, while disabled, after removal, and after reinstall, plus
  unknown names and the old `/modules/…` form through the real proxy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 08:42:34 -05:00
ekklesiaandClaude Opus 5.5 9afe9376c4 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
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]>
2026-10-14 08:11:02 -05:00
ekklesiaandClaude Opus 5.5 e1ac1da416 fix(deploy): reach storage over HTTP/1.1, and own storage CORS in the proxy
CI / verify (pull_request) Successful in 50s
CI / infrastructure-smoke (pull_request) Successful in 13s
CI / migration-smoke (pull_request) Successful in 1m47s
CI / image-build (pull_request) Successful in 18s
CI / deploy-smoke (same-origin) (pull_request) Successful in 47s
CI / deploy-smoke (split) (pull_request) Successful in 1m2s
Every reload of a cached files. image failed in the browser with
ERR_HTTP2_PROTOCOL_ERROR. A browser revalidates a cached signed URL
with If-None-Match. Caddy reached SeaweedFS over HTTP/2, and there
SeaweedFS answers with a 304 declaring content-type: application/xml
and content-length: 327 while sending no body. Caddy then logged
"aborting with incomplete response ... unexpected EOF" and reset the
stream. Over HTTP/1.1 SeaweedFS's 304 is clean. Measured layer by layer:
DevTools, curl through Caddy, and curl direct over HTTP/1.1 and HTTP/2.

The storage upstream is now HTTP/1.1 only (`versions 1.1`). Browsers
still reach the proxy over HTTP/2 and HTTP/3.

Delivery-layer CORS is the deployment's, so it no longer depends on
which S3 backend is behind it. SeaweedFS's default reflected any origin
and advertised PUT, POST and DELETE. Now the proxy:
- grants CORS only to the origins that read objects with fetch(): the
  Ekklesia origin, or, when split, the API origin (displays) and the
  Dashboard origin (a module's preview caches images through fetch());
- allows GET and HEAD only, with Vary: Origin;
- answers preflights itself, so they never reach storage;
- strips the object store's own CORS headers.

The deploy README documents the policy, its reasons, and what a
self-run proxy must do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 01:19:17 +00:00
ekklesiaandClaude Opus 5.5 f63f80f2f0 feat: support split Dashboard and API origins (ADR 0045)
CI / verify (pull_request) Successful in 49s
CI / infrastructure-smoke (pull_request) Successful in 13s
CI / migration-smoke (pull_request) Successful in 1m47s
CI / image-build (pull_request) Successful in 19s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m4s
An installation can now serve the Dashboard and the API on sibling
hostnames (dash./api.) beside the default same-origin topology, from one
image and with configuration alone.

- The Dashboard learns its API origin at run time: the built page carries
  one empty <meta name="ekklesia-api-origin">, which the API fills with its
  identity base URL's origin at startup, before computing the page's ETag.
  The shell accepts only a canonical http(s) origin and otherwise fails
  closed without making a request. The build-time EKKLESIA_API_BASE_URL is
  gone from the bundle; the development server injects the same element.
- The display host stays on the API's origin, so its device cookies,
  requests, and offline cache are unchanged.
- The API trusts its own origin (trusted origins plus the identity base
  URL), as Better Auth already does, so the display page's mutations pass
  the origin check without operators listing it.
- Doctor validates EKKLESIA_DASHBOARD_ORIGIN exactly as the API's startup
  does (it previously skipped it), reports the topology without printing
  hostnames, and warns when split hostnames are not siblings.
- The module UI contract states the CORS header limit (safelisted headers
  plus Content-Type, safelisted response headers only).
- Reference deployment: Caddyfile.split.example (Dashboard hostname serves
  / and its script; API hostname serves /api/*, /display/*, and the
  display script; each refuses the other's paths) and
  EKKLESIA_DASHBOARD_HOSTNAME in the proxy overlay and env examples.
- The deploy smoke test runs either topology (CI runs both). The split run
  installs the module template as a test-only module, checks per-hostname
  routing and exact CORS, then drives a digest-pinned headless Chromium
  over the DevTools protocol, with no added dependency: sign-in on the
  Dashboard hostname, the session cookie host-only on the API hostname,
  the module UI imported cross-origin and its credentialed request
  succeeding, the display and its service worker on the API hostname, and
  sign-out.

ADR 0045 is marked Accepted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 14:47:36 -05:00