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
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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9afe9376c4
commit
ba60f70835
+50
-7
@@ -188,12 +188,13 @@ describe("reference deployment compose files", () => {
|
||||
.filter((line) => !line.trim().startsWith("#"))
|
||||
.join("\n");
|
||||
|
||||
// The Dashboard's reserved namespaces (ADR 0057, `DASHBOARD_PAGE_PREFIXES` in
|
||||
// `surfaces/shell/src/dashboard-routes.ts`), each both bare and with everything below it, so a
|
||||
// refreshed deep link reaches the API instead of the proxy's 404.
|
||||
expect(directives).toContain("@ekklesia path / /api/* /display/* /assets/*");
|
||||
// The Dashboard's other pages (ADR 0057): every top-level name shaped like a module id, and
|
||||
// nothing else, so the API — which knows the installed modules — decides page or 404.
|
||||
expect(directives).toContain(
|
||||
"@ekklesia path / /api/* /display/* /assets/* /modules /modules/* /settings /settings/* /account /account/*",
|
||||
`@dashboardPage path_regexp dashboardPage ${DASHBOARD_PAGE_PATTERN}`,
|
||||
);
|
||||
expect(directives).toMatch(/handle @dashboardPage \{\s*reverse_proxy api:3000 \{/u);
|
||||
expect(directives).toContain("header_up X-Forwarded-For {client_ip}");
|
||||
expect(directives).toContain("header_up -CF-Connecting-IP");
|
||||
expect(directives).toContain("trusted_proxies static {$EKKLESIA_TUNNEL_ADDRESS}/32");
|
||||
@@ -205,6 +206,46 @@ describe("reference deployment compose files", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** The proxies' Dashboard-page matcher: a module id's grammar (`@ekklesia/contracts`) as the first
|
||||
* segment, then anything. Checked against real paths below so the regular expression itself is
|
||||
* tested, not just its spelling. */
|
||||
const DASHBOARD_PAGE_PATTERN = "^/[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:/.*)?$";
|
||||
|
||||
describe("the proxies' Dashboard-page matcher", () => {
|
||||
const matcher = new RegExp(DASHBOARD_PAGE_PATTERN, "u");
|
||||
|
||||
it("forwards every Dashboard page and module-shaped name", () => {
|
||||
for (const path of [
|
||||
"/settings",
|
||||
"/settings/displays/add",
|
||||
"/account",
|
||||
"/signage",
|
||||
"/signage/",
|
||||
"/signage/timelines",
|
||||
"/sunday-production",
|
||||
"/definitely-not-a-real-module",
|
||||
]) {
|
||||
expect(matcher.test(path), path).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards nothing that could not be a module id", () => {
|
||||
for (const path of [
|
||||
"/",
|
||||
"/favicon.ico",
|
||||
"/robots.txt",
|
||||
"/.well-known/x",
|
||||
"/Signage",
|
||||
"/-x",
|
||||
"/1x",
|
||||
"//signage",
|
||||
"/a--b",
|
||||
]) {
|
||||
expect(matcher.test(path), path).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("split-origin reference proxy", () => {
|
||||
it("serves the Dashboard's page, its deep links, and its script on the Dashboard hostname, and no API", async () => {
|
||||
const caddyfile = await text("Caddyfile.split.example");
|
||||
@@ -213,9 +254,11 @@ describe("split-origin reference proxy", () => {
|
||||
.filter((line) => !line.trim().startsWith("#"))
|
||||
.join("\n");
|
||||
|
||||
expect(directives).toContain(
|
||||
"@dashboard path / /assets/ekklesia-dashboard/* /modules /modules/* /settings /settings/* /account /account/*",
|
||||
);
|
||||
expect(directives).toContain("@dashboard path / /assets/ekklesia-dashboard/*");
|
||||
expect(directives).toContain(`path_regexp dashboardPage ${DASHBOARD_PAGE_PATTERN}`);
|
||||
// The API's own namespaces stay refused on the Dashboard hostname even though they are shaped
|
||||
// like a module id.
|
||||
expect(directives).toContain("not path /api /api/* /display /display/* /assets /assets/*");
|
||||
// The API hostname still serves no Dashboard page at all.
|
||||
expect(directives).toContain("@api path /api/* /display/* /assets/ekklesia-display/*");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user