Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Show node state indicator
Sebastian Martinez committed 2 years ago
commit 84addf467dc3114a40da887fa37c93dc6bfe1264
parent 94985d0489a77356d6e731a50e8b3d6668cf46f2
11 files changed +392 -143
modified 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({
modified src/App/Header.svelte
@@ -1,6 +1,10 @@
<script lang="ts">
  import Connect from "@app/App/Header/Connect.svelte";
  import Link from "@app/components/Link.svelte";
+
  import NodeInfo from "@app/App/Header/NodeInfo.svelte";
+
  import Authenticate from "./Header/Authenticate.svelte";
+
  import { httpdStore } from "@app/lib/httpd";
+
  import { nodeStore } from "@app/lib/node";
</script>

<style>
@@ -44,9 +48,13 @@
    </Link>
  </div>

-
  <div class="right">
-
    <div class="layout-desktop">
-
      <Connect />
-
    </div>
+
  <div class="right layout-desktop-flex">
+
    {#if $httpdStore.state !== "stopped"}
+
      {#if $nodeStore}
+
        <NodeInfo running={$nodeStore === "running"} />
+
      {/if}
+
      <Authenticate />
+
    {/if}
+
    <Connect />
  </div>
</header>
added src/App/Header/Authenticate.svelte
@@ -0,0 +1,162 @@
+
<script lang="ts">
+
  import * as httpd from "@app/lib/httpd";
+
  import * as modal from "@app/lib/modal";
+
  import { closeFocused } from "@app/components/Popover.svelte";
+
  import { httpdStore } from "@app/lib/httpd";
+

+
  import Button from "@app/components/Button.svelte";
+
  import ConnectModal from "@app/modals/ConnectModal.svelte";
+
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1.5rem;
+
  }
+
  .host {
+
    display: flex;
+
    justify-content: space-between;
+
    align-items: center;
+
    font-size: var(--font-size-small);
+
  }
+
  .status {
+
    font-size: var(--font-size-tiny);
+
    color: var(--color-fill-gray);
+
    text-align: left;
+
  }
+
  .separator {
+
    height: 1px;
+
    background-color: var(--color-border-hint);
+
  }
+
  .avatar {
+
    height: 1.5rem;
+
    color: var(--color-fill-primary) !important;
+
    display: flex;
+
    gap: 0.5rem;
+
    align-items: center;
+
    justify-content: center;
+
    font-weight: var(--font-weight-regular);
+
    font-family: var(--font-family-monospace);
+
  }
+
  .indicator {
+
    width: 0.75rem;
+
    height: 0.75rem;
+
    background-color: var(--color-fill-secondary);
+
    border-radius: var(--border-radius-round);
+
    position: absolute;
+
    top: -0.375rem;
+
    right: -0.375rem;
+
  }
+
  .row {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
  .user {
+
    display: flex;
+
    justify-content: space-between;
+
    align-items: center;
+
  }
+
  .identity {
+
    color: var(--color-fill-secondary);
+
    display: flex;
+
  }
+
</style>
+

+
{#if $httpdStore.state === "authenticated"}
+
  <Popover
+
    popoverPositionTop="3rem"
+
    popoverPositionRight="-10rem"
+
    popoverPositionLeft="0">
+
    <Button
+
      slot="toggle"
+
      let:toggle
+
      on:click={toggle}
+
      size="large"
+
      variant="primary">
+
      <div class="avatar">
+
        <NodeId
+
          large
+
          disableTooltip
+
          styleColor="var(--color-fill-primary)"
+
          nodeId={$httpdStore.session.publicKey}
+
          alias={$httpdStore.session.alias} />
+
      </div>
+
    </Button>
+

+
    <div slot="popover" class="container">
+
      <div class="row">
+
        <div class="status">Httpd server running</div>
+

+
        <div class="host">
+
          radicle.local
+

+
          <Link
+
            on:afterNavigate={closeFocused}
+
            route={{
+
              resource: "nodes",
+
              params: {
+
                baseUrl: httpd.api.baseUrl,
+
                projectPageIndex: 0,
+
              },
+
            }}>
+
            <IconButton>Browse</IconButton>
+
          </Link>
+
        </div>
+
      </div>
+

+
      <div class="separator" />
+

+
      <div class="row">
+
        <div class="status">Authenticated as</div>
+
        <div class="user">
+
          <div class="identity">
+
            <NodeId
+
              nodeId={$httpdStore.session.publicKey}
+
              alias={$httpdStore.session.alias} />
+
          </div>
+
          <IconButton
+
            on:click={() => {
+
              void httpd.disconnect();
+
              closeFocused();
+
            }}>
+
            Disconnect
+
          </IconButton>
+
        </div>
+
      </div>
+
    </div>
+
  </Popover>
+
{:else if $httpdStore.state === "running"}
+
  <Button
+
    on:click={() => {
+
      modal.show({
+
        component: ConnectModal,
+
        props: {},
+
      });
+
    }}
+
    size="large"
+
    variant="outline">
+
    <IconSmall name="key" />
+
    Authenticate
+
    <div class="indicator" />
+
  </Button>
+
{:else}
+
  <Button
+
    on:click={() => {
+
      modal.show({
+
        component: ConnectModal,
+
        props: {},
+
      });
+
    }}
+
    size="large"
+
    variant="outline">
+
    <IconSmall name="chat" />
+
    Authenticate
+
  </Button>
+
{/if}
modified src/App/Header/Connect.svelte
@@ -1,18 +1,12 @@
<script lang="ts">
  import type { HttpdState } from "@app/lib/httpd";

-
  import * as httpd from "@app/lib/httpd";
-
  import * as modal from "@app/lib/modal";
-
  import { closeFocused } from "@app/components/Popover.svelte";
  import { httpdStore } from "@app/lib/httpd";

  import Button from "@app/components/Button.svelte";
-
  import ConnectModal from "@app/modals/ConnectModal.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import IconButton from "@app/components/IconButton.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
  import Popover from "@app/components/Popover.svelte";
+
  import Command from "@app/components/Command.svelte";

  const buttonTitle: Record<HttpdState["state"], string> = {
    stopped: "radicle-httpd is stopped",
@@ -22,64 +16,19 @@
</script>

<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 1.5rem;
-
    width: "23rem";
-
  }
-
  .host {
-
    display: flex;
-
    justify-content: space-between;
-
    align-items: center;
+
  .label {
+
    display: block;
    font-size: var(--font-size-small);
-
  }
-
  .status {
-
    font-size: var(--font-size-tiny);
-
    color: var(--color-fill-gray);
-
    text-align: left;
-
  }
-
  .separator {
-
    height: 1px;
-
    background-color: var(--color-border-hint);
-
  }
-
  .avatar {
-
    height: 1.5rem;
-
    color: var(--color-fill-secondary);
-
    display: flex;
-
    gap: 0.5rem;
-
    align-items: center;
-
    justify-content: center;
    font-weight: var(--font-weight-regular);
-
    font-family: var(--font-family-monospace);
-
  }
-
  .indicator {
-
    width: 0.75rem;
-
    height: 0.75rem;
-
    background-color: var(--color-fill-secondary);
-
    border-radius: var(--border-radius-round);
-
    position: absolute;
-
    top: -0.375rem;
-
    right: -0.375rem;
-
  }
-
  .row {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0.5rem;
-
  }
-
  .user {
-
    display: flex;
-
    justify-content: space-between;
-
    align-items: center;
-
  }
-
  .identity {
-
    color: var(--color-fill-secondary);
-
    display: flex;
+
    margin-bottom: 0.75rem;
  }
</style>

-
{#if $httpdStore.state === "authenticated"}
-
  <Popover popoverPositionTop="3rem" popoverPositionRight="0">
+
{#if $httpdStore.state === "stopped"}
+
  <Popover
+
    popoverPositionTop="3rem"
+
    popoverPositionRight="0"
+
    popoverPositionLeft="-13rem">
    <Button
      slot="toggle"
      let:toggle
@@ -87,84 +36,32 @@
      title={buttonTitle[$httpdStore.state]}
      size="large"
      variant="outline">
-
      <div class="avatar">
-
        <NodeId
-
          large
-
          disableTooltip
-
          nodeId={$httpdStore.session.publicKey}
-
          alias={$httpdStore.session.alias} />
-
      </div>
+
      <IconSmall name="device" />
+
      Connect
    </Button>

-
    <div slot="popover" class="container">
-
      <div class="row">
-
        <div class="status">Httpd server running</div>
-

-
        <div class="host">
-
          radicle.local
-

-
          <Link
-
            on:afterNavigate={closeFocused}
-
            route={{
-
              resource: "nodes",
-
              params: {
-
                baseUrl: httpd.api.baseUrl,
-
                projectPageIndex: 0,
-
              },
-
            }}>
-
            <IconButton>Browse</IconButton>
-
          </Link>
+
    <svelte:fragment slot="popover">
+
      <div>
+
        <div class="label">
+
          Use the <a
+
            target="_blank"
+
            rel="noreferrer"
+
            href="https://radicle.xyz/#try"
+
            class="txt-link txt-bold">
+
            Radicle CLI
+
          </a>
+
          to connect your device.
        </div>
-
      </div>
-

-
      <div class="separator" />
-

-
      <div class="row">
-
        <div class="status">Authenticated as</div>
-
        <div class="user">
-
          <div class="identity">
-
            <NodeId
-
              nodeId={$httpdStore.session.publicKey}
-
              alias={$httpdStore.session.alias} />
-
          </div>
-
          <IconButton
-
            on:click={() => {
-
              void httpd.disconnect();
-
              closeFocused();
-
            }}>
-
            Disconnect
-
          </IconButton>
+
        <div class="label">
+
          Run the following command to start the httpd daemon.
        </div>
+
        <Command command="radicle-httpd" fullWidth />
      </div>
-
    </div>
+
    </svelte:fragment>
  </Popover>
-
{:else if $httpdStore.state === "running"}
-
  <Button
-
    on:click={() => {
-
      modal.show({
-
        component: ConnectModal,
-
        props: {},
-
      });
-
    }}
-
    title={buttonTitle[$httpdStore.state]}
-
    size="large"
-
    variant="outline">
-
    <Icon name="device" />
-
    Read only
-
    <div class="indicator" />
-
  </Button>
{:else}
-
  <Button
-
    on:click={() => {
-
      modal.show({
-
        component: ConnectModal,
-
        props: {},
-
      });
-
    }}
-
    title={buttonTitle[$httpdStore.state]}
-
    size="large"
-
    variant="secondary">
-
    <Icon name="device" />
-
    Connect
+
  <Button title={buttonTitle[$httpdStore.state]} size="large" variant="primary">
+
    <IconSmall name="device" />
+
    Connected
  </Button>
{/if}
added src/App/Header/NodeInfo.svelte
@@ -0,0 +1,64 @@
+
<script lang="ts">
+
  import Button from "@app/components/Button.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+
  import Command from "@app/components/Command.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+

+
  export let running: boolean = false;
+
</script>
+

+
<style>
+
  .label {
+
    display: block;
+
    font-size: var(--font-size-small);
+
    font-weight: var(--font-weight-regular);
+
    margin-bottom: 0.75rem;
+
  }
+
</style>
+

+
<Popover
+
  popoverPositionTop="3rem"
+
  popoverPositionRight="-13rem"
+
  popoverPositionLeft="0">
+
  <Button
+
    slot="toggle"
+
    let:toggle
+
    on:click={toggle}
+
    size="large"
+
    variant={running ? "primary" : "outline"}>
+
    <IconSmall name="broadcasting" />
+
    {#if running}
+
      Syncing
+
    {:else}
+
      Sync
+
    {/if}
+
  </Button>
+

+
  <svelte:fragment slot="popover">
+
    {#if running}
+
      <div class="label">
+
        Use the <a
+
          target="_blank"
+
          rel="noreferrer"
+
          href="https://radicle.xyz/#try"
+
          class="txt-link txt-bold">
+
          Radicle CLI
+
        </a>
+
        to stop your node.
+
      </div>
+
      <Command command="rad node stop" fullWidth />
+
    {:else}
+
      <div class="label">
+
        Use the <a
+
          target="_blank"
+
          rel="noreferrer"
+
          href="https://radicle.xyz/#try"
+
          class="txt-link txt-bold">
+
          Radicle CLI
+
        </a>
+
        to start your node.
+
      </div>
+
      <Command command="rad node start" fullWidth />
+
    {/if}
+
  </svelte:fragment>
+
</Popover>
modified 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 {
modified 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"}
+
    <path
+
      d="M4.51041 2.85355C4.70567 2.65829 4.70567 2.34171 4.51041 2.14645C4.31515 1.95118 3.99856 1.95118 3.8033 2.14645C1.3989 4.55085 1.3989 8.44916 3.8033 10.8536C3.99856 11.0488 4.31515 11.0488 4.51041 10.8536C4.70567 10.6583 4.70567 10.3417 4.51041 10.1465C2.49653 8.13258 2.49653 4.86743 4.51041 2.85355Z" />
+
    <path
+
      d="M12.5104 2.14645C12.3152 1.95118 11.9986 1.95118 11.8033 2.14645C11.608 2.34171 11.608 2.65829 11.8033 2.85355C13.8172 4.86743 13.8172 8.13258 11.8033 10.1465C11.608 10.3417 11.608 10.6583 11.8033 10.8536C11.9986 11.0488 12.3152 11.0488 12.5104 10.8536C14.9148 8.44916 14.9148 4.55085 12.5104 2.14645Z" />
+
    <path
+
      d="M6.01041 4.35355C6.20567 4.15829 6.20567 3.84171 6.01041 3.64645C5.81515 3.45118 5.49856 3.45118 5.3033 3.64645C3.72733 5.22242 3.72733 7.77758 5.3033 9.35356C5.49856 9.54882 5.81515 9.54882 6.01041 9.35356C6.20567 9.1583 6.20567 8.84171 6.01041 8.64645C4.82496 7.461 4.82496 5.539 6.01041 4.35355Z" />
+
    <path
+
      d="M11.0104 3.64645C10.8152 3.45118 10.4986 3.45118 10.3033 3.64645C10.108 3.84171 10.108 4.15829 10.3033 4.35355C11.4888 5.539 11.4888 7.461 10.3033 8.64645C10.108 8.84171 10.108 9.1583 10.3033 9.35356C10.4986 9.54882 10.8152 9.54882 11.0104 9.35356C12.5864 7.77758 12.5864 5.22242 11.0104 3.64645Z" />
+
    <path
+
      d="M8.92309 8.34797C9.64744 8.04729 10.1569 7.33314 10.1569 6.5C10.1569 5.39543 9.26143 4.5 8.15686 4.5C7.05229 4.5 6.15686 5.39543 6.15686 6.5C6.15686 7.33314 6.66629 8.0473 7.39065 8.34798L5.65202 14.4332C5.57759 14.6937 5.74329 14.9348 6.02212 14.9718C6.30095 15.0087 6.58732 14.8275 6.66175 14.567L8.15687 9.33407L9.65198 14.5669C9.7264 14.8274 10.0128 15.0087 10.2916 14.9717C10.5704 14.9348 10.7361 14.6936 10.6617 14.4331L8.92309 8.34797Z" />
  {:else if name === "chat"}
    <path
      fill-rule="evenodd"
@@ -173,6 +187,11 @@
      d="M3.163 3.163a.556.556 0 01.785 0L8 7.214l4.052-4.051a.556.556 0 01.785.785L8.786 8l4.051 4.052a.556.556 0 01-.785.785L8 8.786l-4.052 4.051a.556.556 0 01-.785-.785L7.214 8 3.163 3.948a.556.556 0 010-.785z"
      clip-rule="evenodd">
    </path>
+
  {:else if name === "device"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4.5306 4C4.47537 4 4.4306 4.04477 4.4306 4.1V8.4H11.8306V4.1C11.8306 4.04477 11.7858 4 11.7306 4H4.5306ZM12.0216 9.4H4.23962L3.01198 11.8553C2.97873 11.9218 3.02708 12 3.10142 12H13.1598C13.2341 12 13.2825 11.9218 13.2492 11.8553L12.0216 9.4ZM3.4306 8.78197V4.1C3.4306 3.49249 3.92309 3 4.5306 3H11.7306C12.3381 3 12.8306 3.49249 12.8306 4.1V8.78197L14.1436 11.4081C14.5093 12.1395 13.9775 13 13.1598 13H3.10142C2.2837 13 1.75185 12.1395 2.11755 11.4081L3.4306 8.78197Z" />
  {:else if name === "delegate"}
    <path
      fill-rule="evenodd"
@@ -328,6 +347,11 @@
    <path d="M7.40908 13.3182H6.22726V14.5H7.40908V13.3182Z" />
    <path d="M9.77273 13.3182H8.59091V14.5H9.77273V13.3182Z" />
    <path d="M10.9546 13.3182H9.77274V14.5H10.9546V13.3182Z" />
+
  {:else if name === "key"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M2.97946 2.31295C3.17472 2.11769 3.4913 2.11769 3.68657 2.31295L5.68657 4.31295C5.88183 4.50821 5.88183 4.8248 5.68657 5.02006C5.49131 5.21532 5.17473 5.21532 4.97946 5.02006L4.60568 4.64628C4.43287 4.47347 4.24071 4.46105 4.10338 4.51851C3.97096 4.57393 3.83227 4.71955 3.83301 5.0046C3.83422 5.47296 4.01312 5.93932 4.37 6.2962C5.08596 7.01216 6.24676 7.01216 6.96272 6.2962C7.67868 5.58024 7.67868 4.41944 6.96272 3.70348C6.60482 3.34558 6.1368 3.16669 5.66707 3.16651C5.39093 3.1664 5.16715 2.94246 5.16726 2.66631C5.16737 2.39017 5.39131 2.1664 5.66745 2.16651C6.39143 2.16678 7.11696 2.4435 7.66983 2.99637C8.77631 4.10285 8.77631 5.89682 7.66983 7.00331C6.56334 8.10979 4.76937 8.10979 3.66289 7.00331C3.11163 6.45204 2.83488 5.72909 2.83301 5.00719C2.83141 4.38747 3.14273 3.8914 3.60722 3.64782L2.97946 3.02006C2.7842 2.8248 2.7842 2.50821 2.97946 2.31295ZM8.31281 7.64628C8.50807 7.45102 8.82466 7.45102 9.01992 7.64629L13.0199 11.6463C13.2152 11.8415 13.2152 12.1581 13.0199 12.3534C12.8247 12.5487 12.5081 12.5487 12.3128 12.3534L11.6664 11.7069L10.3532 13.0201C10.158 13.2153 9.8414 13.2153 9.64614 13.0201C9.45088 12.8248 9.45088 12.5082 9.64614 12.313L10.9593 10.9998L10.333 10.3736L9.01991 11.6867C8.82465 11.882 8.50807 11.882 8.31281 11.6867C8.11754 11.4915 8.11754 11.1749 8.31281 10.9796L9.62592 9.6665L8.31281 8.35339C8.11755 8.15813 8.11755 7.84155 8.31281 7.64628Z" />
  {:else if name === "more"}
    <path
      fill-rule="evenodd"
modified src/lib/httpd.ts
@@ -136,7 +136,7 @@ async function checkState() {
            update(httpdState);
          }
        } else {
-
          await api.getNode();
+
          await api.getNodeInfo();
          update({ state: "running" });
        }
      } catch (error) {
added src/lib/node.ts
@@ -0,0 +1,91 @@
+
import { derived, writable } from "svelte/store";
+
import { withTimeout, Mutex, E_CANCELED, E_TIMEOUT } from "async-mutex";
+

+
import { HttpdClient } from "@httpd-client";
+
import { config } from "@app/lib/config";
+

+
export type NodeState = "stopped" | "running";
+

+
const NODE_STATE_STORAGE_KEY = "nodeState";
+

+
const store = writable<NodeState>("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();
+
}
modified 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(
modified 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());