Add experimental switch in settings

Hide all read/write functionality behind this setting.
This commit is contained in:
Sebastian Martinez
2024-03-07 10:35:21 +01:00
parent 273dff2da4
commit 192524fe90
19 changed files with 296 additions and 137 deletions
+21 -18
View File
@@ -11,6 +11,7 @@
import NodeInfo from "@app/App/Header/NodeInfo.svelte";
import Popover from "@app/components/Popover.svelte";
import ConnectInstructions from "@app/components/ConnectInstructions.svelte";
import { experimental } from "@app/lib/appearance";
const buttonTitle: Record<HttpdState["state"], string> = {
stopped: "radicle-httpd is stopped",
@@ -60,24 +61,26 @@
</div>
<div class="right">
{#if $httpdStore.state === "stopped"}
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
<Button
slot="toggle"
let:toggle
on:click={toggle}
title={buttonTitle[$httpdStore.state]}
variant="naked-toggle">
<IconSmall name="device" />
Connect
</Button>
<div slot="popover" class="connect-popover">
<ConnectInstructions />
</div>
</Popover>
{:else}
<NodeInfo node={$httpdStore.node} />
<Authenticate />
{#if $experimental}
{#if $httpdStore.state === "stopped"}
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
<Button
slot="toggle"
let:toggle
on:click={toggle}
title={buttonTitle[$httpdStore.state]}
variant="naked-toggle">
<IconSmall name="device" />
Connect
</Button>
<div slot="popover" class="connect-popover">
<ConnectInstructions />
</div>
</Popover>
{:else}
<NodeInfo node={$httpdStore.node} />
<Authenticate />
{/if}
{/if}
</div>
</header>
+28 -1
View File
@@ -2,7 +2,9 @@
import {
codeFont,
codeFonts,
experimental,
storeCodeFont,
storeExperimental,
storeTheme,
theme,
} from "@app/lib/appearance";
@@ -14,7 +16,7 @@
<style>
.settings {
width: 18.5rem;
width: 24rem;
display: flex;
flex-direction: column;
align-items: center;
@@ -76,4 +78,29 @@
</Radio>
</div>
</div>
<div class="item">
<div
style="display: flex; flex-direction: row; align-items: center; gap: 0.5rem;">
Make changes on the web (experimental)
</div>
<div class="right">
<Radio>
<Button
styleBorderRadius="0"
on:click={() => storeExperimental(true)}
variant={$experimental ? "selected" : "not-selected"}>
On
</Button>
<div class="global-spacer" />
<Radio>
<Button
styleBorderRadius="0"
on:click={() => storeExperimental(undefined)}
variant={$experimental ? "not-selected" : "selected"}>
Off
</Button>
</Radio>
</Radio>
</div>
</div>
</div>
+31 -12
View File
@@ -1,8 +1,11 @@
<script>
import Command from "./Command.svelte";
import { experimental } from "@app/lib/appearance";
import { api, httpdStore } from "@app/lib/httpd";
import { routeToPath, activeUnloadedRouteStore } from "@app/lib/router";
import Command from "@app/components/Command.svelte";
import ExternalLink from "./ExternalLink.svelte";
$: path = routeToPath($activeUnloadedRouteStore);
$: pathParam = path === "/" ? "" : `--path "${path}"`;
</script>
@@ -30,25 +33,41 @@
</style>
<div>
{#if $httpdStore.state === "running"}
<div class="label">Authenticate with your local node to make changes.</div>
<Command
fullWidth
command={`rad web ${window.origin} --connect ${api.hostname}:${api.port} ${pathParam}`} />
{#if $experimental}
{#if $httpdStore.state === "running"}
<div class="label">
Authenticate with your local node to make changes.
</div>
<Command
fullWidth
command={`rad web ${window.origin} --connect ${api.hostname}:${api.port} ${pathParam}`} />
{:else}
<div class="heading">Connect & Authenticate</div>
<div class="label">
Connect to your local node to browse projects on your local machine,
create issues, and participate in discussions.
</div>
<Command fullWidth command={`rad web ${window.origin} ${pathParam}`} />
<div class="divider" />
<div class="heading">New to Radicle?</div>
<div class="label">
Visit <ExternalLink href="https://radicle.xyz/#try" /> to download Radicle
and get started.
</div>
{/if}
{:else}
<div class="heading">Connect & Authenticate</div>
<div class="heading">Browse your local projects</div>
<div class="label">
Connect to your local node to browse projects on your local machine,
create issues, and participate in discussions.
To browse projects on your local node, run the following command.
</div>
<Command fullWidth command={`rad web ${window.origin} ${pathParam}`} />
<Command fullWidth command="radicle-httpd" />
<div class="divider" />
<div class="heading">New to Radicle?</div>
<div class="label">
Run the following command and follow the instructions to install Radicle
Visit <ExternalLink href="https://radicle.xyz/#try" /> to download Radicle
and get started.
</div>
<Command fullWidth command="curl -sSf https://radicle.xyz/install | sh" />
{/if}
</div>
+15 -8
View File
@@ -25,16 +25,23 @@
<div class="reactions">
{#each reactions as { emoji, authors }}
<IconButton
on:click={async () => {
if (handleReaction) {
await handleReaction(authors, emoji);
}
}}>
<div class="reaction txt-tiny">
{#if handleReaction}
<IconButton
on:click={async () => {
if (handleReaction) {
await handleReaction(authors, emoji);
}
}}>
<div class="reaction txt-tiny">
<span>{emoji}</span>
<span title={authors.join("\n")}>{authors.length}</span>
</div>
</IconButton>
{:else}
<div class="reaction txt-tiny" style="padding: 2px 4px;">
<span>{emoji}</span>
<span title={authors.join("\n")}>{authors.length}</span>
</div>
</IconButton>
{/if}
{/each}
</div>
+22
View File
@@ -48,3 +48,25 @@ export function storeCodeFont(newCodeFont: CodeFont): void {
codeFont.set(newCodeFont);
window.localStorage.setItem("codefont", newCodeFont);
}
export const experimental = writable<true | undefined>(
loadExperimentalSetting(),
);
function loadExperimentalSetting(): true | undefined {
const storedExperimental = window.localStorage.getItem("experimental");
if (storedExperimental === null) {
return undefined;
} else {
return storedExperimental === "true" ? true : undefined;
}
}
export function storeExperimental(newSetting: true | undefined): void {
experimental.set(newSetting);
window.localStorage.setItem(
"experimental",
newSetting === true ? "true" : "undefined",
);
}
+10
View File
@@ -6,6 +6,7 @@ import { withTimeout, Mutex, E_CANCELED, E_TIMEOUT } from "async-mutex";
import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { experimental } from "./appearance";
export interface Session {
id: string;
@@ -128,6 +129,15 @@ async function checkState() {
try {
const node = await api.getNode();
// Return quickly and avoid additional fetches
// if experimental settings aren't updated
if (!get(experimental)) {
update({
state: "running",
node,
});
}
if (httpdState && httpdState.state !== "stopped") {
httpdState.node = node;
}
+5
View File
@@ -1,4 +1,6 @@
import { parseNodeId } from "@app/lib/utils";
import { get } from "svelte/store";
import { experimental } from "./appearance";
export function isDelegate(
publicKey: string | undefined,
@@ -27,10 +29,13 @@ function matchAuthor(
// All restricted actions are a combination of either:
// - the user is a delegate
// - the user is an author of the comment, issue, patch, etc.
//
// If the experimental setting isn't turned on, we return undefined early.
export function isDelegateOrAuthor(
publicKey: string | undefined,
delegates: string[],
author: string,
) {
if (get(experimental) === undefined) return undefined;
return isDelegate(publicKey, delegates) || matchAuthor(publicKey, author);
}
+59 -47
View File
@@ -6,8 +6,9 @@
import { literal, union } from "zod";
import { api, httpdStore } from "@app/lib/httpd";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { baseUrlToString } from "@app/lib/utils";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { experimental } from "@app/lib/appearance";
import { handleError } from "@app/views/home/error";
import { isDelegate } from "@app/lib/roles";
import { preferredSeeds } from "@app/lib/seeds";
@@ -127,54 +128,65 @@
<AppLayout>
<div class="wrapper">
<div class="global-hide-on-mobile">
<HomepageSection
loading={$httpdStore.state !== "stopped" && localProjects === undefined}
empty={$httpdStore.state === "stopped" ||
(filteredLocalProjects instanceof Array &&
!filteredLocalProjects.length) ||
localProjects instanceof Error}
title="Local projects"
subtitle="Projects you're seeding with your local node">
<svelte:fragment slot="actions">
<FilterButton disabled={!nodeId} bind:value={$localProjectsFilter} />
<NewProjectButton disabled={!nodeId} />
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="empty-state">
{#if !nodeId}
<div style="text-align: left; width: 100%;">
<ConnectInstructions />
</div>
{:else if localProjects instanceof Error}
<ErrorMessage
{...handleError(localProjects, baseUrlToString(api.baseUrl))} />
{:else if !localProjects?.length}
<div class="heading">No local projects</div>
<div class="label">
Seed or check out a project to work with it on your local node.
</div>
{:else}
<div class="heading">Nothing to see here</div>
<div class="label">
No local projects matched your filter settings.
</div>
{#if $experimental}
<div class="global-hide-on-mobile">
<HomepageSection
loading={$httpdStore.state !== "stopped" &&
localProjects === undefined}
empty={$httpdStore.state === "stopped" ||
(filteredLocalProjects instanceof Array &&
!filteredLocalProjects.length) ||
localProjects instanceof Error}
title="Local projects"
subtitle="Projects you're seeding with your local node">
<svelte:fragment slot="actions">
<FilterButton
disabled={!nodeId}
bind:value={$localProjectsFilter} />
<NewProjectButton disabled={!nodeId} />
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="empty-state">
{#if !nodeId}
<div style="text-align: left; width: 100%;">
<ConnectInstructions />
</div>
{:else if localProjects instanceof Error}
<ErrorMessage
{...handleError(
localProjects,
baseUrlToString(api.baseUrl),
)} />
{:else if !localProjects?.length}
<div class="heading">No local projects</div>
<div class="label">
Seed or check out a project to work with it on your local
node.
</div>
{:else}
<div class="heading">Nothing to see here</div>
<div class="label">
No local projects matched your filter settings.
</div>
{/if}
</div>
</svelte:fragment>
<div class="project-grid">
{#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
{#each filteredLocalProjects as projectInfo}
<ProjectCard
{projectInfo}
isSeeding={true}
isDelegate={isDelegate(
nodeId,
projectInfo.project.delegates,
) ?? false} />
{/each}
{/if}
</div>
</svelte:fragment>
<div class="project-grid">
{#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
{#each filteredLocalProjects as projectInfo}
<ProjectCard
{projectInfo}
isSeeding={true}
isDelegate={isDelegate(nodeId, projectInfo.project.delegates) ??
false} />
{/each}
{/if}
</div>
</HomepageSection>
</div>
</HomepageSection>
</div>
{/if}
<HomepageSection
loading={preferredSeedProjects === undefined}
+44 -25
View File
@@ -1,8 +1,10 @@
<script lang="ts">
import * as modal from "@app/lib/modal";
import { experimental } from "@app/lib/appearance";
import { httpdStore, api } from "@app/lib/httpd";
import Button from "@app/components/Button.svelte";
import Command from "@app/components/Command.svelte";
import ErrorModal from "@app/modals/ErrorModal.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Popover from "@app/components/Popover.svelte";
@@ -70,63 +72,80 @@
.counter {
font-weight: var(--font-weight-regular);
border-radius: var(--border-radius-tiny);
background-color: var(--color-fill-secondary-counter);
background-color: var(--color-fill-ghost-hover);
border: 1px solid var(--color-border-secondary-counter);
color: var(--color-foreground-match-background);
color: var(--color-foreground-contrast);
padding: 0 0.25rem;
}
.seeding {
background-color: var(--color-fill-counter-emphasized);
color: var(--color-foreground-emphasized);
}
.not-seeding {
background-color: var(--color-fill-secondary-counter);
color: var(--color-foreground-match-background);
}
.disabled {
background-color: var(--color-fill-ghost-hover);
background-color: var(--color-fill-float-hover);
color: var(--color-foreground-disabled);
}
</style>
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button
disabled={!canEditSeeding}
slot="toggle"
disabled={$experimental ? !canEditSeeding : false}
let:toggle
on:click={async () => {
if (!seeding && canEditSeeding) {
if ($experimental && !seeding && canEditSeeding) {
await editSeeding();
} else {
toggle();
}
}}
variant={seeding ? "secondary-toggle-on" : "secondary-toggle-off"}>
variant={!$experimental
? "outline"
: seeding
? "secondary-toggle-on"
: "secondary-toggle-off"}>
<IconSmall name="network" />
<span class="title-counter">
{seeding ? "Seeding" : "Seed"}
<span
class="counter"
class:seeding
class:disabled={!canEditSeeding}
class:seeding={$experimental ? seeding : false}
class:not-seeding={$experimental ? !seeding : false}
class:disabled={$experimental ? !canEditSeeding : false}
style:font-weight="var(--font-weight-regular)">
{seedCount}
</span>
</span>
</Button>
<div slot="popover" let:toggle style:width={seeding ? "19.5rem" : "30.5rem"}>
<div class="seed-label txt-bold">Stop seeding</div>
<div class="seed-label">
Are you sure you want to stop seeding this project? If you don't seed a
project it won't appear in the local projects section anymore and any
changes you make to it won't propagate to the network.
</div>
<Button
styleWidth="100%"
disabled={editSeedingInProgress}
on:click={async () => {
await editSeeding();
toggle();
}}>
<IconSmall name="network" />
Stop seeding
</Button>
<div
slot="popover"
style:width={$experimental ? (seeding ? "19.5rem" : "30.5rem") : "auto"}>
{#if $experimental && canEditSeeding && seeding}
<div class="seed-label txt-bold">Stop seeding</div>
<div class="seed-label">
Are you sure you want to stop seeding this project? If you don't seed a
project it won't appear in the local projects section anymore and any
changes you make to it won't propagate to the network.
</div>
<Button
styleWidth="100%"
disabled={editSeedingInProgress}
on:click={async () => {
await editSeeding();
}}>
<IconSmall name="network" />
Stop seeding
</Button>
{:else}
<span class="seed-label">
Use the Radicle CLI to {seeding ? "stop" : "start"} seeding this project.
</span>
<Command command={`rad ${seeding ? "unseed" : "seed"} ${projectId}`} />
{/if}
</div>
</Popover>
+13 -6
View File
@@ -17,6 +17,7 @@
import * as role from "@app/lib/roles";
import * as router from "@app/lib/router";
import * as utils from "@app/lib/utils";
import { experimental } from "@app/lib/appearance";
import { HttpdClient } from "@httpd-client";
import { closeFocused } from "@app/components/Popover.svelte";
import { httpdStore } from "@app/lib/httpd";
@@ -537,7 +538,7 @@
{/if}
</svelte:fragment>
<div slot="description">
{#if issueState !== "read"}
{#if $experimental && issueState !== "read"}
<ExtendedTextarea
isValid={() => newTitle.length > 0}
disallowEmptyBody
@@ -578,7 +579,7 @@
<span class="txt-missing">No description</span>
{/if}
<div class="reactions">
{#if session}
{#if $experimental && session}
<ReactionSelector
reactions={issue.discussion[0].reactions}
on:select={async ({ detail: { authors, emoji } }) => {
@@ -635,14 +636,20 @@
session?.publicKey,
project.delegates,
)}
editComment={session && partial(editComment, session.id)}
createReply={session && partial(createReply, session.id)}
reactOnComment={session && partial(reactOnComment, session)} />
editComment={$experimental &&
session &&
partial(editComment, session.id)}
createReply={$experimental &&
session &&
partial(createReply, session.id)}
reactOnComment={$experimental &&
session &&
partial(reactOnComment, session)} />
<div class="connector" />
{/each}
</div>
{/if}
{#if session}
{#if $experimental && session}
{#if threads.length === 0}
<div class="connector" />{/if}
<CommentToggleInput
+5 -4
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import type { BaseUrl, Issue, IssueState, Project } from "@httpd-client";
import capitalize from "lodash/capitalize";
import { HttpdClient } from "@httpd-client";
import { ISSUES_PER_PAGE } from "./router";
import { closeFocused } from "@app/components/Popover.svelte";
import { httpdStore } from "@app/lib/httpd";
import { baseUrlToString, isLocal } from "@app/lib/utils";
import capitalize from "lodash/capitalize";
import { closeFocused } from "@app/components/Popover.svelte";
import { experimental } from "@app/lib/appearance";
import { httpdStore } from "@app/lib/httpd";
import Button from "@app/components/Button.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
@@ -146,7 +147,7 @@
<div style="margin-left: auto; display: flex; gap: 1rem;">
<Share {baseUrl} />
{#if $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
{#if $experimental && $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
<Link
route={{
resource: "project.newIssue",
+14 -8
View File
@@ -46,6 +46,7 @@
import * as role from "@app/lib/roles";
import * as router from "@app/lib/router";
import * as utils from "@app/lib/utils";
import { experimental } from "@app/lib/appearance";
import capitalize from "lodash/capitalize";
import isEqual from "lodash/isEqual";
import partial from "lodash/partial";
@@ -767,7 +768,7 @@
</svelte:fragment>
<svelte:fragment slot="description">
<div class="revision-description">
{#if session && patchState !== "read" && lastEdit}
{#if $experimental && session && patchState !== "read" && lastEdit}
<ExtendedTextarea
isValid={() => patch.title.length > 0}
enableAttachments
@@ -802,7 +803,7 @@
{:else}
<span class="txt-missing">No description available</span>
{/if}
{#if session || (firstRevision.revisionReactions && firstRevision.revisionReactions.length > 0)}
{#if ($experimental && session) || (firstRevision.revisionReactions && firstRevision.revisionReactions.length > 0)}
<div class="actions">
{#if session}
<ReactionSelector
@@ -979,15 +980,20 @@
session?.publicKey,
project.delegates,
)}
editRevision={session &&
editRevision={$experimental &&
session &&
partial(editRevision, session.id, revision.revisionId)}
editComment={session &&
editComment={$experimental &&
session &&
partial(editComment, session.id, revision.revisionId)}
reactOnComment={session &&
reactOnComment={$experimental &&
session &&
partial(reactOnComment, session, revision.revisionId)}
reactOnRevision={session &&
reactOnRevision={$experimental &&
session &&
partial(reactOnRevision, session, revision.revisionId)}
createReply={session &&
createReply={$experimental &&
session &&
partial(createReply, session.id, revision.revisionId)}
patchId={patch.id}
patchState={patch.state}
@@ -995,7 +1001,7 @@
previousRevId={previousRevision?.id}
previousRevOid={previousRevision?.oid}>
{#if index === patch.revisions.length - 1}
{#if session && view.name === "activity"}
{#if $experimental && session && view.name === "activity"}
<div class="connector" />
<CommentToggleInput
rawPath={rawPath(patch.revisions[0].id)}
+2 -1
View File
@@ -5,6 +5,7 @@
import capitalize from "lodash/capitalize";
import { PATCHES_PER_PAGE } from "./router";
import { experimental } from "@app/lib/appearance";
import { httpdStore } from "@app/lib/httpd";
import { baseUrlToString, isLocal } from "@app/lib/utils";
@@ -160,7 +161,7 @@
<div style="margin-left: auto; display: flex; gap: 1rem;">
<Share {baseUrl} />
{#if $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
{#if $experimental && $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button
slot="toggle"
+12 -3
View File
@@ -2,6 +2,7 @@
import type { ActiveTab } from "./Header.svelte";
import type { BaseUrl, Project } from "@httpd-client";
import { experimental } from "@app/lib/appearance";
import { queryProject } from "@app/lib/projects";
import { httpdStore, api } from "@app/lib/httpd";
import { isLocal } from "@app/lib/utils";
@@ -36,7 +37,9 @@
let queryingLocalProject: boolean = true;
let localProject: "notFound" | "found" | undefined = undefined;
$: hideContextHelp =
isLocal(baseUrl.hostname) && $httpdStore.state === "authenticated";
$experimental &&
isLocal(baseUrl.hostname) &&
$httpdStore.state === "authenticated";
httpdStore.subscribe(async () => {
if ($httpdStore.state !== "stopped" && !queryingLocalProject) {
@@ -65,7 +68,13 @@
queryingLocalProject = false;
}
onMount(async () => await detectLocalProject());
onMount(async () => {
if ($httpdStore.state !== "stopped") {
await detectLocalProject();
} else {
localProject = "notFound";
}
});
</script>
<style>
@@ -250,7 +259,7 @@
</div>
<div class="bottom">
<div class="help" class:expanded>
{#if !hideContextHelp && expanded}
{#if !hideContextHelp && expanded && $experimental}
{#if !localProject}
<div
style="display: flex; justify-content: center; align-items: center; height: 2rem;">
@@ -4,12 +4,12 @@
import { twemoji } from "@app/lib/utils";
import Badge from "@app/components/Badge.svelte";
import CloneButton from "../Header/CloneButton.svelte";
import CloneButton from "@app/views/projects/Header/CloneButton.svelte";
import CopyableId from "@app/components/CopyableId.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import Link from "@app/components/Link.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import SeedButton from "../Header/SeedButton.svelte";
import SeedButton from "@app/views/projects/Header/SeedButton.svelte";
import Share from "@app/views/projects/Share.svelte";
export let project: Project;
+5
View File
@@ -27,6 +27,8 @@ import { ResponseError } from "@httpd-client/lib/fetcher";
import { handleError } from "@app/views/projects/error";
import { nodePath } from "@app/views/nodes/router";
import { unreachable } from "@app/lib/utils";
import { get } from "svelte/store";
import { experimental } from "@app/lib/appearance";
export const COMMITS_PER_PAGE = 30;
export const PATCHES_PER_PAGE = 10;
@@ -242,6 +244,9 @@ function parseRevisionToOid(
}
async function isLocalNodeSeeding(route: ProjectRoute): Promise<boolean> {
if (!get(experimental) && get(httpd.httpdStore).state === "stopped") {
return false;
}
try {
const tracking = await httpd.api.getTracking();
return tracking.some(({ id }) => id === route.project);
+1 -1
View File
@@ -5,5 +5,5 @@ test("exceptions in production build", async ({ page }) => {
// Wait for scripts to finish executing, there might be exceptions that
// happen after the page has been painted.
await page.waitForTimeout(2000);
await expect(page.getByText("Local projects")).toBeVisible();
await expect(page.getByText("Explore", { exact: true })).toBeVisible();
});
+1
View File
@@ -5,6 +5,7 @@ test.use({
});
test("show pinned projects", async ({ page }) => {
await page.addInitScript(() => localStorage.setItem("experimental", "true"));
await page.addInitScript(appConfigWithFixture);
await page.goto("/");
await expect(page.getByText("Local projects")).toBeVisible();
+5
View File
@@ -151,6 +151,9 @@ export const test = base.extend<{
},
authenticatedPeer: async ({ page, peerManager }, use) => {
await page.addInitScript(() => {
window.localStorage.setItem("experimental", "true");
});
const peer = await peerManager.createPeer({
name: "httpd",
gitOptions: gitOptions["bob"],
@@ -161,6 +164,8 @@ export const test = base.extend<{
const { stdout } = await peer.spawn("rad-web", [
"http://localhost:3001",
"--no-open",
"--path",
"/",
"--connect",
`${peer.httpdBaseUrl.hostname}:${peer.httpdBaseUrl.port}`,
]);