feat(deploy): single-origin path-based deployment + project site
Serve the whole stack behind one host: site at /, game UI at /game/, gateway REST at /api + /healthz, Connect at /rpc (prefix stripped by the edge Caddy). The built artifact is domain-agnostic — the UI talks to the gateway same-origin via relative URLs, so the same bundle runs under any host with no rebuild and with CORS disabled. - Rename the Connect proto service galaxy.gateway.v1.EdgeGateway -> edge.v1.Gateway; regenerate Go + TS; public path /rpc/edge.v1.Gateway. - Move the game UI under base path /game (env BASE_PATH); make the manifest, service-worker scope, WASM loader, and all navigation base-aware via a withBase helper. - Relative API + /rpc Connect prefix; Vite dev proxy mirrors the strip. - Rewrite the edge Caddy (dev + prod) for path-based routing; empty CORS allow-lists (same-origin); single host. - New VitePress project site (site/): i18n en/ru with switcher, LaTeX math, minimal monospace theme; built and served at /. - dev-deploy compose/Makefile + CI (dev-deploy, prod-build, new site-build) build and seed the site; probes hit /, /game/, /healthz. - Sync docs (ARCHITECTURE, gateway README/openapi, dev-deploy & local-dev READMEs, CLAUDE.md, ui/PLAN). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ header now — we just hand the routes down as callbacks so the
|
||||
viewer keeps its prop-driven contract.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { getContext } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
@@ -126,10 +127,10 @@ viewer keeps its prop-driven contract.
|
||||
});
|
||||
|
||||
function backToReport() {
|
||||
goto(`/games/${gameId}/report`);
|
||||
goto(withBase(`/games/${gameId}/report`));
|
||||
}
|
||||
function backToMap() {
|
||||
goto(`/games/${gameId}/map`);
|
||||
goto(withBase(`/games/${gameId}/map`));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ fractions is a Phase 21 decision documented in
|
||||
`ui/docs/science-designer-ux.md`.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { getContext, tick } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
@@ -125,7 +126,7 @@ fractions is a Phase 21 decision documented in
|
||||
}
|
||||
|
||||
function backToTable(): void {
|
||||
void goto(`/games/${gameId}/table/sciences`);
|
||||
void goto(withBase(`/games/${gameId}/table/sciences`));
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
|
||||
@@ -20,6 +20,7 @@ Phase 29 wires the wrap-mode toggle on top of the per-game `wrapMode`
|
||||
preference the store already manages.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { getContext, onDestroy, onMount, untrack } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
@@ -636,14 +637,14 @@ preference the store already manages.
|
||||
const gameId = page.params.id ?? "";
|
||||
const turn = store?.report?.turn ?? 0;
|
||||
void goto(
|
||||
`/games/${gameId}/battle/${target.battleId}?turn=${turn}`,
|
||||
withBase(`/games/${gameId}/battle/${target.battleId}?turn=${turn}`),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "bombing": {
|
||||
const gameId = page.params.id ?? "";
|
||||
void goto(
|
||||
`/games/${gameId}/report#report-bombings`,
|
||||
withBase(`/games/${gameId}/report#report-bombings`),
|
||||
).then(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
const row = document.querySelector(
|
||||
|
||||
@@ -20,6 +20,7 @@ The active section is computed by the orchestrator
|
||||
`activeSlug` prop. The TOC itself owns no observers.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||
@@ -62,7 +63,7 @@ The active section is computed by the orchestrator
|
||||
}
|
||||
|
||||
async function backToMap(): Promise<void> {
|
||||
await goto(`/games/${gameId}/map`);
|
||||
await goto(withBase(`/games/${gameId}/map`));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ monospace `<span>`; the rewire here is the one-liner the Phase 23
|
||||
decision log called out.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { getContext } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
|
||||
@@ -47,7 +48,7 @@ decision log called out.
|
||||
</span>
|
||||
<a
|
||||
class="uuid"
|
||||
href={`/games/${gameId}/battle/${b.id}?turn=${turn}`}
|
||||
href={withBase(`/games/${gameId}/battle/${b.id}?turn=${turn}`)}
|
||||
data-testid="report-battle-row"
|
||||
data-id={b.id}
|
||||
>{b.id}</a>
|
||||
|
||||
@@ -17,6 +17,7 @@ The component sits inside the active-view slot owned by
|
||||
data fetching is performed here — the layout is responsible.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { getContext } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
@@ -117,11 +118,11 @@ data fetching is performed here — the layout is responsible.
|
||||
}
|
||||
|
||||
function openDesigner(name: string): void {
|
||||
void goto(`/games/${gameId}/designer/science/${encodeURIComponent(name)}`);
|
||||
void goto(withBase(`/games/${gameId}/designer/science/${encodeURIComponent(name)}`));
|
||||
}
|
||||
|
||||
function newScience(): void {
|
||||
void goto(`/games/${gameId}/designer/science`);
|
||||
void goto(withBase(`/games/${gameId}/designer/science`));
|
||||
}
|
||||
|
||||
async function deleteScience(name: string): Promise<void> {
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
// at the first import.
|
||||
//
|
||||
// `VITE_GATEWAY_BASE_URL` is the base URL of the gateway public REST
|
||||
// surface and the Connect-Web authenticated edge (same host, same
|
||||
// port; the gateway listener serves both). It defaults to the local
|
||||
// dev address used by `tools/local-ci` and the integration suite.
|
||||
// surface. An empty value means "same origin": the single-origin
|
||||
// deployment serves the UI, the REST surface (`/api/...`), and the
|
||||
// authenticated Connect-Web edge (`/rpc/...`) behind one host, so the
|
||||
// browser issues same-origin requests and needs no absolute base. A
|
||||
// non-empty value (the Vite dev proxy, `tools/local-ci`, the
|
||||
// integration suite) points REST at that absolute host instead. The
|
||||
// Connect base is derived from this value by `gatewayRpcBaseUrl`,
|
||||
// which appends the `/rpc` routing prefix.
|
||||
//
|
||||
// `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` is the gateway's response-signing
|
||||
// Ed25519 public key, encoded as standard (non-URL-safe) base64 of
|
||||
@@ -26,6 +31,21 @@ const RAW_RESPONSE_PUBLIC_KEY: string =
|
||||
|
||||
export const GATEWAY_BASE_URL: string = stripTrailingSlash(RAW_BASE_URL);
|
||||
|
||||
/**
|
||||
* gatewayRpcBaseUrl is the base URL for the authenticated Connect-Web
|
||||
* surface. The edge Caddy and the Vite dev proxy route the `/rpc`
|
||||
* prefix to the gateway's Connect listener, stripping it before the
|
||||
* request reaches the proto-derived `edge.v1.Gateway` service path.
|
||||
* When `GATEWAY_BASE_URL` is empty the gateway shares the document
|
||||
* origin, so the origin is resolved at call time from `window`.
|
||||
*/
|
||||
export function gatewayRpcBaseUrl(): string {
|
||||
const origin =
|
||||
GATEWAY_BASE_URL ||
|
||||
(typeof window !== "undefined" ? window.location.origin : "");
|
||||
return `${origin}/rpc`;
|
||||
}
|
||||
|
||||
export const GATEWAY_RESPONSE_PUBLIC_KEY: Uint8Array = decodeBase64(
|
||||
RAW_RESPONSE_PUBLIC_KEY,
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ navigation. Phase 26 introduces the history-mode entry; Phase 35
|
||||
polishes microcopy.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||
@@ -41,7 +42,7 @@ polishes microcopy.
|
||||
|
||||
function go(path: string): void {
|
||||
open = false;
|
||||
void goto(path);
|
||||
void goto(withBase(path));
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent): void {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Base-path helpers for the single-origin deployment. The game UI is
|
||||
// served under `kit.paths.base` — empty at the root for local dev,
|
||||
// vitest, and Playwright, and `/game` in the deployed single-origin
|
||||
// build. SvelteKit does not auto-prefix `goto`, `<a href>`, raw asset
|
||||
// fetches, or the service-worker scope, so every app-internal absolute
|
||||
// path is routed through `withBase`.
|
||||
//
|
||||
// `base` from `$app/paths` is the low-level primitive that `resolve()`
|
||||
// builds on. We use it directly here (rather than `resolve()`) because
|
||||
// the client navigates to and fetches dynamic, runtime-built paths
|
||||
// (command routes, `core.wasm`, the service worker) that `resolve()`'s
|
||||
// statically-typed route-id surface cannot express.
|
||||
import { base } from "$app/paths";
|
||||
|
||||
/** appBase is the configured base path (empty string at the root). */
|
||||
export const appBase = base;
|
||||
|
||||
/**
|
||||
* withBase prefixes an app-internal absolute path (leading slash) with
|
||||
* the configured base path. At the root it returns the path unchanged;
|
||||
* under the single-origin deployment it yields e.g. `/game/lobby`.
|
||||
*/
|
||||
export function withBase(path: string): string {
|
||||
return `${base}${path}`;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ exists; until then the convenience of one source of truth for
|
||||
destinations beats the duplication.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { withBase } from "$lib/paths";
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||
@@ -47,13 +48,13 @@ destinations beats the duplication.
|
||||
async function selectTool(tool: MobileTool): Promise<void> {
|
||||
moreOpen = false;
|
||||
onSelectTool(tool);
|
||||
await goto(`/games/${gameId}/map`);
|
||||
await goto(withBase(`/games/${gameId}/map`));
|
||||
}
|
||||
|
||||
async function go(path: string): Promise<void> {
|
||||
moreOpen = false;
|
||||
onSelectTool("map");
|
||||
await goto(path);
|
||||
await goto(withBase(path));
|
||||
}
|
||||
|
||||
function toggleMore(): void {
|
||||
|
||||
Reference in New Issue
Block a user