9cb5097f54
Tests · UI / test (push) Has been cancelled
Build · Site / build (push) Successful in 8s
Tests · Integration / integration (pull_request) Successful in 1m42s
Build · Site / build (pull_request) Successful in 6s
Tests · UI / test (pull_request) Successful in 2m23s
Tests · Go / test (pull_request) Successful in 1m56s
- The app root ("/", i.e. /game/) rendered a dev "workspace skeleton"
stub, and the layout guard only redirected anonymous users off it, so
an authenticated visitor stayed on the stub. Redirect "/" to /lobby
(authenticated) and /login (anonymous), and replace the stub with a
minimal loading placeholder. Drop the obsolete landing-stub unit test
(root redirect is covered by the auth-flow e2e).
- Ship a tombstone /service-worker.js on the project site so any old
root-scoped PWA worker (from when the game lived at the origin root)
unregisters itself instead of serving a stale cached page at the
site origin. The game now registers its worker only under /game/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
3.6 KiB
Svelte
123 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import "$lib/theme/tokens.css";
|
|
import "$lib/theme/base.css";
|
|
import { onMount } from "svelte";
|
|
import { goto } from "$app/navigation";
|
|
import { page } from "$app/state";
|
|
import { dev } from "$app/environment";
|
|
import { appBase, withBase } from "$lib/paths";
|
|
import { i18n } from "$lib/i18n/index.svelte";
|
|
import { session } from "$lib/session-store.svelte";
|
|
import { eventStream } from "../api/events.svelte";
|
|
import { loadCore } from "../platform/core/index";
|
|
import { GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
|
|
import ToastHost from "$lib/toast-host.svelte";
|
|
|
|
let { children } = $props();
|
|
|
|
// `streamSessionId` records the device session id the event stream
|
|
// is currently bound to. The `$effect` below uses it to detect a
|
|
// re-login (different session id while still authenticated) and
|
|
// restart the stream against the fresh credentials.
|
|
let streamSessionId: string | null = null;
|
|
|
|
onMount(() => {
|
|
void session.init();
|
|
// Production-only service-worker registration (auto-register is off
|
|
// in svelte.config.js) so `vite dev` and the dev-server e2e suite
|
|
// run without the worker intercepting requests.
|
|
if (!dev && "serviceWorker" in navigator) {
|
|
void navigator.serviceWorker.register(withBase("/service-worker.js"), {
|
|
scope: withBase("/"),
|
|
});
|
|
}
|
|
return () => {
|
|
eventStream.stop();
|
|
streamSessionId = null;
|
|
};
|
|
});
|
|
|
|
$effect(() => {
|
|
if (
|
|
session.status === "authenticated" &&
|
|
session.keypair !== null &&
|
|
session.deviceSessionId !== null &&
|
|
GATEWAY_RESPONSE_PUBLIC_KEY.length > 0
|
|
) {
|
|
const keypair = session.keypair;
|
|
const deviceSessionId = session.deviceSessionId;
|
|
if (streamSessionId !== deviceSessionId) {
|
|
if (streamSessionId !== null) {
|
|
eventStream.stop();
|
|
}
|
|
streamSessionId = deviceSessionId;
|
|
void (async (): Promise<void> => {
|
|
try {
|
|
const core = await loadCore();
|
|
// Bail out if the session flipped away from this id
|
|
// while we were loading core (logout, re-login).
|
|
if (
|
|
session.deviceSessionId !== deviceSessionId ||
|
|
session.status !== "authenticated"
|
|
) {
|
|
return;
|
|
}
|
|
eventStream.start({
|
|
core,
|
|
keypair,
|
|
deviceSessionId,
|
|
gatewayResponsePublicKey: GATEWAY_RESPONSE_PUBLIC_KEY,
|
|
});
|
|
} catch (err) {
|
|
console.info("layout: failed to start event stream", err);
|
|
}
|
|
})();
|
|
}
|
|
} else if (streamSessionId !== null) {
|
|
eventStream.stop();
|
|
streamSessionId = null;
|
|
}
|
|
|
|
// page.url.pathname includes the configured base path; strip it so
|
|
// the route comparisons below stay base-agnostic.
|
|
const pathname = page.url.pathname.slice(appBase.length);
|
|
// Debug-only routes under /__debug/* run their own bootstrap
|
|
// path against the storage primitives and must bypass the
|
|
// auth guard so Phase 6's Playwright spec can drive the
|
|
// keystore directly.
|
|
if (pathname.startsWith("/__debug/")) {
|
|
return;
|
|
}
|
|
if (session.status === "anonymous" && pathname !== "/login") {
|
|
void goto(withBase("/login"), { replaceState: true });
|
|
} else if (
|
|
session.status === "authenticated" &&
|
|
(pathname === "/login" || pathname === "/")
|
|
) {
|
|
void goto(withBase("/lobby"), { replaceState: true });
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if session.status === "loading"}
|
|
<main class="status">
|
|
<p>{i18n.t("common.loading")}</p>
|
|
</main>
|
|
{:else if session.status === "unsupported"}
|
|
<main class="status">
|
|
<h1>{i18n.t("common.browser_not_supported_title")}</h1>
|
|
<p>{i18n.t("common.browser_not_supported_body")}</p>
|
|
</main>
|
|
{:else}
|
|
{@render children()}
|
|
{/if}
|
|
|
|
<ToastHost />
|
|
|
|
<style>
|
|
.status {
|
|
padding: var(--space-6);
|
|
font-family: var(--font-sans);
|
|
}
|
|
</style>
|