Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Use `/nodes/` in URL instead of `/seeds/`
Sebastian Martinez committed 2 years ago
commit 452577126493634a6c920b4c9d124f7454aeeaff
parent 8d798cc93f381038abc2fc095b70d9556a5ef9a4
52 files changed +518 -523
modified README.md
@@ -21,5 +21,5 @@ Your app is ready to be deployed!

Latest production deployment 👉 https://app.radicle.xyz

-
If you want to connect to pre-heartwood seed nodes, you can run the app locally
+
If you want to connect to pre-heartwood nodes, you can run the app locally
using the `legacy` tag, available by running `git checkout legacy`.
modified src/App.svelte
@@ -12,7 +12,7 @@

  import Home from "@app/views/home/Index.svelte";
  import Projects from "@app/views/projects/View.svelte";
-
  import Seeds from "@app/views/seeds/View.svelte";
+
  import Nodes from "@app/views/nodes/View.svelte";
  import Session from "@app/views/session/Index.svelte";

  import LoadError from "@app/components/LoadError.svelte";
@@ -63,8 +63,8 @@
  <div class="wrapper">
    {#if $activeRouteStore.resource === "home"}
      <Home {...$activeRouteStore.params} />
-
    {:else if $activeRouteStore.resource === "seeds"}
-
      <Seeds {...$activeRouteStore.params} />
+
    {:else if $activeRouteStore.resource === "nodes"}
+
      <Nodes {...$activeRouteStore.params} />
    {:else if $activeRouteStore.resource === "session"}
      <Session activeRoute={$activeRouteStore} />
    {:else if $activeRouteStore.resource === "projects"}
modified src/App/Header/Connect.svelte
@@ -119,7 +119,7 @@
        <Link
          on:afterNavigate={closeFocused}
          route={{
-
            resource: "seeds",
+
            resource: "nodes",
            params: {
              baseUrl: httpd.api.baseUrl,
              projectPageIndex: 0,
@@ -152,7 +152,7 @@
        <Link
          on:afterNavigate={closeFocused}
          route={{
-
            resource: "seeds",
+
            resource: "nodes",
            params: {
              baseUrl: httpd.api.baseUrl,
              projectPageIndex: 0,
modified src/App/Header/Search.svelte
@@ -51,7 +51,7 @@
        void router.push({
          resource: "project.tree",
          project: project.id,
-
          seed: baseUrl,
+
          node: baseUrl,
        });
      } else {
        modal.show({
modified src/App/Header/SearchResultsModal.svelte
@@ -38,7 +38,7 @@
              on:afterNavigate={modal.hide}
              route={{
                resource: "project.tree",
-
                seed: result.baseUrl,
+
                node: result.baseUrl,
                project: result.project.id,
              }}>
              <span title={result.baseUrl.hostname}>
modified src/config.json
@@ -1,6 +1,6 @@
{
  "reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
-
  "seeds": {
+
  "nodes": {
    "defaultHttpdPort": 443,
    "defaultLocalHttpdPort": 8080,
    "defaultHttpdScheme": "https",
modified src/lib/commit.ts
@@ -31,44 +31,38 @@ export function groupCommits(commits: CommitHeader[]): CommitGroup[] {
  const groupedCommits: CommitGroup[] = [];
  let groupDate: Date | undefined = undefined;

-
  try {
-
    commits = commits.sort((a, b) => {
-
      if (a.committer.time > b.committer.time) {
-
        return -1;
-
      } else if (a.committer.time < b.committer.time) {
-
        return 1;
-
      }
+
  commits = commits.sort((a, b) => {
+
    if (a.committer.time > b.committer.time) {
+
      return -1;
+
    } else if (a.committer.time < b.committer.time) {
+
      return 1;
+
    }

-
      return 0;
-
    });
-

-
    for (const commit of commits) {
-
      const time = commit.committer.time * 1000;
-
      const date = new Date(time);
-
      const isNewDay =
-
        !groupedCommits.length ||
-
        !groupDate ||
-
        date.getDate() < groupDate.getDate() ||
-
        date.getMonth() < groupDate.getMonth() ||
-
        date.getFullYear() < groupDate.getFullYear();
-

-
      if (isNewDay) {
-
        groupedCommits.push({
-
          date: formatGroupTime(time),
-
          time,
-
          commits: [],
-
          week: 0,
-
        });
-
        groupDate = date;
-
      }
-
      groupedCommits[groupedCommits.length - 1].commits.push(commit);
+
    return 0;
+
  });
+

+
  for (const commit of commits) {
+
    const time = commit.committer.time * 1000;
+
    const date = new Date(time);
+
    const isNewDay =
+
      !groupedCommits.length ||
+
      !groupDate ||
+
      date.getDate() < groupDate.getDate() ||
+
      date.getMonth() < groupDate.getMonth() ||
+
      date.getFullYear() < groupDate.getFullYear();
+

+
    if (isNewDay) {
+
      groupedCommits.push({
+
        date: formatGroupTime(time),
+
        time,
+
        commits: [],
+
        week: 0,
+
      });
+
      groupDate = date;
    }
-
    return groupedCommits;
-
  } catch (err) {
-
    throw new Error(
-
      "Not able to create commit history, please consider updating seed HTTP API.",
-
    );
+
    groupedCommits[groupedCommits.length - 1].commits.push(commit);
  }
+
  return groupedCommits;
}

function groupCommitsByWeek(commits: number[]): WeeklyActivity[] {
modified src/lib/config.ts
@@ -4,7 +4,7 @@ import configJson from "@app/config.json";

export interface Config {
  reactions: string[];
-
  seeds: {
+
  nodes: {
    defaultHttpdPort: number;
    defaultLocalHttpdPort: number;
    defaultNodePort: number;
@@ -24,7 +24,7 @@ function getConfig(): Config {
  if (window.VITEST) {
    return {
      reactions: [],
-
      seeds: {
+
      nodes: {
        defaultHttpdPort: 8081,
        defaultLocalHttpdPort: 8081,
        defaultHttpdScheme: "http",
modified src/lib/httpd.ts
@@ -25,7 +25,7 @@ export const httpdStore = derived(store, s => s);

export const api = new HttpdClient({
  hostname: "127.0.0.1",
-
  port: config.seeds.defaultLocalHttpdPort,
+
  port: config.nodes.defaultLocalHttpdPort,
  scheme: "http",
});

modified src/lib/router.ts
@@ -12,7 +12,7 @@ import {
  resolveProjectRoute,
} from "@app/views/projects/router";
import { loadRoute } from "@app/lib/router/definitions";
-
import { seedPath } from "@app/views/seeds/router";
+
import { nodePath } from "@app/views/nodes/router";

export { type Route };

@@ -128,7 +128,7 @@ function setTitle(loadedRoute: LoadedRoute) {
    title.push("Radicle");
  } else if (loadedRoute.resource === "projects") {
    title.push(...projectTitle(loadedRoute));
-
  } else if (loadedRoute.resource === "seeds") {
+
  } else if (loadedRoute.resource === "nodes") {
    title.push(loadedRoute.params.baseUrl.hostname);
  } else if (loadedRoute.resource === "session") {
    title.push("Authenticating");
@@ -151,15 +151,15 @@ export async function replace(newRoute: Route): Promise<void> {
function extractBaseUrl(hostAndPort: string): BaseUrl {
  if (
    hostAndPort === "radicle.local" ||
-
    hostAndPort === `radicle.local:${config.seeds.defaultHttpdPort}` ||
+
    hostAndPort === `radicle.local:${config.nodes.defaultHttpdPort}` ||
    hostAndPort === "0.0.0.0" ||
-
    hostAndPort === `0.0.0.0:${config.seeds.defaultHttpdPort}` ||
+
    hostAndPort === `0.0.0.0:${config.nodes.defaultHttpdPort}` ||
    hostAndPort === "127.0.0.1" ||
-
    hostAndPort === `127.0.0.1:${config.seeds.defaultHttpdPort}`
+
    hostAndPort === `127.0.0.1:${config.nodes.defaultHttpdPort}`
  ) {
    return {
      hostname: "127.0.0.1",
-
      port: config.seeds.defaultHttpdPort,
+
      port: config.nodes.defaultHttpdPort,
      scheme: "http",
    };
  } else if (hostAndPort.includes(":")) {
@@ -169,13 +169,13 @@ function extractBaseUrl(hostAndPort: string): BaseUrl {
      port: Number(port),
      scheme: utils.isLocal(hostname)
        ? "http"
-
        : config.seeds.defaultHttpdScheme,
+
        : config.nodes.defaultHttpdScheme,
    };
  } else {
    return {
      hostname: hostAndPort,
-
      port: config.seeds.defaultHttpdPort,
-
      scheme: config.seeds.defaultHttpdScheme,
+
      port: config.nodes.defaultHttpdPort,
+
      scheme: config.nodes.defaultHttpdScheme,
    };
  }
}
@@ -185,6 +185,7 @@ function urlToRoute(url: URL): Route | null {

  const resource = segments.shift();
  switch (resource) {
+
    case "nodes":
    case "seeds": {
      const hostAndPort = segments.shift();
      if (hostAndPort) {
@@ -194,7 +195,7 @@ function urlToRoute(url: URL): Route | null {
          return resolveProjectRoute(baseUrl, id, segments, url.search);
        } else {
          return {
-
            resource: "seeds",
+
            resource: "nodes",
            params: { baseUrl, projectPageIndex: 0 },
          };
        }
@@ -229,8 +230,8 @@ export function routeToPath(route: Route): string {
    return "/";
  } else if (route.resource === "session") {
    return `/session?id=${route.params.id}&sig=${route.params.signature}&pk=${route.params.publicKey}`;
-
  } else if (route.resource === "seeds") {
-
    return seedPath(route.params.baseUrl);
+
  } else if (route.resource === "nodes") {
+
    return nodePath(route.params.baseUrl);
  } else if (route.resource === "loadError") {
    return "";
  } else if (
modified src/lib/router/definitions.ts
@@ -3,11 +3,11 @@ import type {
  ProjectLoadedRoute,
  ProjectRoute,
} from "@app/views/projects/router";
-
import type { SeedsLoadedRoute, SeedsRoute } from "@app/views/seeds/router";
+
import type { NodesRoute, NodesLoadedRoute } from "@app/views/nodes/router";

import { loadHomeRoute } from "@app/views/home/router";
import { loadProjectRoute } from "@app/views/projects/router";
-
import { loadSeedRoute } from "@app/views/seeds/router";
+
import { loadNodeRoute } from "@app/views/nodes/router";

interface BootingRoute {
  resource: "booting";
@@ -38,7 +38,7 @@ export type Route =
  | LoadError
  | NotFoundRoute
  | ProjectRoute
-
  | SeedsRoute
+
  | NodesRoute
  | SessionRoute;

export type LoadedRoute =
@@ -47,12 +47,12 @@ export type LoadedRoute =
  | LoadError
  | NotFoundRoute
  | ProjectLoadedRoute
-
  | SeedsLoadedRoute
+
  | NodesLoadedRoute
  | SessionRoute;

export async function loadRoute(route: Route): Promise<LoadedRoute> {
-
  if (route.resource === "seeds") {
-
    return await loadSeedRoute(route.params);
+
  if (route.resource === "nodes") {
+
    return await loadNodeRoute(route.params);
  } else if (route.resource === "home") {
    return await loadHomeRoute();
  } else if (
modified src/lib/search.ts
@@ -19,13 +19,13 @@ export async function searchProjectsAndProfiles(
  query: string,
): Promise<SearchResult> {
  try {
-
    const pinned = config.seeds.pinned.map(seed => ({
+
    const pinned = config.nodes.pinned.map(node => ({
      id: query,
-
      baseUrl: seed.baseUrl,
+
      baseUrl: node.baseUrl,
    }));

    if (utils.isRepositoryId(query)) {
-
      const results = await getProjectsFromSeeds(pinned);
+
      const results = await getProjectsFromNodes(pinned);

      if (results.length === 0) {
        return { type: "nothing" };
@@ -49,7 +49,7 @@ export async function searchProjectsAndProfiles(
  }
}

-
export async function getProjectsFromSeeds(
+
export async function getProjectsFromNodes(
  params: { id: string; baseUrl: BaseUrl }[],
): Promise<ProjectBaseUrl[]> {
  const projectPromises = params.map(async param => {
modified src/views/home/Index.svelte
@@ -72,7 +72,7 @@
            route={{
              resource: "project.tree",
              project: project.id,
-
              seed: baseUrl,
+
              node: baseUrl,
            }}>
            <ProjectCard
              compact
modified src/views/home/router.ts
@@ -3,7 +3,7 @@ import type { ProjectBaseUrl } from "@app/lib/search";
import type { WeeklyActivity } from "@app/lib/commit";

import { config } from "@app/lib/config";
-
import { getProjectsFromSeeds } from "@app/lib/search";
+
import { getProjectsFromNodes } from "@app/lib/search";
import { loadProjectActivity } from "@app/lib/commit";

export interface ProjectBaseUrlActivity extends ProjectBaseUrl {
@@ -21,15 +21,15 @@ export interface HomeLoadedRoute {

export async function loadHomeRoute(): Promise<HomeLoadedRoute | LoadError> {
  try {
-
    const projects = await getProjectsFromSeeds(config.projects.pinned);
+
    const projects = await getProjectsFromNodes(config.projects.pinned);
    const results = await Promise.all(
-
      projects.map(async projectSeed => {
+
      projects.map(async projectNode => {
        const activity = await loadProjectActivity(
-
          projectSeed.project.id,
-
          projectSeed.baseUrl,
+
          projectNode.project.id,
+
          projectNode.baseUrl,
        );
        return {
-
          ...projectSeed,
+
          ...projectNode,
          activity,
        };
      }),
added src/views/nodes/View.svelte
@@ -0,0 +1,153 @@
+
<script lang="ts">
+
  import type { BaseUrl } from "@httpd-client";
+
  import type { ProjectActivity } from "@app/views/nodes/router";
+

+
  import { config } from "@app/lib/config";
+
  import { isLocal, truncateId } from "@app/lib/utils";
+
  import { loadProjects } from "@app/views/nodes/router";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Clipboard from "@app/components/Clipboard.svelte";
+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import Loading from "@app/components/Loading.svelte";
+
  import ProjectCard from "@app/components/ProjectCard.svelte";
+

+
  export let baseUrl: BaseUrl;
+
  export let nid: string;
+
  export let projectCount: number;
+
  export let projectPageIndex: number;
+
  export let projects: ProjectActivity[] = [];
+
  export let version: string;
+

+
  let error: any;
+
  let loadingProjects = false;
+

+
  async function loadMore(): Promise<void> {
+
    loadingProjects = true;
+
    try {
+
      const result = await loadProjects(projectPageIndex, baseUrl);
+
      projectCount = result.total;
+
      projects = [...projects, ...result.projects];
+
      projectPageIndex += 1;
+
    } catch (err) {
+
      error = err;
+
    } finally {
+
      loadingProjects = false;
+
    }
+
  }
+

+
  $: hostname = isLocal(baseUrl.hostname) ? "radicle.local" : baseUrl.hostname;
+
  $: showMoreButton =
+
    !loadingProjects &&
+
    !error &&
+
    projectCount &&
+
    projects.length < projectCount;
+
</script>
+

+
<style>
+
  .wrapper {
+
    width: 720px;
+
    margin: 5rem 0;
+
  }
+
  .header {
+
    align-items: center;
+
    color: var(--color-secondary);
+
    display: flex;
+
    flex-direction: row;
+
    font-size: var(--font-size-large);
+
    font-weight: var(--font-weight-bold);
+
    justify-content: space-between;
+
    margin-bottom: 2rem;
+
    overflow-x: hidden;
+
    text-align: left;
+
    text-overflow: ellipsis;
+
    width: 100%;
+
  }
+
  table {
+
    border-collapse: collapse;
+
  }
+
  td {
+
    padding-bottom: 1.5rem;
+
    padding-right: 3rem;
+
  }
+
  .node-address {
+
    display: flex;
+
    align-items: center;
+
    color: var(--color-foreground-6);
+
    white-space: nowrap;
+
  }
+
  .more {
+
    margin-top: 2rem;
+
    text-align: center;
+
  }
+
  @media (max-width: 720px) {
+
    .wrapper {
+
      width: 100%;
+
      padding: 1.5rem;
+
    }
+
  }
+
</style>
+

+
<div class="wrapper">
+
  <div class="header">
+
    {hostname}
+
  </div>
+

+
  <table>
+
    <tr>
+
      <td class="txt-highlight">Address</td>
+
      <td>
+
        <div class="node-address">
+
          {truncateId(nid)}@{baseUrl.hostname}
+
          <Clipboard
+
            small
+
            text={`${nid}@${baseUrl.hostname}:${config.nodes.defaultNodePort}`} />
+
        </div>
+
      </td>
+
    </tr>
+
    <tr>
+
      <td class="txt-highlight">Version</td>
+
      <td>
+
        {version}
+
      </td>
+
    </tr>
+
  </table>
+

+
  <div style:margin-bottom="5rem">
+
    <div style:margin-top="1rem">
+
      {#each projects as { project, activity }}
+
        <div style:margin-bottom="0.5rem">
+
          <Link
+
            route={{
+
              resource: "project.tree",
+
              project: project.id,
+
              node: baseUrl,
+
            }}>
+
            <ProjectCard
+
              {activity}
+
              id={project.id}
+
              name={project.name}
+
              description={project.description}
+
              head={project.head} />
+
          </Link>
+
        </div>
+
      {/each}
+
    </div>
+
    {#if loadingProjects}
+
      <div class="more">
+
        <Loading small />
+
      </div>
+
    {/if}
+
    {#if showMoreButton}
+
      <div class="more">
+
        <Button variant="foreground" on:click={loadMore}>More</Button>
+
      </div>
+
    {/if}
+
    {#if error}
+
      <ErrorMessage
+
        message="Not able to load more projects from this node."
+
        stackTrace={error.stack} />
+
    {/if}
+
  </div>
+
</div>
added src/views/nodes/router.ts
@@ -0,0 +1,113 @@
+
import type { BaseUrl, Project } from "@httpd-client";
+
import type { LoadError } from "@app/lib/router/definitions";
+
import type { WeeklyActivity } from "@app/lib/commit";
+

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

+
export interface NodesRouteParams {
+
  baseUrl: BaseUrl;
+
  projectPageIndex: number;
+
}
+

+
export interface ProjectActivity {
+
  project: Project;
+
  activity: WeeklyActivity[];
+
}
+

+
export interface NodesRoute {
+
  resource: "nodes";
+
  params: NodesRouteParams;
+
}
+

+
export interface NodesLoadedRoute {
+
  resource: "nodes";
+
  params: {
+
    baseUrl: BaseUrl;
+
    projectPageIndex: number;
+
    version: string;
+
    nid: string;
+
    projects: ProjectActivity[];
+
    projectCount: number;
+
  };
+
}
+

+
const PROJECTS_PER_PAGE = 10;
+

+
export async function loadProjects(
+
  page: number,
+
  baseUrl: BaseUrl,
+
): Promise<{
+
  total: number;
+
  projects: ProjectActivity[];
+
}> {
+
  const api = new HttpdClient(baseUrl);
+

+
  const [nodeStats, projects] = await Promise.all([
+
    api.getStats(),
+
    api.project.getAll({ page, perPage: PROJECTS_PER_PAGE }),
+
  ]);
+

+
  const results = await Promise.all(
+
    projects.map(async project => {
+
      const activity = await loadProjectActivity(project.id, baseUrl);
+
      return {
+
        project,
+
        activity,
+
      };
+
    }),
+
  );
+
  // Sorts projects by most recent commit descending.
+
  const sortedProjects = results.sort(
+
    (a, b) => b.activity[0].time - a.activity[0].time,
+
  );
+

+
  return {
+
    total: nodeStats.projects.count,
+
    projects: sortedProjects,
+
  };
+
}
+

+
export function nodePath(baseUrl: BaseUrl) {
+
  const port = baseUrl.port ?? config.nodes.defaultHttpdPort;
+

+
  if (port === config.nodes.defaultHttpdPort) {
+
    return `/nodes/${baseUrl.hostname}`;
+
  } else {
+
    return `/nodes/${baseUrl.hostname}:${port}`;
+
  }
+
}
+

+
export async function loadNodeRoute(
+
  params: NodesRouteParams,
+
): Promise<NodesLoadedRoute | LoadError> {
+
  const api = new HttpdClient(params.baseUrl);
+
  try {
+
    const projectPageIndex = 0;
+
    const [nodeInfo, { projects, total }] = await Promise.all([
+
      api.getNodeInfo(),
+
      loadProjects(projectPageIndex, params.baseUrl),
+
    ]);
+
    return {
+
      resource: "nodes",
+
      params: {
+
        projectPageIndex: projectPageIndex + 1,
+
        baseUrl: params.baseUrl,
+
        nid: nodeInfo.node.id,
+
        version: nodeInfo.version,
+
        projects: projects,
+
        projectCount: total,
+
      },
+
    };
+
  } catch (error: any) {
+
    return {
+
      resource: "loadError",
+
      params: {
+
        title: `${params.baseUrl.hostname}:${params.baseUrl.port}`,
+
        errorMessage: "Not able to query this node.",
+
        stackTrace: error.stack,
+
      },
+
    };
+
  }
+
}
modified src/views/projects/Browser.svelte
@@ -48,7 +48,7 @@
    selected: remote.id === peer,
    route: {
      resource: "project.tree",
-
      seed: baseUrl,
+
      node: baseUrl,
      project: project.id,
      peer: remote.id,
    } as Route,
@@ -58,7 +58,7 @@
    name,
    route: {
      resource: "project.tree",
-
      seed: baseUrl,
+
      node: baseUrl,
      project: project.id,
      peer,
      revision: name,
@@ -146,7 +146,7 @@
</style>

<SourceBrowsingHeader
-
  seed={baseUrl}
+
  node={baseUrl}
  {project}
  peers={peersWithRoute}
  branches={branchesWithRoute}
modified src/views/projects/CloneButton.svelte
@@ -13,8 +13,8 @@

  $: radCloneUrl = `rad clone ${id}`;
  $: portFragment =
-
    baseUrl.scheme === config.seeds.defaultHttpdScheme &&
-
    baseUrl.port === config.seeds.defaultHttpdPort
+
    baseUrl.scheme === config.nodes.defaultHttpdScheme &&
+
    baseUrl.port === config.nodes.defaultHttpdPort
      ? ""
      : `:${baseUrl.port}`;
  $: gitCloneUrl = `git clone ${baseUrl.scheme}://${
modified src/views/projects/Cob/Revision.svelte
@@ -213,7 +213,7 @@
            route={{
              resource: "project.patch",
              project: projectId,
-
              seed: baseUrl,
+
              node: baseUrl,
              patch: patchId,
              view: {
                name: "diff",
@@ -239,7 +239,7 @@
                  route={{
                    resource: "project.patch",
                    project: projectId,
-
                    seed: baseUrl,
+
                    node: baseUrl,
                    patch: patchId,
                    view: {
                      name: "diff",
@@ -301,7 +301,7 @@
                      route={{
                        resource: "project.commit",
                        project: projectId,
-
                        seed: baseUrl,
+
                        node: baseUrl,
                        commit: commit.id,
                      }}>
                      <div class="commit-summary" use:twemoji>
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -100,7 +100,7 @@
        route={{
          resource: "project.commit",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          commit: commit.id,
        }}>
        <div class="summary" use:twemoji>
@@ -134,7 +134,7 @@
        route={{
          resource: "project.tree",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          revision: commit.id,
        }}>
        <Icon name="browse" />
modified src/views/projects/Header.svelte
@@ -45,7 +45,7 @@
    route={{
      resource: "project.tree",
      project: projectId,
-
      seed: baseUrl,
+
      node: baseUrl,
      path: "/",
    }}>
    <SquareButton active={resource === "tree" || resource === "history"}>
@@ -59,7 +59,7 @@
    route={{
      resource: "project.issues",
      project: projectId,
-
      seed: baseUrl,
+
      node: baseUrl,
    }}>
    <SquareButton active={resource === "issues" || resource === "issue"}>
      <svelte:fragment slot="icon">
@@ -74,7 +74,7 @@
    route={{
      resource: "project.patches",
      project: projectId,
-
      seed: baseUrl,
+
      node: baseUrl,
    }}>
    <SquareButton active={resource === "patches" || resource === "patch"}>
      <svelte:fragment slot="icon">
@@ -88,7 +88,7 @@

  <Link
    route={{
-
      resource: "seeds",
+
      resource: "nodes",
      params: {
        baseUrl,
        projectPageIndex: 0,
modified src/views/projects/History.svelte
@@ -65,7 +65,7 @@
    selected: remote.id === peer,
    route: {
      resource: "project.history",
-
      seed: baseUrl,
+
      node: baseUrl,
      project: project.id,
      peer: remote.id,
    } as Route,
@@ -75,7 +75,7 @@
    name,
    route: {
      resource: "project.history",
-
      seed: baseUrl,
+
      node: baseUrl,
      project: project.id,
      peer,
      revision: name,
@@ -109,7 +109,7 @@
</style>

<SourceBrowsingHeader
-
  seed={baseUrl}
+
  node={baseUrl}
  {project}
  peers={peersWithRoute}
  branches={branchesWithRoute}
modified src/views/projects/Issue.svelte
@@ -169,7 +169,7 @@
        void router.push({
          resource: "project.issue",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          issue: issue.id,
        });
      }
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -107,7 +107,7 @@
        route={{
          resource: "project.issue",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          issue: issue.id,
        }}>
        <span class="issue-title">
modified src/views/projects/Issue/New.svelte
@@ -54,7 +54,7 @@
      void router.push({
        resource: "project.issue",
        project: projectId,
-
        seed: baseUrl,
+
        node: baseUrl,
        issue: result.id,
      });
    } catch {
modified src/views/projects/Issues.svelte
@@ -108,7 +108,7 @@
              route={{
                resource: "project.issues",
                project: projectId,
-
                seed: baseUrl,
+
                node: baseUrl,
                state: option.value,
              }}>
              <SquareButton
@@ -134,7 +134,7 @@
        route={{
          resource: "project.newIssue",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
        }}>
        <SquareButton>New issue</SquareButton>
      </Link>
modified src/views/projects/Patch.svelte
@@ -131,7 +131,7 @@
    const baseRoute = {
      resource: "project.patch",
      project: projectId,
-
      seed: baseUrl,
+
      node: baseUrl,
      patch: patch.id,
    } as const;
    // For cleaner URLs, we omit the the revision part when we link to the
@@ -364,7 +364,7 @@
            route={{
              resource: "project.patch",
              project: projectId,
-
              seed: baseUrl,
+
              node: baseUrl,
              patch: patch.id,
              view: {
                name: "diff",
@@ -400,7 +400,7 @@
                  route={{
                    resource: "project.patch",
                    project: projectId,
-
                    seed: baseUrl,
+
                    node: baseUrl,
                    patch: patch.id,
                    view: {
                      name: view.view.name,
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -130,7 +130,7 @@
        route={{
          resource: "project.patch",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          patch: patch.id,
        }}>
        <span class="patch-title">
modified src/views/projects/Patches.svelte
@@ -117,7 +117,7 @@
            route={{
              resource: "project.patches",
              project: projectId,
-
              seed: baseUrl,
+
              node: baseUrl,
              search: `state=${option.value}`,
            }}>
            <SquareButton
modified src/views/projects/ProjectMeta.svelte
@@ -89,7 +89,7 @@
        route={{
          resource: "project.tree",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
        }}>
        <span class="project-name">
          {projectName}
modified src/views/projects/Readme.svelte
@@ -25,7 +25,7 @@
        routeToPath({
          resource: "project.tree",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          peer,
          revision,
          path: "README.md",
modified src/views/projects/SourceBrowser/FileDiff.svelte
@@ -341,7 +341,7 @@
        route={{
          resource: "project.tree",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          path: file.path,
          revision,
        }}>
modified src/views/projects/SourceBrowser/FileLocationChange.svelte
@@ -55,7 +55,7 @@
        route={{
          resource: "project.tree",
          project: projectId,
-
          seed: baseUrl,
+
          node: baseUrl,
          path: file.newPath,
          revision,
        }}>
modified src/views/projects/SourceBrowsingHeader.svelte
@@ -9,7 +9,7 @@
  import Link from "@app/components/Link.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";

-
  export let seed: BaseUrl;
+
  export let node: BaseUrl;
  export let branches: Array<{ name: string; route: Route }>;
  export let peers: Array<{ remote: Remote; selected: boolean; route: Route }>;
  export let historyLinkActive: boolean;
@@ -64,7 +64,7 @@
    route={{
      resource: "project.history",
      project: project.id,
-
      seed: seed,
+
      node: node,
      peer,
      revision,
    }}>
modified src/views/projects/Tree.svelte
@@ -38,7 +38,7 @@
      route={{
        resource: "project.tree",
        project: projectId,
-
        seed: baseUrl,
+
        node: baseUrl,
        path: entry.path,
        peer,
        revision,
modified src/views/projects/Tree/Folder.svelte
@@ -100,7 +100,7 @@
              route={{
                resource: "project.tree",
                project: projectId,
-
                seed: baseUrl,
+
                node: baseUrl,
                path: entry.path,
                peer,
                revision,
modified src/views/projects/router.ts
@@ -17,7 +17,7 @@ import type {
import { HttpdClient } from "@httpd-client";
import * as Syntax from "@app/lib/syntax";
import { unreachable } from "@app/lib/utils";
-
import { seedPath } from "@app/views/seeds/router";
+
import { nodePath } from "@app/views/nodes/router";

export const COMMITS_PER_PAGE = 30;
export const PATCHES_PER_PAGE = 10;
@@ -28,15 +28,15 @@ export type ProjectRoute =
  | ProjectHistoryRoute
  | {
      resource: "project.commit";
-
      seed: BaseUrl;
+
      node: BaseUrl;
      project: string;
      commit: string;
    }
  | ProjectIssuesRoute
-
  | { resource: "project.newIssue"; seed: BaseUrl; project: string }
+
  | { resource: "project.newIssue"; node: BaseUrl; project: string }
  | {
      resource: "project.issue";
-
      seed: BaseUrl;
+
      node: BaseUrl;
      project: string;
      issue: string;
    }
@@ -45,14 +45,14 @@ export type ProjectRoute =

interface ProjectIssuesRoute {
  resource: "project.issues";
-
  seed: BaseUrl;
+
  node: BaseUrl;
  project: string;
  state?: "open" | "closed";
}

interface ProjectTreeRoute {
  resource: "project.tree";
-
  seed: BaseUrl;
+
  node: BaseUrl;
  project: string;
  path?: string;
  peer?: string;
@@ -62,7 +62,7 @@ interface ProjectTreeRoute {

interface ProjectHistoryRoute {
  resource: "project.history";
-
  seed: BaseUrl;
+
  node: BaseUrl;
  project: string;
  peer?: string;
  revision?: string;
@@ -70,7 +70,7 @@ interface ProjectHistoryRoute {

interface ProjectPatchRoute {
  resource: "project.patch";
-
  seed: BaseUrl;
+
  node: BaseUrl;
  project: string;
  patch: string;
  view?:
@@ -90,7 +90,7 @@ interface ProjectPatchRoute {

interface ProjectPatchesRoute {
  resource: "project.patches";
-
  seed: BaseUrl;
+
  node: BaseUrl;
  project: string;
  search?: string;
}
@@ -197,7 +197,7 @@ function parseRevisionToOid(
export async function loadProjectRoute(
  route: ProjectRoute,
): Promise<ProjectLoadedRoute | LoadError> {
-
  const api = new HttpdClient(route.seed);
+
  const api = new HttpdClient(route.node);
  try {
    if (route.resource === "project.tree") {
      return loadTreeView(route);
@@ -213,7 +213,7 @@ export async function loadProjectRoute(
        resource: "projects",
        params: {
          id: route.project,
-
          baseUrl: route.seed,
+
          baseUrl: route.node,
          project,
          view: {
            resource: "commit",
@@ -231,7 +231,7 @@ export async function loadProjectRoute(
          resource: "projects",
          params: {
            id: route.project,
-
            baseUrl: route.seed,
+
            baseUrl: route.node,
            project,
            view: {
              resource: "issue",
@@ -259,7 +259,7 @@ export async function loadProjectRoute(
        resource: "projects",
        params: {
          id: route.project,
-
          baseUrl: route.seed,
+
          baseUrl: route.node,
          view: {
            resource: "newIssue",
          },
@@ -286,7 +286,7 @@ export async function loadProjectRoute(
async function loadPatchesView(
  route: ProjectPatchesRoute,
): Promise<ProjectLoadedRoute> {
-
  const api = new HttpdClient(route.seed);
+
  const api = new HttpdClient(route.node);
  const searchParams = new URLSearchParams(route.search || "");
  const state = (searchParams.get("state") as PatchState["status"]) || "open";

@@ -303,7 +303,7 @@ async function loadPatchesView(
    resource: "projects",
    params: {
      id: route.project,
-
      baseUrl: route.seed,
+
      baseUrl: route.node,
      view: {
        resource: "patches",
        patches,
@@ -317,7 +317,7 @@ async function loadPatchesView(
async function loadIssuesView(
  route: ProjectIssuesRoute,
): Promise<ProjectLoadedRoute> {
-
  const api = new HttpdClient(route.seed);
+
  const api = new HttpdClient(route.node);
  const state = route.state || "open";

  const [project, issues] = await Promise.all([
@@ -333,7 +333,7 @@ async function loadIssuesView(
    resource: "projects",
    params: {
      id: route.project,
-
      baseUrl: route.seed,
+
      baseUrl: route.node,
      view: {
        resource: "issues",
        issues,
@@ -347,7 +347,7 @@ async function loadIssuesView(
async function loadTreeView(
  route: ProjectTreeRoute,
): Promise<ProjectLoadedRoute> {
-
  const api = new HttpdClient(route.seed);
+
  const api = new HttpdClient(route.node);

  const [project, peers, branchMap] = await Promise.all([
    api.project.getById(route.project),
@@ -380,7 +380,7 @@ async function loadTreeView(
    resource: "projects",
    params: {
      id: route.project,
-
      baseUrl: route.seed,
+
      baseUrl: route.node,
      project,
      view: {
        resource: "tree",
@@ -439,7 +439,7 @@ async function loadBlob(
async function loadHistoryView(
  route: ProjectHistoryRoute,
): Promise<ProjectLoadedRoute> {
-
  const api = new HttpdClient(route.seed);
+
  const api = new HttpdClient(route.node);

  const [project, peers, branchMap] = await Promise.all([
    api.project.getById(route.project),
@@ -475,7 +475,7 @@ async function loadHistoryView(
    resource: "projects",
    params: {
      id: route.project,
-
      baseUrl: route.seed,
+
      baseUrl: route.node,
      project,
      view: {
        resource: "history",
@@ -494,7 +494,7 @@ async function loadHistoryView(
async function loadPatchView(
  route: ProjectPatchRoute,
): Promise<ProjectLoadedRoute> {
-
  const api = new HttpdClient(route.seed);
+
  const api = new HttpdClient(route.node);
  const [project, patch] = await Promise.all([
    api.project.getById(route.project),
    api.project.getPatchById(route.project, route.patch),
@@ -547,7 +547,7 @@ async function loadPatchView(
    resource: "projects",
    params: {
      id: route.project,
-
      baseUrl: route.seed,
+
      baseUrl: route.node,
      project,
      view: {
        resource: "patch",
@@ -604,7 +604,7 @@ function sanitizeQueryString(queryString: string): string {
}

export function resolveProjectRoute(
-
  seed: BaseUrl,
+
  node: BaseUrl,
  project: string,
  segments: string[],
  urlSearch: string,
@@ -619,7 +619,7 @@ export function resolveProjectRoute(
  if (!content || content === "tree") {
    return {
      resource: "project.tree",
-
      seed,
+
      node,
      project,
      peer,
      path: undefined,
@@ -629,7 +629,7 @@ export function resolveProjectRoute(
  } else if (content === "history") {
    return {
      resource: "project.history",
-
      seed,
+
      node,
      project,
      peer,
      revision: segments.join("/"),
@@ -637,7 +637,7 @@ export function resolveProjectRoute(
  } else if (content === "commits") {
    return {
      resource: "project.commit",
-
      seed,
+
      node,
      project,
      commit: segments[0],
    };
@@ -646,13 +646,13 @@ export function resolveProjectRoute(
    if (issueOrAction === "new") {
      return {
        resource: "project.newIssue",
-
        seed,
+
        node,
        project,
      };
    } else if (issueOrAction) {
      return {
        resource: "project.issue",
-
        seed,
+
        node,
        project,
        issue: issueOrAction,
      };
@@ -666,20 +666,20 @@ export function resolveProjectRoute(
      }
      return {
        resource: "project.issues",
-
        seed,
+
        node,
        project,
        state,
      };
    }
  } else if (content === "patches") {
-
    return resolvePatchesRoute(seed, project, segments, urlSearch);
+
    return resolvePatchesRoute(node, project, segments, urlSearch);
  } else {
    return null;
  }
}

function resolvePatchesRoute(
-
  seed: BaseUrl,
+
  node: BaseUrl,
  project: string,
  segments: string[],
  urlSearch: string,
@@ -691,7 +691,7 @@ function resolvePatchesRoute(
    const tab = searchParams.get("tab");
    const base = {
      resource: "project.patch",
-
      seed,
+
      node,
      project,
      patch,
    } as const;
@@ -722,7 +722,7 @@ function resolvePatchesRoute(
  } else {
    return {
      resource: "project.patches",
-
      seed,
+
      node,
      project,
      search: sanitizeQueryString(urlSearch),
    };
@@ -730,9 +730,9 @@ function resolvePatchesRoute(
}

export function projectRouteToPath(route: ProjectRoute): string {
-
  const seed = seedPath(route.seed);
+
  const node = nodePath(route.node);

-
  const pathSegments = [seed, route.project];
+
  const pathSegments = [node, route.project];

  if (route.resource === "project.tree") {
    if (route.peer) {
@@ -802,9 +802,9 @@ export function projectRouteToPath(route: ProjectRoute): string {
}

function patchRouteToPath(route: ProjectPatchRoute): string {
-
  const seed = seedPath(route.seed);
+
  const node = nodePath(route.node);

-
  const pathSegments = [seed, route.project];
+
  const pathSegments = [node, route.project];

  pathSegments.push("patches", route.patch);
  if (route.view?.name === "commits" || route.view?.name === "files") {
deleted src/views/seeds/View.svelte
@@ -1,153 +0,0 @@
-
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
-
  import type { ProjectActivity } from "@app/views/seeds/router";
-

-
  import { config } from "@app/lib/config";
-
  import { isLocal, truncateId } from "@app/lib/utils";
-
  import { loadProjects } from "@app/views/seeds/router";
-

-
  import Button from "@app/components/Button.svelte";
-
  import Clipboard from "@app/components/Clipboard.svelte";
-
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import Loading from "@app/components/Loading.svelte";
-
  import ProjectCard from "@app/components/ProjectCard.svelte";
-

-
  export let baseUrl: BaseUrl;
-
  export let nid: string;
-
  export let projectCount: number;
-
  export let projectPageIndex: number;
-
  export let projects: ProjectActivity[] = [];
-
  export let version: string;
-

-
  let error: any;
-
  let loadingProjects = false;
-

-
  async function loadMore(): Promise<void> {
-
    loadingProjects = true;
-
    try {
-
      const result = await loadProjects(projectPageIndex, baseUrl);
-
      projectCount = result.total;
-
      projects = [...projects, ...result.projects];
-
      projectPageIndex += 1;
-
    } catch (err) {
-
      error = err;
-
    } finally {
-
      loadingProjects = false;
-
    }
-
  }
-

-
  $: hostname = isLocal(baseUrl.hostname) ? "radicle.local" : baseUrl.hostname;
-
  $: showMoreButton =
-
    !loadingProjects &&
-
    !error &&
-
    projectCount &&
-
    projects.length < projectCount;
-
</script>
-

-
<style>
-
  .wrapper {
-
    width: 720px;
-
    margin: 5rem 0;
-
  }
-
  .header {
-
    align-items: center;
-
    color: var(--color-secondary);
-
    display: flex;
-
    flex-direction: row;
-
    font-size: var(--font-size-large);
-
    font-weight: var(--font-weight-bold);
-
    justify-content: space-between;
-
    margin-bottom: 2rem;
-
    overflow-x: hidden;
-
    text-align: left;
-
    text-overflow: ellipsis;
-
    width: 100%;
-
  }
-
  table {
-
    border-collapse: collapse;
-
  }
-
  td {
-
    padding-bottom: 1.5rem;
-
    padding-right: 3rem;
-
  }
-
  .seed-address {
-
    display: flex;
-
    align-items: center;
-
    color: var(--color-foreground-6);
-
    white-space: nowrap;
-
  }
-
  .more {
-
    margin-top: 2rem;
-
    text-align: center;
-
  }
-
  @media (max-width: 720px) {
-
    .wrapper {
-
      width: 100%;
-
      padding: 1.5rem;
-
    }
-
  }
-
</style>
-

-
<div class="wrapper">
-
  <div class="header">
-
    {hostname}
-
  </div>
-

-
  <table>
-
    <tr>
-
      <td class="txt-highlight">Address</td>
-
      <td>
-
        <div class="seed-address">
-
          {truncateId(nid)}@{baseUrl.hostname}
-
          <Clipboard
-
            small
-
            text={`${nid}@${baseUrl.hostname}:${config.seeds.defaultNodePort}`} />
-
        </div>
-
      </td>
-
    </tr>
-
    <tr>
-
      <td class="txt-highlight">Version</td>
-
      <td>
-
        {version}
-
      </td>
-
    </tr>
-
  </table>
-

-
  <div style:margin-bottom="5rem">
-
    <div style:margin-top="1rem">
-
      {#each projects as { project, activity }}
-
        <div style:margin-bottom="0.5rem">
-
          <Link
-
            route={{
-
              resource: "project.tree",
-
              project: project.id,
-
              seed: baseUrl,
-
            }}>
-
            <ProjectCard
-
              {activity}
-
              id={project.id}
-
              name={project.name}
-
              description={project.description}
-
              head={project.head} />
-
          </Link>
-
        </div>
-
      {/each}
-
    </div>
-
    {#if loadingProjects}
-
      <div class="more">
-
        <Loading small />
-
      </div>
-
    {/if}
-
    {#if showMoreButton}
-
      <div class="more">
-
        <Button variant="foreground" on:click={loadMore}>More</Button>
-
      </div>
-
    {/if}
-
    {#if error}
-
      <ErrorMessage
-
        message="Not able to load more projects from this seed."
-
        stackTrace={error.stack} />
-
    {/if}
-
  </div>
-
</div>
deleted src/views/seeds/router.ts
@@ -1,113 +0,0 @@
-
import type { BaseUrl, Project } from "@httpd-client";
-
import type { LoadError } from "@app/lib/router/definitions";
-
import type { WeeklyActivity } from "@app/lib/commit";
-

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

-
interface SeedsRouteParams {
-
  baseUrl: BaseUrl;
-
  projectPageIndex: number;
-
}
-

-
export interface ProjectActivity {
-
  project: Project;
-
  activity: WeeklyActivity[];
-
}
-

-
export interface SeedsRoute {
-
  resource: "seeds";
-
  params: SeedsRouteParams;
-
}
-

-
export interface SeedsLoadedRoute {
-
  resource: "seeds";
-
  params: {
-
    baseUrl: BaseUrl;
-
    projectPageIndex: number;
-
    version: string;
-
    nid: string;
-
    projects: ProjectActivity[];
-
    projectCount: number;
-
  };
-
}
-

-
const PROJECTS_PER_PAGE = 10;
-

-
export async function loadProjects(
-
  page: number,
-
  baseUrl: BaseUrl,
-
): Promise<{
-
  total: number;
-
  projects: ProjectActivity[];
-
}> {
-
  const api = new HttpdClient(baseUrl);
-

-
  const [nodeStats, projects] = await Promise.all([
-
    api.getStats(),
-
    api.project.getAll({ page, perPage: PROJECTS_PER_PAGE }),
-
  ]);
-

-
  const results = await Promise.all(
-
    projects.map(async project => {
-
      const activity = await loadProjectActivity(project.id, baseUrl);
-
      return {
-
        project,
-
        activity,
-
      };
-
    }),
-
  );
-
  // Sorts projects by most recent commit descending.
-
  const sortedProjects = results.sort(
-
    (a, b) => b.activity[0].time - a.activity[0].time,
-
  );
-

-
  return {
-
    total: nodeStats.projects.count,
-
    projects: sortedProjects,
-
  };
-
}
-

-
export function seedPath(baseUrl: BaseUrl) {
-
  const port = baseUrl.port ?? config.seeds.defaultHttpdPort;
-

-
  if (port === config.seeds.defaultHttpdPort) {
-
    return `/seeds/${baseUrl.hostname}`;
-
  } else {
-
    return `/seeds/${baseUrl.hostname}:${port}`;
-
  }
-
}
-

-
export async function loadSeedRoute(
-
  params: SeedsRouteParams,
-
): Promise<SeedsLoadedRoute | LoadError> {
-
  const api = new HttpdClient(params.baseUrl);
-
  try {
-
    const projectPageIndex = 0;
-
    const [nodeInfo, { projects, total }] = await Promise.all([
-
      api.getNodeInfo(),
-
      loadProjects(projectPageIndex, params.baseUrl),
-
    ]);
-
    return {
-
      resource: "seeds",
-
      params: {
-
        projectPageIndex: projectPageIndex + 1,
-
        baseUrl: params.baseUrl,
-
        nid: nodeInfo.node.id,
-
        version: nodeInfo.version,
-
        projects: projects,
-
        projectCount: total,
-
      },
-
    };
-
  } catch (error: any) {
-
    return {
-
      resource: "loadError",
-
      params: {
-
        title: `${params.baseUrl.hostname}:${params.baseUrl.port}`,
-
        errorMessage: "Not able to query this seed.",
-
        stackTrace: error.stack,
-
      },
-
    };
-
  }
-
}
modified src/views/session/Index.svelte
@@ -19,7 +19,7 @@
    if (isAuthenticated) {
      modal.show({ component: AuthenticatedModal, props: {} });
      void router.push({
-
        resource: "seeds",
+
        resource: "nodes",
        params: {
          baseUrl: httpd.api.baseUrl,
          projectPageIndex: 0,
modified tests/e2e/clipboard.spec.ts
@@ -4,7 +4,7 @@ import {
  expect,
  sourceBrowsingUrl,
  sourceBrowsingRid,
-
  seedRemote,
+
  nodeRemote,
  test,
} from "@tests/support/fixtures.js";

@@ -61,11 +61,11 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
    );
  }

-
  await page.goto("/seeds/radicle.local");
-
  // Seed address.
+
  await page.goto("/nodes/radicle.local");
+
  // Node address.
  {
    await page.locator(".clipboard").first().click();
-
    await expectClipboard(`${seedRemote}@127.0.0.1:8776`, page);
+
    await expectClipboard(`${nodeRemote}@127.0.0.1:8776`, page);
  }

  // Clear the system clipboard contents so developers don't wonder why there's
modified tests/e2e/hashRouter.spec.ts
@@ -24,18 +24,18 @@ test("navigate between landing and project page", async ({ page }) => {
  await expectUrlPersistsReload(page);
});

-
test("navigation between seed and project pages", async ({ page }) => {
-
  await page.goto("/#/seeds/radicle.local");
+
test("navigation between node and project pages", async ({ page }) => {
+
  await page.goto("/#/nodes/radicle.local");

  const project = page.locator(".project", { hasText: "source-browsing" });
  await project.click();
  await expect(page).toHaveURL(`/#${sourceBrowsingUrl}`);

-
  await expectBackAndForwardNavigationWorks("/#/seeds/radicle.local", page);
+
  await expectBackAndForwardNavigationWorks("/#/nodes/radicle.local", page);
  await expectUrlPersistsReload(page);

  await page.getByRole("link", { name: "radicle.local" }).click();
-
  await expect(page).toHaveURL("/#/seeds/127.0.0.1");
+
  await expect(page).toHaveURL("/#/nodes/127.0.0.1");
});

test.describe("project page navigation", () => {
modified tests/e2e/historyRouter.spec.ts
@@ -24,18 +24,18 @@ test("navigate between landing and project page", async ({ page }) => {
  await expectUrlPersistsReload(page);
});

-
test("navigation between seed and project pages", async ({ page }) => {
-
  await page.goto("/seeds/radicle.local");
+
test("navigation between node and project pages", async ({ page }) => {
+
  await page.goto("/nodes/radicle.local");

  const project = page.locator(".project", { hasText: "source-browsing" });
  await project.click();
  await expect(page).toHaveURL(sourceBrowsingUrl);

-
  await expectBackAndForwardNavigationWorks("/seeds/radicle.local", page);
+
  await expectBackAndForwardNavigationWorks("/nodes/radicle.local", page);
  await expectUrlPersistsReload(page);

  await page.getByRole("link", { name: "radicle.local" }).click();
-
  await expect(page).toHaveURL("/seeds/127.0.0.1");
+
  await expect(page).toHaveURL("/nodes/127.0.0.1");
});

test.describe("project page navigation", () => {
added tests/e2e/node.spec.ts
@@ -0,0 +1,40 @@
+
import {
+
  aliceMainHead,
+
  expect,
+
  sourceBrowsingRid,
+
  nodeRemote,
+
  test,
+
} from "@tests/support/fixtures.js";
+

+
test("node metadata", async ({ page }) => {
+
  await page.goto("/nodes/radicle.local");
+

+
  await expect(
+
    page.locator(".header").getByText("radicle.local"),
+
  ).toBeVisible();
+
  await expect(
+
    page.getByText(`${nodeRemote.substring(0, 6)}…${nodeRemote.slice(-6)}`),
+
  ).toBeVisible();
+
  await expect(page.getByText("0.1.0-")).toBeVisible();
+
});
+

+
test("node projects", async ({ page }) => {
+
  await page.goto("/nodes/radicle.local");
+
  const project = page.locator(".project", { hasText: "source-browsing" });
+

+
  // Project metadata.
+
  {
+
    await expect(project.getByText("source-browsing")).toBeVisible();
+
    await expect(
+
      project.getByText("Git repository for source browsing tests"),
+
    ).toBeVisible();
+
    await expect(project.getByText(aliceMainHead)).toBeVisible();
+
  }
+

+
  // Show project ID on hover.
+
  {
+
    await expect(project.getByText(sourceBrowsingRid)).not.toBeVisible();
+
    await project.hover();
+
    await expect(project.getByText(sourceBrowsingRid)).toBeVisible();
+
  }
+
});
modified tests/e2e/project/issues.spec.ts
@@ -97,7 +97,7 @@ test("go through the entire ui issue flow", async ({
  const { rid } = await createProject(authenticatedPeer, "commenting");

  await page.goto(
-
    `/seeds/${authenticatedPeer.httpdBaseUrl.hostname}:${authenticatedPeer.httpdBaseUrl.port}/${rid}`,
+
    `/nodes/${authenticatedPeer.httpdBaseUrl.hostname}:${authenticatedPeer.httpdBaseUrl.port}/${rid}`,
  );
  await page.getByRole("link", { name: "0 issues" }).click();
  await page.getByRole("link", { name: "New issue" }).click();
modified tests/e2e/project/patches.spec.ts
@@ -1,7 +1,7 @@
import { test, cobUrl, expect } from "@tests/support/fixtures.js";
import { createProject } from "@tests/support/project";

-
test("navigate listing", async ({ page }) => {
+
test("navigate patch listing", async ({ page }) => {
  await page.goto(cobUrl);
  await page.getByRole("link", { name: "2 patches" }).click();
  await expect(page).toHaveURL(`${cobUrl}/patches`);
deleted tests/e2e/seed.spec.ts
@@ -1,40 +0,0 @@
-
import {
-
  aliceMainHead,
-
  expect,
-
  sourceBrowsingRid,
-
  seedRemote,
-
  test,
-
} from "@tests/support/fixtures.js";
-

-
test("seed metadata", async ({ page }) => {
-
  await page.goto("/seeds/radicle.local");
-

-
  await expect(
-
    page.locator(".header").getByText("radicle.local"),
-
  ).toBeVisible();
-
  await expect(
-
    page.getByText(`${seedRemote.substring(0, 6)}…${seedRemote.slice(-6)}`),
-
  ).toBeVisible();
-
  await expect(page.getByText("0.1.0-")).toBeVisible();
-
});
-

-
test("seed projects", async ({ page }) => {
-
  await page.goto("/seeds/radicle.local");
-
  const project = page.locator(".project", { hasText: "source-browsing" });
-

-
  // Project metadata.
-
  {
-
    await expect(project.getByText("source-browsing")).toBeVisible();
-
    await expect(
-
      project.getByText("Git repository for source browsing tests"),
-
    ).toBeVisible();
-
    await expect(project.getByText(aliceMainHead)).toBeVisible();
-
  }
-

-
  // Show project ID on hover.
-
  {
-
    await expect(project.getByText(sourceBrowsingRid)).not.toBeVisible();
-
    await project.hover();
-
    await expect(project.getByText(sourceBrowsingRid)).toBeVisible();
-
  }
-
});
modified tests/support/fixtures.ts
@@ -92,7 +92,7 @@ export const test = base.extend<{
        await page.addInitScript(() => {
          window.APP_CONFIG = {
            reactions: [],
-
            seeds: {
+
            nodes: {
              defaultHttpdPort: 8081,
              defaultLocalHttpdPort: 8081,
              defaultHttpdScheme: "http",
@@ -230,7 +230,7 @@ function log(text: string, label: string, outputLog: Stream.Writable) {
export function appConfigWithFixture() {
  window.APP_CONFIG = {
    reactions: [],
-
    seeds: {
+
    nodes: {
      defaultHttpdPort: 8081,
      defaultLocalHttpdPort: 8081,
      defaultHttpdScheme: "http",
@@ -637,10 +637,10 @@ export const bobHead = "28f37105bb78db48111e36281291ff253dd050e8";
export const sourceBrowsingRid = "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir";
export const cobRid = "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe";
export const markdownRid = "rad:z2tchH2Ti4LxRKdssPQYs6VHE5rsg";
-
export const sourceBrowsingUrl = `/seeds/127.0.0.1/${sourceBrowsingRid}`;
-
export const cobUrl = `/seeds/127.0.0.1/${cobRid}`;
-
export const markdownUrl = `/seeds/127.0.0.1/${markdownRid}`;
-
export const seedRemote = "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S";
+
export const sourceBrowsingUrl = `/nodes/127.0.0.1/${sourceBrowsingRid}`;
+
export const cobUrl = `/nodes/127.0.0.1/${cobRid}`;
+
export const markdownUrl = `/nodes/127.0.0.1/${markdownRid}`;
+
export const nodeRemote = "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S";
export const defaultHttpdPort = 8081;
export const gitOptions = {
  alice: {
modified tests/support/peerManager.ts
@@ -55,7 +55,7 @@ export interface RoutingEntry {

interface PeerManagerParams {
  dataPath: string;
-
  seed: string;
+
  node: string;
  name: string;
  gitOptions?: Record<string, string>;
  outputLog: Stream.Writable;
@@ -68,7 +68,7 @@ export interface PeerManager {
  }): Promise<RadiclePeer>;
}

-
export function generateSeed(index: number) {
+
export function generateNode(index: number) {
  return Array(64).fill(index.toString()).join("");
}

@@ -95,7 +95,7 @@ export async function createPeerManager(createParams: {
        dataPath: createParams.dataDir,
        name: params.name,
        gitOptions: params.gitOptions,
-
        seed: generateSeed(nodes.length + 1),
+
        node: generateNode(nodes.length + 1),
        outputLog,
      });
      nodes.push(peer);
@@ -109,7 +109,7 @@ export class RadiclePeer {
  public checkoutPath: string;
  public nodeId: string;

-
  #seed: string;
+
  #node: string;
  #socket: string;
  #radHome: string;
  #eventRecords: NodeEvent[] = [];
@@ -123,7 +123,7 @@ export class RadiclePeer {
  private constructor(props: {
    checkoutPath: string;
    nodeId: string;
-
    seed: string;
+
    node: string;
    socket: string;
    gitOptions?: Record<string, string>;
    radHome: string;
@@ -132,7 +132,7 @@ export class RadiclePeer {
    this.checkoutPath = props.checkoutPath;
    this.nodeId = props.nodeId;
    this.#gitOptions = props.gitOptions;
-
    this.#seed = props.seed;
+
    this.#node = props.node;
    this.#socket = props.socket;
    this.#radHome = props.radHome;
    this.#outputLog = props.logFile;
@@ -151,7 +151,7 @@ export class RadiclePeer {
    dataPath,
    name,
    gitOptions,
-
    seed,
+
    node,
    outputLog: logFile,
  }: PeerManagerParams): Promise<RadiclePeer> {
    const checkoutPath = Path.join(dataPath, name, "copy");
@@ -168,7 +168,7 @@ export class RadiclePeer {
      ...gitOptions,
      RAD_HOME: radHome,
      RAD_PASSPHRASE: "asdf",
-
      RAD_SEED: seed,
+
      RAD_SEED: node,
      RAD_SOCKET: socket,
    };

@@ -178,7 +178,7 @@ export class RadiclePeer {
    return new RadiclePeer({
      checkoutPath,
      gitOptions,
-
      seed,
+
      node,
      socket,
      nodeId,
      radHome,
@@ -335,11 +335,11 @@ export class RadiclePeer {
      throw new Error("No httpd service running");
    }

-
    return `/seeds/${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`;
+
    return `/nodes/${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`;
  }

  public ridUrl(rid: string): string {
-
    return `/seeds/${this.httpdBaseUrl.hostname}:${this.httpdBaseUrl.port}/${rid}`;
+
    return `/nodes/${this.httpdBaseUrl.hostname}:${this.httpdBaseUrl.port}/${rid}`;
  }

  public get httpdBaseUrl(): BaseUrl {
@@ -371,7 +371,7 @@ export class RadiclePeer {
        RAD_HOME: this.#radHome,
        RAD_PASSPHRASE: "asdf",
        RAD_COMMIT_TIME: "1671125284",
-
        RAD_SEED: this.#seed,
+
        RAD_SEED: this.#node,
        RAD_SOCKET: this.#socket,
        ...opts?.env,
        ...this.#gitOptions,
modified tests/support/process.ts
@@ -8,7 +8,7 @@ import { execa } from "execa";
import { make } from "./logLabel.js";

// Processes that should be SIGKILLed when the Node process shutsdown.
-
// We add all proxy and seed instances that we spawn to this list.
+
// We add all proxy and node instances that we spawn to this list.
const processes: ExecaChildProcess[] = [];

onExit(killAllProcesses);
modified tests/unit/router.test.ts
@@ -7,7 +7,7 @@ window.origin = "http://localhost:3000";

describe("route invariant when parsed", () => {
  const origin = "http://localhost:3000";
-
  const seed = {
+
  const node = {
    hostname: "willow.radicle.garden",
    port: 8000,
    scheme: "http",
@@ -16,21 +16,21 @@ describe("route invariant when parsed", () => {
  test("home", () => {
    return expectParsingInvariant({ resource: "home" });
  });
-
  test("seeds", () => {
+
  test("nodes", () => {
    expectParsingInvariant({
-
      resource: "seeds",
+
      resource: "nodes",
      params: {
        // TODO: This only works with the value 0. The value is not actually
        // extract.
        projectPageIndex: 0,
-
        baseUrl: seed,
+
        baseUrl: node,
      },
    });
  });
  test("projects.tree", () => {
    expectParsingInvariant({
      resource: "project.tree",
-
      seed,
+
      node,
      project: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
      route: "",
    });
@@ -39,7 +39,7 @@ describe("route invariant when parsed", () => {
  test("projects.tree with peer", () => {
    expectParsingInvariant({
      resource: "project.tree",
-
      seed,
+
      node,
      project: "PROJECT",
      peer: "PEER",
      route: "",
@@ -49,7 +49,7 @@ describe("route invariant when parsed", () => {
  test("projects.tree with peer and revision", () => {
    const route: Route = {
      resource: "project.tree",
-
      seed,
+
      node,
      project: "PROJECT",
      peer: "PEER",
      revision: "REVISION",
@@ -64,7 +64,7 @@ describe("route invariant when parsed", () => {
  test("projects.tree with peer and revision and path", () => {
    const route: Route = {
      resource: "project.tree",
-
      seed,
+
      node,
      project: "PROJECT",
      peer: "PEER",
      path: "PATH",
@@ -81,7 +81,7 @@ describe("route invariant when parsed", () => {
  test("projects.history", () => {
    expectParsingInvariant({
      resource: "project.history",
-
      seed,
+
      node,
      project: "PROJECT",
      revision: "",
    });
@@ -90,7 +90,7 @@ describe("route invariant when parsed", () => {
  test("projects.history with revision", () => {
    expectParsingInvariant({
      resource: "project.history",
-
      seed,
+
      node,
      project: "PROJECT",
      revision: "REVISION",
    });
@@ -99,7 +99,7 @@ describe("route invariant when parsed", () => {
  test("projects.commits", () => {
    expectParsingInvariant({
      resource: "project.commit",
-
      seed,
+
      node,
      project: "PROJECT",
      commit: "COMMIT",
    });
@@ -108,7 +108,7 @@ describe("route invariant when parsed", () => {
  test("projects.issues", () => {
    expectParsingInvariant({
      resource: "project.issues",
-
      seed,
+
      node,
      project: "PROJECT",
    });
  });
@@ -116,7 +116,7 @@ describe("route invariant when parsed", () => {
  test("projects.issues with state", () => {
    expectParsingInvariant({
      resource: "project.issues",
-
      seed,
+
      node,
      project: "PROJECT",
      state: "closed",
    });
@@ -125,7 +125,7 @@ describe("route invariant when parsed", () => {
  test("projects.newIssue", () => {
    expectParsingInvariant({
      resource: "project.newIssue",
-
      seed,
+
      node,
      project: "PROJECT",
    });
  });
@@ -133,7 +133,7 @@ describe("route invariant when parsed", () => {
  test("projects.issue", () => {
    expectParsingInvariant({
      resource: "project.issue",
-
      seed,
+
      node,
      project: "PROJECT",
      issue: "ISSUE",
    });
@@ -143,7 +143,7 @@ describe("route invariant when parsed", () => {
    () => {
      expectParsingInvariant({
        resource: "project.patches",
-
        seed,
+
        node,
        project: "PROJECT",
      });
    };
@@ -151,7 +151,7 @@ describe("route invariant when parsed", () => {
  test("projects.patches with search", () => {
    expectParsingInvariant({
      resource: "project.patches",
-
      seed,
+
      node,
      project: "PROJECT",
      search: "SEARCH",
    });
@@ -160,7 +160,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch default view", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
    });
@@ -169,7 +169,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch activity", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
      view: { name: "activity" },
@@ -179,7 +179,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch commits", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
      view: { name: "commits" },
@@ -189,7 +189,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch commits with revision", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
      view: { name: "commits", revision: "REVISION" },
@@ -199,7 +199,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch files", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
      view: { name: "files" },
@@ -209,7 +209,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch files with revision", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
      view: { name: "files", revision: "REVISION" },
@@ -219,7 +219,7 @@ describe("route invariant when parsed", () => {
  test("projects.patch diff", () => {
    expectParsingInvariant({
      resource: "project.patch",
-
      seed,
+
      node,
      project: "PROJECT",
      patch: "PATCH",
      view: {
@@ -246,9 +246,9 @@ describe("pathToRoute", () => {
    expectPathToRoute("/", { resource: "home" });
  });

-
  test("seeds", () => {
-
    expectPathToRoute("/seeds/willow.radicle.garden", {
-
      resource: "seeds",
+
  test("nodes", () => {
+
    expectPathToRoute("/nodes/willow.radicle.garden", {
+
      resource: "nodes",
      params: {
        baseUrl: {
          hostname: "willow.radicle.garden",
@@ -262,10 +262,10 @@ describe("pathToRoute", () => {

  test("project with trailing slash", () => {
    expectPathToRoute(
-
      "/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/",
+
      "/nodes/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/",
      {
        resource: "project.tree",
-
        seed: {
+
        node: {
          hostname: "willow.radicle.garden",
          scheme: "http",
          port: defaultHttpdPort,
@@ -278,10 +278,10 @@ describe("pathToRoute", () => {

  test("project without trailing slash", () => {
    expectPathToRoute(
-
      "/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
+
      "/nodes/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
      {
        resource: "project.tree",
-
        seed: {
+
        node: {
          hostname: "willow.radicle.garden",
          scheme: "http",
          port: defaultHttpdPort,
@@ -294,7 +294,7 @@ describe("pathToRoute", () => {

  test("non-existent project route", () => {
    expectPathToRoute(
-
      "/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/nope",
+
      "/nodes/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/nope",
      null,
    );
  });
modified tests/visual/seed.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "@tests/support/fixtures.js";

-
test("seed page", async ({ page }) => {
+
test("node page", async ({ page }) => {
  await page.addInitScript(() => {
    window.initializeTestStubs = () => {
      window.e2eTestStubs.FakeTimers.install({
@@ -11,6 +11,6 @@ test("seed page", async ({ page }) => {
    };
  });

-
  await page.goto("/seeds/radicle.local", { waitUntil: "networkidle" });
+
  await page.goto("/nodes/radicle.local", { waitUntil: "networkidle" });
  await expect(page).toHaveScreenshot();
});