wsb-daemon 0.291.8591

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the host
<!--
  The boards — every GitHub Projects board of the accounts — the most
  recently changed first, as many across as the group is wide. Each says what
  is happening on it: its latest cards, the column each is in, and when it
  changed, so a board need not be opened to see what moved.

  A board belongs to no project; the projects that work from one are named on
  it. The boards are the workshop's mirror, kept current by its GitHub stream,
  so nothing here needs reloading.
-->
<script lang="ts">
  import { onMount } from "svelte";
  import { Button, Card, CardGrid, EmptyState, List, ListItem, Notice, Page, Spinner, Tag } from "@wsb/uikit/components";
  import { keyed } from "@wsb/uikit/handles";
  import { boards } from "../../stores/boards.svelte.js";
  import { workbench } from "../../stores/workbench.svelte.js";
  import { columnName, itemKind, type Project, type RecentCard } from "../../api/types/github.js";
  import { formatRelativeTime } from "../../utils/format.js";

  onMount(() => {
    void boards.load();
    return boards.watch();
  });

  const ICONS = { issue: "issue", "pull-request": "pull-request", draft: "notes" } as const;

  function openBoard(board: Project, event: MouseEvent) {
    workbench.openBoard(board.owner, board.number, { beside: event.altKey });
  }

  function openCard(board: Project, card: RecentCard, event: MouseEvent) {
    boards.showCard(board.owner, board.number, card.id);
    workbench.openBoard(board.owner, board.number, { beside: event.altKey });
  }

  /** Which card it is and when it changed: `#42 · 2h ago`, `draft · 2h ago`. */
  function about(card: RecentCard): string {
    const which = card.number !== null ? `#${card.number}` : itemKind(card) === "draft" ? "draft" : "";
    return [which, formatRelativeTime(card.updated_at)].filter(Boolean).join(" · ");
  }
</script>

{#snippet refresh()}
  <Button
    handle="page.boards.refresh"
    label={boards.refreshing ? "Asking GitHub…" : "Refresh"}
    variant="secondary"
    busy={boards.refreshing}
    onpress={() => void boards.refresh()}
  />
{/snippet}

<Page title="Boards" detail="Every board, the most recently changed first" handle="page.boards" actions={refresh}>
  {#if boards.noToken}
    <Notice
      tone="warning"
      title="There is no GitHub login on this host"
      detail="The boards shown are the last ones read. Log in with `gh auth login`."
    />
  {/if}
  {#each boards.problems as problem (problem)}
    <Notice tone="warning" title="A board could not be listed" detail={problem} />
  {/each}
  {#if boards.error}
    <Notice tone="danger" title="The boards could not be read" detail={boards.error} />
  {/if}

  {#if boards.loading && boards.projects.length === 0}
    <Spinner label="Reading the boards" />
  {:else if boards.projects.length === 0}
    <EmptyState
      icon="boards"
      title="No boards"
      detail="They are read from GitHub with the read:project scope on the GitHub login (gh auth login, or GITHUB_TOKEN); a token without it sees none."
    />
  {/if}

  <CardGrid ariaLabel="Boards">
    {#each boards.byActivity as { board, activity } (board.id)}
      {@const using = boards.projectsUsing(board)}
      {#snippet projects()}
        {#each using as project (project)}<Tag label={project} icon="project" />{/each}
      {/snippet}
      <Card
        handle={keyed("page.board", `${board.owner}/${board.number}`)}
        title={board.title}
        detail={[
          `${board.owner} · #${board.number}`,
          board.closed ? "closed" : null,
          activity.newest_change ? `changed ${formatRelativeTime(activity.newest_change)}` : null,
        ]
          .filter(Boolean)
          .join(" · ")}
        icon="board"
        muted={board.closed}
        meta={using.length > 0 ? projects : undefined}
        onpress={(event) => openBoard(board, event)}
      >
        {#if activity.latest.length > 0}
          <List ariaLabel={`Latest on ${board.title}`}>
            {#each activity.latest as card (card.id)}
              {@const column = columnName(card, board)}
              {#snippet where()}{#if column}<Tag label={column} />{/if}{/snippet}
              <ListItem
                handle={keyed("page.board.card", card.id)}
                title={card.title}
                detail={about(card)}
                icon={ICONS[itemKind(card)]}
                meta={column ? where : undefined}
                onpress={(event) => openCard(board, card, event)}
              />
            {/each}
          </List>
        {:else}
          <EmptyState title={activity.cards.ever_synced ? "No cards" : "Cards not read yet"} />
        {/if}
      </Card>
    {/each}
  </CardGrid>
</Page>