wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<script lang="ts">
  import { analytics } from "../../stores/analytics.svelte.js";
  import LoadingSpinner from "../layout/LoadingSpinner.svelte";

  const CELL_SIZE = 17;
  const CELL_GAP = 2;
  const CELL_STEP = CELL_SIZE + CELL_GAP;
  const ROW_LABEL_WIDTH = 29;
  const COL_LABEL_HEIGHT = 18;
  const DAY_LABELS = [
    "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
  ];

  const LEVEL_COLORS_LIGHT = [
    "var(--vscode-input-background)",
    "#9be9a8",
    "#40c463",
    "#30a14e",
    "#216e39",
  ];

  const LEVEL_COLORS_DARK = [
    "var(--vscode-input-background)",
    "#0e4429",
    "#006d32",
    "#26a641",
    "#39d353",
  ];

  function levelColor(level: number): string {
    const isDark =
      document.documentElement.classList.contains("dark");
    const colors = isDark
      ? LEVEL_COLORS_DARK
      : LEVEL_COLORS_LIGHT;
    return colors[level] ?? colors[0]!;
  }

  function assignLevel(value: number, max: number): number {
    if (value <= 0) return 0;
    if (max <= 0) return 1;
    const ratio = value / max;
    if (ratio <= 0.25) return 1;
    if (ratio <= 0.5) return 2;
    if (ratio <= 0.75) return 3;
    return 4;
  }

  let tooltip = $state<{
    x: number;
    y: number;
    text: string;
  } | null>(null);

  const grid = $derived.by(() => {
    const cells = analytics.hourOfWeek?.cells;
    if (!cells || cells.length === 0) return null;

    const lookup = new Map<string, number>();
    let max = 0;
    for (const c of cells) {
      lookup.set(`${c.day_of_week}:${c.hour}`, c.messages);
      if (c.messages > max) max = c.messages;
    }

    const rows: {
      day: string;
      dayIdx: number;
      hours: {
        hour: number;
        value: number;
        level: number;
      }[];
    }[] = [];
    for (let d = 0; d < 7; d++) {
      const hours: {
        hour: number;
        value: number;
        level: number;
      }[] = [];
      for (let h = 0; h < 24; h++) {
        const value = lookup.get(`${d}:${h}`) ?? 0;
        hours.push({
          hour: h,
          value,
          level: assignLevel(value, max),
        });
      }
      rows.push({ day: DAY_LABELS[d]!, dayIdx: d, hours });
    }
    return rows;
  });

  const chartWidth = ROW_LABEL_WIDTH + 24 * CELL_STEP + 4;
  const chartHeight = COL_LABEL_HEIGHT + 7 * CELL_STEP + 4;

  function handleCellHover(
    e: MouseEvent,
    day: string,
    hour: number,
    value: number,
  ) {
    const rect = (
      e.currentTarget as HTMLElement
    ).getBoundingClientRect();
    const h = hour.toString().padStart(2, "0");
    tooltip = {
      x: rect.left + rect.width / 2,
      y: rect.top - 4,
      text: `${day} ${h}:00 - ${value.toLocaleString()} messages`,
    };
  }

  function handleCellLeave() {
    tooltip = null;
  }

  function handleCellClick(dow: number, hour: number) {
    analytics.selectHourOfWeek(dow, hour);
  }

  function handleDayClick(dow: number) {
    analytics.selectHourOfWeek(dow, null);
  }

  function handleHourClick(hour: number) {
    analytics.selectHourOfWeek(null, hour);
  }

  function isDimmed(dow: number, hour: number): boolean {
    const sd = analytics.selectedDow;
    const sh = analytics.selectedHour;
    if (sd === null && sh === null) return false;
    if (sd !== null && sh !== null) {
      return dow !== sd || hour !== sh;
    }
    if (sd !== null) return dow !== sd;
    return hour !== sh;
  }
</script>

<div class="how-container">
  {#if !analytics.loaded.hourOfWeek || analytics.loading.hourOfWeek}
    <LoadingSpinner label="Loading hour-of-week activity" />
  {:else if analytics.errors.hourOfWeek}
    <div class="error">
      {analytics.errors.hourOfWeek}
      <button
        class="retry-btn"
        onclick={() => analytics.fetchHourOfWeek()}
      >
        Retry
      </button>
    </div>
  {:else if grid}
    <div class="how-scroll">
      <div
        class="how-chart"
        style={`width:${chartWidth}px;height:${chartHeight}px;`}
      >
        {#each [0, 3, 6, 9, 12, 15, 18, 21] as hour}
          <button
            class="hour-label"
            class:active-label={analytics.selectedHour === hour}
            style={`left:${hour * CELL_STEP + ROW_LABEL_WIDTH + CELL_SIZE / 2}px;top:${COL_LABEL_HEIGHT - 12}px;`}
            onclick={() => handleHourClick(hour)}
          >
            {hour}
          </button>
        {/each}

        {#each grid as row, rowIdx}
          <button
            class="day-label"
            class:active-label={analytics.selectedDow === row.dayIdx}
            style={`left:${ROW_LABEL_WIDTH - 4}px;top:${rowIdx * CELL_STEP + COL_LABEL_HEIGHT + CELL_SIZE - 11}px;`}
            onclick={() => handleDayClick(row.dayIdx)}
          >
            {row.day}
          </button>

          {#each row.hours as cell}
            <button
              class="how-cell"
              class:dimmed={isDimmed(row.dayIdx, cell.hour)}
              style={`left:${cell.hour * CELL_STEP + ROW_LABEL_WIDTH}px;top:${rowIdx * CELL_STEP + COL_LABEL_HEIGHT}px;background:${levelColor(cell.level)};`}
              aria-label={`${row.day} ${cell.hour}:00 ${cell.value} messages`}
              onmouseenter={(e) =>
                handleCellHover(e, row.day, cell.hour, cell.value)}
              onmouseleave={handleCellLeave}
              onclick={() =>
                handleCellClick(row.dayIdx, cell.hour)}
            ></button>
          {/each}
        {/each}
      </div>
    </div>

    {#if tooltip}
      <div
        class="tooltip"
        style="left: {tooltip.x}px; top: {tooltip.y}px;"
      >
        {tooltip.text}
      </div>
    {/if}
  {:else}
    <div class="empty">No data for this period</div>
  {/if}
</div>

<style>
  .how-container {
    position: relative;
    flex: 1;
  }

  .how-scroll {
    overflow-x: auto;
    padding-bottom: var(--wb-spacing-4);
  }

  .how-chart {
    position: relative;
  }

  .hour-label,
  .day-label {
    position: absolute;
    font-size: var(--wb-font-size-9);
    color: var(--vscode-disabledForeground);
    font-family: var(--wsb-font-sans);
    cursor: pointer;
    white-space: nowrap;
  }

  .hour-label {
    transform: translateX(-50%);
  }

  .day-label {
    transform: translateX(-100%);
  }

  .hour-label:hover,
  .day-label:hover {
    color: var(--vscode-editor-foreground);
  }

  .active-label {
    color: var(--vscode-focusBorder);
    font-weight: 600;
  }

  .how-cell {
    position: absolute;
    width: var(--wb-control-size-17);
    height: var(--wb-control-size-17);
    border-radius: var(--wb-corner-radius-2);
    cursor: pointer;
    transition: opacity 0.15s;
  }

  .how-cell:hover {
    opacity: 0.8;
    outline: 1px solid var(--vscode-disabledForeground);
  }

  .how-cell.dimmed {
    opacity: 0.2;
  }

  .tooltip {
    position: fixed;
    transform: translateX(-50%) translateY(-100%);
    padding: var(--wb-spacing-4) var(--wb-spacing-8);
    background: var(--vscode-editor-foreground);
    color: var(--vscode-editor-background);
    font-size: var(--wb-font-size-10);
    border-radius: var(--wsb-radius-sm);
    white-space: nowrap;
    pointer-events: none;
    z-index: 100;
  }

  .empty {
    color: var(--vscode-disabledForeground);
    font-size: var(--wb-font-size-12);
    padding: var(--wb-spacing-24);
    text-align: center;
  }

  .error {
    color: var(--vscode-errorForeground);
    font-size: var(--wb-font-size-12);
    padding: var(--wb-spacing-12);
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-8);
  }

  .retry-btn {
    padding: var(--wb-spacing-2) var(--wb-spacing-8);
    border: 1px solid currentColor;
    border-radius: var(--wsb-radius-sm);
    font-size: var(--wb-font-size-11);
    color: inherit;
    cursor: pointer;
  }
</style>