<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>