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">
  // Session analytics, where there is room to read them.
  //
  // They were on the welcome page — four charts, a heatmap, a top-sessions
  // table and a filter bar — which made the first thing anyone saw a study
  // rather than a glance. Welcome answers "what is the state of things"; this
  // answers "how has the work gone", and the two are different questions asked
  // at different moments.
  //
  // Nothing is dropped. The same components, the same store, the same filters.
  import { onMount, untrack } from "svelte";
  import { sessions } from "../../stores/sessions.svelte.js";
  import ActiveFilters from "../analytics/ActiveFilters.svelte";
  import ActivityTimeline from "../analytics/ActivityTimeline.svelte";
  import DateRangePicker from "../analytics/DateRangePicker.svelte";
  import Heatmap from "../analytics/Heatmap.svelte";
  import HourOfWeekHeatmap from "../analytics/HourOfWeekHeatmap.svelte";
  import ProjectBreakdown from "../analytics/ProjectBreakdown.svelte";
  import SummaryCards from "../analytics/SummaryCards.svelte";
  import TopSessions from "../analytics/TopSessions.svelte";
  import AppIcon from "../layout/AppIcon.svelte";
  import { analytics } from "../../stores/analytics.svelte.js";
  import { exportAnalyticsCSV } from "../../utils/csv-export.js";

  /// The exporter takes the five fields it writes, not the store — so a field
  /// added to the store does not silently start appearing in exports.
  function exportCSV() {
    exportAnalyticsCSV({
      from: analytics.from,
      to: analytics.to,
      summary: analytics.summary,
      activity: analytics.activity,
      projects: analytics.projects,
    });
  }

  function shortTz(zone: string): string {
    return zone.split("/").pop()?.replace(/_/g, " ") ?? zone;
  }

  // The session filters steer the analytics.
  //
  // This effect lived on the welcome page, where the charts used to be. It
  // belongs wherever they are: it is the thing that keeps "the sessions I am
  // looking at" and "the sessions these charts count" the same set, and split
  // from the charts it would silently stop mattering.
  $effect(() => {
    const headerProject = sessions.filters.project;
    const headerAgent = sessions.filters.agent;
    const headerRecentlyActive = sessions.filters.recentlyActive;
    const headerMinUserMessages =
      sessions.filters.minUserMessages;
    const headerIncludeOneShot =
      sessions.filters.includeOneShot;

    const curProject = untrack(() => analytics.project);
    const curAgent = untrack(() => analytics.agent);
    const curRecentlyActive = untrack(
      () => analytics.recentlyActive,
    );
    const curMinUser = untrack(
      () => analytics.minUserMessages,
    );
    const curIncludeOneShot = untrack(
      () => analytics.includeOneShot,
    );

    let changed = false;
    if (curProject !== headerProject) {
      analytics.project = headerProject;
      changed = true;
    }
    if (curAgent !== headerAgent) {
      analytics.agent = headerAgent;
      changed = true;
    }
    if (curRecentlyActive !== headerRecentlyActive) {
      analytics.recentlyActive = headerRecentlyActive;
      changed = true;
    }

    const minUserVal = headerMinUserMessages > 0
      ? headerMinUserMessages
      : 0;
    if (curMinUser !== minUserVal) {
      analytics.minUserMessages = minUserVal;
      changed = true;
    }

    if (curIncludeOneShot !== headerIncludeOneShot) {
      analytics.includeOneShot = headerIncludeOneShot;
      changed = true;
    }

    if (changed) {
      untrack(() => analytics.fetchAll());
    }
  });


  onMount(() => {
    // Fetched when the view is opened rather than on every page load: this is
    // the only thing that reads them now, so a worktree nobody asks about
    // costs nothing.
    void analytics.fetchAll();
  });
</script>

<div class="statistics">
  <header>
    <div class="title">
      <h1>Session statistics</h1>
      <p class="sub">Every session this worktree has recorded, over the selected range.</p>
    </div>
    <div class="actions">
      <DateRangePicker />
      <button
        class="icon-btn"
        onclick={() => analytics.fetchAll()}
        title="Refresh analytics"
        aria-label="Refresh analytics"
      >
        <AppIcon name="refresh" size={14} />
      </button>
      <button class="secondary" onclick={exportCSV}>Export CSV</button>
    </div>
  </header>

  <ActiveFilters />
  <SummaryCards />

  <div class="chart-grid">
    <div class="chart-panel">
      <div class="chart-header">
        <h2 class="chart-title">
          Activity by day and hour
          <span class="tz">{shortTz(analytics.timezone)}</span>
        </h2>
      </div>
      <ActivityTimeline />
      <div class="divider"></div>
      <HourOfWeekHeatmap />
    </div>

    <div class="chart-panel">
      <TopSessions />
    </div>

    <div class="chart-panel">
      <Heatmap />
    </div>

    <div class="chart-panel">
      <ProjectBreakdown />
    </div>
  </div>
</div>

<style>
  .statistics {
    max-width: 1400px;
    margin: 0 auto;
    padding: var(--wb-spacing-18) var(--wb-spacing-22) var(--wb-spacing-40);
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-14);
  }

  header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--wb-spacing-12);
    flex-wrap: wrap;
  }

  h1 {
    margin: 0;
    font-size: var(--wb-font-size-17);
    font-weight: 600;
  }

  .sub {
    margin: var(--wb-spacing-2) 0 0;
    font-size: var(--wb-font-size-12);
    opacity: 0.62;
  }

  .actions {
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-8);
  }

  .icon-btn,
  .secondary {
    display: inline-flex;
    align-items: center;
    gap: var(--wb-spacing-5);
    padding: var(--wb-spacing-4) var(--wb-spacing-10);
    font-size: var(--wb-font-size-11);
    font-family: inherit;
    border-radius: var(--wb-corner-radius-3);
    border: 1px solid var(--vscode-button-border, rgba(128, 128, 128, 0.3));
    background: transparent;
    color: inherit;
    cursor: pointer;
  }

  .icon-btn {
    padding: var(--wb-spacing-4) var(--wb-spacing-6);
  }

  .chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: var(--wb-spacing-14);
  }

  .chart-panel {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-8);
    padding: var(--wb-spacing-12) var(--wb-spacing-14);
    border-radius: var(--wb-corner-radius-5);
    border: 1px solid var(--vscode-panel-border, rgba(128, 128, 128, 0.18));
    background: var(--vscode-editor-inactiveSelectionBackground, rgba(128, 128, 128, 0.04));
    min-width: 0;
  }

  .chart-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }

  .chart-title {
    margin: 0;
    font-size: var(--wb-font-size-12);
    font-weight: 600;
    display: flex;
    align-items: baseline;
    gap: var(--wb-spacing-6);
  }

  .tz {
    font-size: var(--wb-font-size-10);
    font-weight: 400;
    opacity: 0.55;
  }

  .divider {
    height: 1px;
    background: var(--vscode-panel-border, rgba(128, 128, 128, 0.18));
    margin: var(--wb-spacing-4) 0;
  }
</style>