diff --git a/src/App.svelte b/src/App.svelte index 98dcf737c..400f12e5f 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,6 +3,7 @@ import * as router from "@app/lib/router"; import * as httpd from "@app/lib/httpd"; + import * as node from "@app/lib/node"; import { unreachable } from "@app/lib/utils"; import Footer from "./App/Footer.svelte"; @@ -30,6 +31,7 @@ const activeRouteStore = router.activeRouteStore; void httpd.initialize().finally(() => void router.loadFromLocation()); + void node.initialize(); if (!window.VITEST && !window.PLAYWRIGHT && import.meta.env.PROD) { const plausible = Plausible({ diff --git a/src/App/Header.svelte b/src/App/Header.svelte index af9db771d..56a1ae134 100644 --- a/src/App/Header.svelte +++ b/src/App/Header.svelte @@ -1,6 +1,10 @@ + +{#if $httpdStore.state === "authenticated"} + + + + + + + + + + Httpd server running + + + radicle.local + + + Browse + + + + + + + + Authenticated as + + + + + { + void httpd.disconnect(); + closeFocused(); + }}> + Disconnect + + + + + +{:else if $httpdStore.state === "running"} + { + modal.show({ + component: ConnectModal, + props: {}, + }); + }} + size="large" + variant="outline"> + + Authenticate + + +{:else} + { + modal.show({ + component: ConnectModal, + props: {}, + }); + }} + size="large" + variant="outline"> + + Authenticate + +{/if} diff --git a/src/App/Header/Connect.svelte b/src/App/Header/Connect.svelte index 534f11167..60b084b8f 100644 --- a/src/App/Header/Connect.svelte +++ b/src/App/Header/Connect.svelte @@ -1,18 +1,12 @@ -{#if $httpdStore.state === "authenticated"} - +{#if $httpdStore.state === "stopped"} + - - - + + Connect - - - Httpd server running - - - radicle.local - - - Browse - + + + + Use the + Radicle CLI + + to connect your device. - - - - - - Authenticated as - - - - - { - void httpd.disconnect(); - closeFocused(); - }}> - Disconnect - + + Run the following command to start the httpd daemon. + - + -{:else if $httpdStore.state === "running"} - { - modal.show({ - component: ConnectModal, - props: {}, - }); - }} - title={buttonTitle[$httpdStore.state]} - size="large" - variant="outline"> - - Read only - - {:else} - { - modal.show({ - component: ConnectModal, - props: {}, - }); - }} - title={buttonTitle[$httpdStore.state]} - size="large" - variant="secondary"> - - Connect + + + Connected {/if} diff --git a/src/App/Header/NodeInfo.svelte b/src/App/Header/NodeInfo.svelte new file mode 100644 index 000000000..d101f25a8 --- /dev/null +++ b/src/App/Header/NodeInfo.svelte @@ -0,0 +1,64 @@ + + + + + + + + {#if running} + Syncing + {:else} + Sync + {/if} + + + + {#if running} + + Use the + Radicle CLI + + to stop your node. + + + {:else} + + Use the + Radicle CLI + + to start your node. + + + {/if} + + diff --git a/src/components/Button.svelte b/src/components/Button.svelte index 8493b03a9..c03deb248 100644 --- a/src/components/Button.svelte +++ b/src/components/Button.svelte @@ -138,8 +138,9 @@ } .primary { - color: var(--color-foreground-match-background); - background-color: var(--color-fill-primary); + color: var(--color-fill-primary); + background-color: var(--color-fill-merged); + border: 1px solid var(--color-border-merged); } .primary[disabled] { @@ -148,7 +149,7 @@ } .primary:not([disabled]):hover { - background-color: var(--color-fill-primary-hover); + border: 1px solid var(--color-fill-primary); } .secondary { diff --git a/src/components/IconSmall.svelte b/src/components/IconSmall.svelte index 4dd2a14a7..647cb54cb 100644 --- a/src/components/IconSmall.svelte +++ b/src/components/IconSmall.svelte @@ -5,6 +5,7 @@ | "arrow-box-up-right" | "arrow-reply" | "branch" + | "broadcasting" | "brush" | "chat" | "checkmark" @@ -18,6 +19,7 @@ | "commit" | "cross" | "delegate" + | "device" | "diff" | "download" | "edit" @@ -28,6 +30,7 @@ | "face" | "file" | "issue" + | "key" | "logo" | "more" | "network" @@ -106,6 +109,17 @@ fill-rule="evenodd" clip-rule="evenodd" d="M8.83331 3.58333L8.83331 2.25L9.83331 2.25L9.83331 3.58333C9.83331 3.85948 9.60946 4.08333 9.33331 4.08333C9.05717 4.08333 8.83331 3.85948 8.83331 3.58333Z" /> + {:else if name === "broadcasting"} + + + + + {:else if name === "chat"} + {:else if name === "device"} + {:else if name === "delegate"} + {:else if name === "key"} + {:else if name === "more"} ("stopped"); +export const nodeStore = derived(store, s => s); + +export const api = new HttpdClient({ + hostname: "127.0.0.1", + port: config.nodes.defaultLocalHttpdPort, + scheme: "http", +}); + +let pollHttpdStateHandle: number | undefined = undefined; + +function update(state: NodeState) { + window.localStorage.setItem(NODE_STATE_STORAGE_KEY, state); + store.set(state); +} + +const stateMutex = withTimeout(new Mutex(), 5_000); + +async function checkState() { + let nodeState: NodeState | null = null; + const rawNodeState = window.localStorage.getItem(NODE_STATE_STORAGE_KEY); + if (rawNodeState === "running" || rawNodeState === "stopped") { + nodeState = rawNodeState; + } + + await stateMutex + .runExclusive(async () => { + try { + if (nodeState) { + update(nodeState); + } + const node = await api.getNode(); + update(node.state); + } catch (error) { + if (error instanceof TypeError) { + console.error(error); + } + update("stopped"); + } + }) + .catch(error => { + if (error !== E_CANCELED && error !== E_TIMEOUT) { + throw error; + } + }); +} + +function pollSession() { + if (pollHttpdStateHandle) { + return; + } + + pollHttpdStateHandle = window.setInterval(() => checkState(), 10_000); +} + +export async function initialize() { + // Sync session state changes with other open tabs and windows. + addEventListener("storage", event => { + if ( + event.key === NODE_STATE_STORAGE_KEY && + event.oldValue !== event.newValue + ) { + void checkState(); + } + }); + + await checkState(); + + // Properly clean up setInterval and restart session polling when Vite + // performs hot module reload on file changes. + if (import.meta.hot) { + import.meta.hot.accept(); + import.meta.hot.dispose(() => { + clearInterval(pollHttpdStateHandle); + pollHttpdStateHandle = undefined; + pollSession(); + }); + } + + pollSession(); +} diff --git a/tests/e2e/httpd.spec.ts b/tests/e2e/httpd.spec.ts index 0ad806f79..9e92b1af4 100644 --- a/tests/e2e/httpd.spec.ts +++ b/tests/e2e/httpd.spec.ts @@ -7,7 +7,7 @@ test("rad web command reacts to port change", async ({ page, peerManager }) => { await peer.startHttpd(8090); await page.goto("/"); - await page.getByRole("button", { name: "Read only" }).click(); + await page.getByRole("button", { name: "Authenticate" }).click(); await expect( page.getByText( diff --git a/tests/support/fixtures.ts b/tests/support/fixtures.ts index bb5231677..e2f57cd54 100644 --- a/tests/support/fixtures.ts +++ b/tests/support/fixtures.ts @@ -169,7 +169,7 @@ export const test = base.extend<{ await peer.startHttpd(); await peer.startNode(); await page.goto("/"); - await page.getByRole("button", { name: "Read only" }).click(); + await page.getByRole("button", { name: "Authenticate" }).click(); await page .locator('input[name="port"]') .fill(peer.httpdBaseUrl.port.toString());