Scan a directory or workspace for SKILL.md files across all agents and repos, capture supporting files (references, scripts, linked docs), dedupe vendored copies, enrich each Tessl tile with registry signals, and emit a canonical JSON inventory validated by JSON Schema. Then run four analytical phases in parallel against the inventory — staleness + git provenance (history, broken refs, contributors), quality (Tessl `skill review`), duplicates (similarity + LLM judgement), registry-search (per-standalone-skill registry suggestions, HTTP only) — and render a self-contained interactive HTML report with a top-of-report health overview, top-issues panel, recently-changed list, and per-tessl.json manifests view.
84
90%
Does it follow best practices?
Impact
97%
1.44xAverage score across 2 eval scenarios
Advisory
Suggest reviewing before use
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tessl | Org Skill Usage</title>
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOTIiIGhlaWdodD0iMTkyIiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMTkyIDE5MiI+PHBhdGggZmlsbD0iI2ZhZmFmYSIgZD0iTTU3LjQ2IDgzLjc4MWMuMDAyLTEuNzI0IDEuODc0LTIuNzg2IDMuMzY4LTEuOTM3bDMwLjY0NCAxNy42MXYzOS42MTdjMCA2LjkzMSA3LjU4NyAxMS4yMTIgMTMuNTY2IDcuNjhsMjkuNDQtMTcuNDIydjQzLjMxM2wtMjkuNTI3IDE2Ljk2OWExOC4wMiAxOC4wMiAwIDAgMS0xNy45MzQgMEw1Ny40NiAxNzIuNjQyek0xODIgMTM1LjA5NWExNy44MyAxNy44MyAwIDAgMS04Ljk3MSAxNS40NTNsLTI5LjU0OSAxNi45Njl2LTQzLjUwMUwxODIgMTAxLjIwM3pNNDguNDk3IDEyMy4yNzRoLS4wMjR2NDQuMjJsLTI5LjUwMi0xNi45NDZBMTcuODMgMTcuODMgMCAwIDEgMTAgMTM1LjA5NVYxMDEuMTh6bTEyNC41MDgtODEuNzlhMTcuODMgMTcuODMgMCAwIDEgOC45NzIgMTUuNDUzVjkwLjgybC03OC4xNDMgNDYuMjY3Yy0xLjQ5My44OTctMy4zODItLjE5MS0zLjM4NC0xLjkxNVY5OS41MDhsMzcuNDI2LTIxLjUxNmM0LjQ4Mi0yLjU3NyA0LjUwNS04Ljk3Ny4wNDgtMTEuNTc4bC0zMy40MDUtMTkuNTMyIDM4LjkzNy0yMi4zNzV6bS00Ni4yNjMgMjguNzU4YzEuNDkzLjg1IDEuNDk0IDMuMDAyIDAgMy44NTFMOTYuMDEyIDkxLjc1IDU4LjYxNyA3MC4yNjVjLTQuNDg0LTIuNTc1LTEwLjA5Mi42MzgtMTAuMTIgNS43OXYzNi45MTRMMTAgOTAuODc1VjU2LjkzN2MwLTYuMzcyIDMuNDItMTIuMjY2IDguOTcxLTE1LjQ1M2wyOS41NS0xN3ptLTIyLjIyMy0yMy4zNi0uMDE2LjAxNnYtLjAyNHpNODcuMDE3IDIuMzg5YTE4LjAyIDE4LjAyIDAgMCAxIDE3LjkzNCAwbDI5LjUyNyAxNi45NDZMOTUuNjEgNDEuNjYzIDU3LjQ2IDE5LjM1OHoiLz48L3N2Zz4=" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@400..700&family=Atkinson+Hyperlegible+Next:wght@400..700&display=swap" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body {
font-family: "Atkinson Hyperlegible Next", system-ui, sans-serif;
background-color: #0a0a0a;
color: var(--fg);
font-size: 14px;
line-height: 1.625;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
ul, ol { list-style: none; }
code, .mono { font-family: "Atkinson Hyperlegible Mono", ui-monospace, monospace; font-size: 12px; }
:root {
--red: #f87171;
--orange: #fb923c;
--yellow: #fbbf24;
--green: #4ade80;
--blue: #60a5fa;
--purple: #a78bfa;
--fg: #f9fafb;
--fg-muted: #9ca3af;
--fg-subtle: #6b7280;
--border: rgba(255, 255, 255, 0.04);
--border-strong: rgba(255, 255, 255, 0.08);
--surface: #111;
--surface-raised: #161616;
}
.shell { max-width: 1200px; margin: 0 auto; padding: 32px; }
/* Header */
.header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
.header h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }
.header .subtitle { font-size: 13px; color: var(--fg-muted); }
.meta-row {
font-size: 12px; color: var(--fg-subtle);
margin-bottom: 32px; display: flex; gap: 16px; flex-wrap: wrap;
}
.meta-row strong { color: var(--fg-muted); font-weight: 500; }
.alpha-tag {
font-size: 11px; font-weight: 600; letter-spacing: 0.1em;
padding: 2px 7px; border-radius: 4px;
background: rgba(251, 191, 36, 0.14); color: var(--yellow);
text-transform: uppercase; line-height: 1.375;
}
/* Filter banner */
.filter-banner {
background: rgba(96, 165, 250, 0.07);
border: 1px solid rgba(96, 165, 250, 0.18);
border-radius: 8px; padding: 14px 18px;
margin: 0 0 24px 0;
display: flex; gap: 16px; align-items: baseline;
flex-wrap: wrap;
}
.filter-banner.unfiltered {
background: rgba(251, 146, 60, 0.07);
border-color: rgba(251, 146, 60, 0.22);
}
.filter-banner .label {
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--blue);
}
.filter-banner.unfiltered .label { color: var(--orange); }
.filter-banner .repos {
font-family: "Atkinson Hyperlegible Mono", monospace;
font-size: 13px; color: var(--fg);
}
.filter-banner .meta { font-size: 12px; color: var(--fg-muted); }
.filter-banner .meta strong { color: var(--fg); font-weight: 500; }
/* Repo chip filter */
.repo-filter {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
margin: 0 0 24px 0;
}
.repo-filter .head {
display: flex; justify-content: space-between; align-items: baseline;
gap: 12px; margin-bottom: 8px; flex-wrap: wrap;
}
.repo-filter .head h3 {
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--fg-subtle);
}
.repo-filter .head .summary {
font-size: 12px; color: var(--fg-muted);
}
.repo-filter .head .summary strong { color: var(--fg); font-weight: 500; }
.repo-filter .head .actions {
display: flex; gap: 6px;
}
.repo-filter .head .action-btn {
font-size: 11px; padding: 3px 8px;
color: var(--fg-muted);
border: 1px solid var(--border); border-radius: 4px;
background: rgba(255, 255, 255, 0.02);
transition: color 0.15s, border-color 0.15s;
}
.repo-filter .head .action-btn:hover {
color: var(--fg); border-color: var(--border-strong);
}
.repo-filter .chips {
display: flex; gap: 6px; flex-wrap: wrap;
}
.repo-filter .chip {
display: inline-flex; align-items: center; gap: 6px;
font-family: "Atkinson Hyperlegible Mono", monospace;
font-size: 11px;
padding: 4px 9px;
border: 1px solid var(--border-strong);
border-radius: 999px;
background: rgba(96, 165, 250, 0.08);
color: var(--fg);
cursor: pointer;
user-select: none;
transition: all 0.15s;
line-height: 1.4;
}
.repo-filter .chip:hover {
border-color: var(--blue);
}
.repo-filter .chip.excluded {
background: transparent;
color: var(--fg-subtle);
border-color: var(--border);
text-decoration: line-through;
}
.repo-filter .chip .count {
font-size: 10px;
color: var(--fg-muted);
font-weight: 400;
}
.repo-filter .chip.excluded .count { color: var(--fg-subtle); }
/* Synthetic "(no repo)" chip — events that matched the org filter but had no
gitRepo to attribute. Italic + purple tint to distinguish from real repos. */
.repo-filter .chip.synthetic {
font-style: italic;
background: rgba(167, 139, 250, 0.10);
border-color: rgba(167, 139, 250, 0.30);
color: var(--purple);
}
.repo-filter .chip.synthetic:hover { border-color: var(--purple); }
.repo-filter .chip.synthetic.excluded {
background: transparent;
border-color: var(--border);
color: var(--fg-subtle);
}
.repo-filter .show-more {
font-size: 11px; color: var(--fg-muted);
margin-top: 8px; padding: 0;
background: none; border: none; cursor: pointer;
}
.repo-filter .show-more:hover { color: var(--fg); }
/* Filter-applied indicator above re-aggregated sections */
.filter-applied-banner {
display: inline-block;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(96, 165, 250, 0.12);
color: var(--blue);
margin-left: 8px;
vertical-align: middle;
}
/* Window selector */
.window-tabs {
display: inline-flex; gap: 4px; margin-bottom: 24px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 6px; padding: 3px;
}
.window-tab {
font-size: 12px; padding: 4px 12px; border-radius: 4px;
color: var(--fg-muted); transition: all 0.15s;
}
.window-tab:hover { color: var(--fg); }
.window-tab.active {
background: rgba(255, 255, 255, 0.06); color: var(--fg);
}
/* Stats grid */
.stats-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 12px; margin-bottom: 32px;
}
.stat-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border);
border-radius: 8px; padding: 16px;
}
.stat-card .label {
font-size: 11px; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--fg-subtle);
}
.stat-card .value {
font-size: 22px; font-weight: 600; margin-top: 4px;
}
.stat-card .sub {
font-size: 11px; color: var(--fg-subtle); margin-top: 4px;
}
/* Sections */
.section { margin-bottom: 40px; }
.section-header {
display: flex; justify-content: space-between; align-items: baseline;
margin-bottom: 8px;
}
.section h2 {
font-size: 13px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.1em;
color: var(--fg-subtle);
}
.section .section-meta {
font-size: 12px; color: var(--fg-subtle);
}
.section p.section-desc {
font-size: 12px; color: var(--fg-muted); margin-bottom: 12px;
line-height: 1.5;
}
/* Filter input */
.filter-input {
width: 100%; max-width: 320px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 6px; padding: 6px 10px; color: var(--fg);
font-size: 12px; margin-bottom: 12px;
}
.filter-input:focus { outline: none; border-color: var(--border-strong); }
/* Tables */
table.data {
width: 100%; border-collapse: collapse;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border); border-radius: 8px;
overflow: hidden;
}
table.data th {
font-size: 11px; font-weight: 500;
text-transform: uppercase; letter-spacing: 0.05em;
color: var(--fg-subtle); text-align: left;
padding: 10px 12px;
border-bottom: 1px solid var(--border-strong);
background: rgba(255, 255, 255, 0.01);
cursor: pointer; user-select: none;
position: sticky; top: 0;
}
table.data th:hover { color: var(--fg); }
table.data th.sort-asc::after { content: ' ▲'; color: var(--blue); }
table.data th.sort-desc::after { content: ' ▼'; color: var(--blue); }
table.data td {
font-size: 13px; padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--fg-muted);
}
table.data tr:last-child td { border-bottom: none; }
table.data tr:hover td { background: rgba(255, 255, 255, 0.015); }
table.data td.num {
font-family: "Atkinson Hyperlegible Mono", monospace;
font-size: 12px; text-align: right;
color: var(--fg);
}
table.data td.num.dim { color: var(--fg-subtle); }
table.data td.tile, table.data td.skill {
font-family: "Atkinson Hyperlegible Mono", monospace;
font-size: 12px;
}
table.data td.tile { color: var(--fg-muted); }
table.data td.skill { color: var(--fg); }
table.data td.scope {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
}
table.data td.scope.project { color: var(--blue); }
table.data td.scope.global { color: var(--purple); }
table.data td.synthetic-row {
font-style: italic;
color: var(--purple);
}
table.data td.synthetic-row.dim { color: var(--fg-subtle); }
.empty-state {
padding: 24px; text-align: center;
color: var(--fg-subtle); font-size: 13px;
}
.table-wrap { max-height: 540px; overflow-y: auto; }
/* Footer */
.footer {
margin-top: 48px; padding-top: 16px;
border-top: 1px solid var(--border);
font-size: 11px; color: var(--fg-subtle);
line-height: 1.7;
}
.footer code {
font-size: 11px;
background: rgba(255, 255, 255, 0.03); padding: 1px 5px;
border-radius: 3px;
}
.footer .caveat { color: var(--orange); }
</style>
</head>
<body>
<div class="shell">
<div class="header">
<div>
<h1>Org Skill Usage <span class="alpha-tag">PostHog</span></h1>
<div class="subtitle">
Cross-organisation skill activations and install footprint, sourced from
<code>cli:agent-signals:*</code> events.
</div>
</div>
</div>
<div class="meta-row" id="meta-row"></div>
<div id="filter-banner"></div>
<div class="repo-filter" id="repo-filter">
<div class="head">
<h3>Repos</h3>
<div class="summary" id="repo-filter-summary"></div>
<div class="actions">
<button class="action-btn" id="repo-include-all">include all</button>
<button class="action-btn" id="repo-exclude-all">exclude all</button>
</div>
</div>
<div class="chips" id="repo-filter-chips"></div>
<button class="show-more" id="repo-show-more"></button>
</div>
<div class="window-tabs" id="window-tabs"></div>
<div class="stats-grid" id="stats-grid"></div>
<div class="section">
<div class="section-header">
<h2>Top-line totals</h2>
<div class="section-meta" id="totals-meta"></div>
</div>
<div id="totals-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Repos by activations</h2>
<div class="section-meta" id="repos-meta"></div>
</div>
<p class="section-desc">Per-repo top-line activity. Repos here are <code>properties.gitRepo</code> values from skill-activation events; events with no <code>gitRepo</code> aren't attributed to any row (their share is reported in the filter banner above). Click a chip in the Repos filter at the top to remove a repo from every section below.</p>
<input type="text" class="filter-input" id="repos-filter" placeholder="filter repo name…" />
<div class="table-wrap" id="repos-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Tiles by activations</h2>
<div class="section-meta" id="tiles-meta"></div>
</div>
<p class="section-desc">Every Tessl tile with at least one activation in the largest configured window. Sorted by primary-window activations.</p>
<input type="text" class="filter-input" id="tiles-filter" placeholder="filter tile name…" />
<div class="table-wrap" id="tiles-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Skills by activations</h2>
<div class="section-meta" id="skills-meta"></div>
</div>
<p class="section-desc">Per-skill breakdown for the top tiles by primary-window activations. <code>providers</code> are activations split between claude-code and cursor-ide in the primary window.</p>
<input type="text" class="filter-input" id="skills-filter" placeholder="filter tile or skill…" />
<div class="table-wrap" id="skills-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Loaded skills</h2>
<div class="section-meta" id="loaded-meta"></div>
</div>
<p class="section-desc">
Per-(tile, name, scope) install footprint, derived by unrolling the <code>installedSkills</code> array attached to every activation event.
Tells you who has each skill <em>available</em>, independent of whether they ever activated it.
Only includes users with at least one activation event in the window — users who never fire any activation contribute no rows here.
<span class="caveat" id="loaded-filter-note" style="display:none">Repo filter is ignored on this section — install footprint is per-user, not per-repo.</span>
</p>
<input type="text" class="filter-input" id="loaded-filter" placeholder="filter tile or skill…" />
<div class="table-wrap" id="loaded-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Untiled skills</h2>
<div class="section-meta" id="untiled-meta"></div>
</div>
<p class="section-desc">Activations of skills with no <code>skillTile</code> attribution — third-party plugins (e.g. <code>superpowers:*</code>) and private SKILL.md files. Cross-reference against discovery is name-only here.</p>
<input type="text" class="filter-input" id="untiled-filter" placeholder="filter skill name…" />
<div class="table-wrap" id="untiled-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Tessl MCP tools</h2>
<div class="section-meta" id="mcp-meta"></div>
</div>
<p class="section-desc">Activations of <code>mcp__tessl__*</code> tool calls — what users do with the Tessl MCP server itself.</p>
<div id="mcp-table"></div>
</div>
<div class="section">
<div class="section-header">
<h2>Session aggregates</h2>
<div class="section-meta" id="sessions-meta"></div>
</div>
<p class="section-desc">Aggregated counts from <code>cli:agent-signals:session-processed</code> events, summed across all sessions in the window.</p>
<div id="sessions-table"></div>
</div>
<div class="footer" id="footer"></div>
</div>
<script id="org-usage-data" type="application/json"><!--@ORG_USAGE_DATA@--></script>
<script>
(function () {
"use strict";
var raw = document.getElementById("org-usage-data").textContent.trim();
if (!raw || raw === "null") {
document.querySelector(".shell").innerHTML =
'<div class="empty-state">No org usage data was attached to this report.</div>';
return;
}
var data;
try {
data = JSON.parse(raw);
} catch (err) {
document.querySelector(".shell").innerHTML =
'<div class="empty-state">Failed to parse org_usage.json: ' + String(err) + "</div>";
return;
}
// ── Synthetic "(no repo)" bucket ─────────────────────────────
// Events that matched the org filter but had a NULL gitRepo
// can't be attributed to any real repo. They appear in the
// all-repos `totals` but not in `repos[]` or any `*_by_repo[]`
// array. To make them filterable alongside real repos via the
// same chip-filter UI, we synthesize a "(no repo)" bucket at
// init time by subtracting the per-repo aggregates from the
// all-repos rollups. This is purely client-side derivation —
// no new HogQL queries, no schema change.
//
// After injection, `data` looks the same shape as if PostHog
// had returned a row keyed `(no repo)` for every aggregate.
// The existing chip filter, repo table, and subtractive view
// logic handle it without modification — except we tag the
// synthetic repo row with `synthetic: true` so the chip
// renderer can style it distinctly.
var NO_REPO_LABEL = "(no repo)";
injectNoRepoBucket(data);
var ALL_REPOS = (data.repos || []).map(function (r) { return r.repo; });
var REPO_EXPAND_THRESHOLD = 18;
function injectNoRepoBucket(data) {
// Per-window summary for the synthetic repo row.
var repoWindows = {};
var hasAnyActivity = false;
function nn(n) { return n < 0 ? 0 : n; }
(data.windows || []).forEach(function (d) {
var w = d + "d";
var orig = (data.totals || {})[w] || {};
var sub = {
activations: 0, users: 0, sessions: 0,
tiled_activations: 0, untiled_activations: 0,
};
var subProvs = {};
(data.repos || []).forEach(function (r) {
var rw = r.windows[w]; if (!rw) return;
sub.activations += rw.activations || 0;
sub.users += rw.users || 0;
sub.sessions += rw.sessions || 0;
sub.tiled_activations += rw.tiled_activations || 0;
sub.untiled_activations += rw.untiled_activations || 0;
for (var pk in (rw.providers || {})) {
var dst = subProvs[pk] || (subProvs[pk] = { activations: 0, users: 0 });
dst.activations += (rw.providers[pk] || {}).activations || 0;
dst.users += (rw.providers[pk] || {}).users || 0;
}
});
var newProvs = {};
var origProvs = orig.providers || {};
for (var pk in origProvs) {
var op = origProvs[pk] || {};
var sp = subProvs[pk] || { activations: 0, users: 0 };
newProvs[pk] = {
activations: nn((op.activations || 0) - sp.activations),
users: nn((op.users || 0) - sp.users),
};
}
var noRepoActs = nn((orig.activations || 0) - sub.activations);
if (noRepoActs > 0) hasAnyActivity = true;
repoWindows[w] = {
activations: noRepoActs,
users: nn((orig.users || 0) - sub.users),
sessions: nn((orig.sessions || 0) - sub.sessions),
tiled_activations: nn((orig.tiled_activations || 0) - sub.tiled_activations),
untiled_activations: nn((orig.untiled_activations || 0) - sub.untiled_activations),
providers: newProvs,
};
});
if (!hasAnyActivity) return false;
data.repos = (data.repos || []).slice();
data.repos.push({
repo: NO_REPO_LABEL,
synthetic: true,
windows: repoWindows,
});
// Per-rollup synthetic rows: for each (tiles, skills,
// untiled_skills, mcp_tools), derive the no-repo portion
// of each row by subtracting the sum of per-repo entries
// from the all-repos aggregate.
function deriveNoRepoFor(originalRows, byRepoArrayKey, identityKey) {
var byRepoArray = data[byRepoArrayKey] = (data[byRepoArrayKey] || []).slice();
var subBy = {};
byRepoArray.forEach(function (r) {
var k = identityKey(r);
var d = subBy[k] || (subBy[k] = {});
for (var w in r.windows) {
var sw = d[w] || (d[w] = {});
for (var f in r.windows[w]) {
sw[f] = (sw[f] || 0) + (r.windows[w][f] || 0);
}
}
});
(originalRows || []).forEach(function (orig) {
var k = identityKey(orig);
var sub = subBy[k] || {};
var newWindows = {};
var any = false;
for (var w in orig.windows) {
var src = orig.windows[w] || {};
var sw = sub[w] || {};
var fields = {};
for (var f in src) {
fields[f] = nn((src[f] || 0) - (sw[f] || 0));
}
newWindows[w] = fields;
if ((fields.activations || 0) > 0) any = true;
}
if (any) {
byRepoArray.push(Object.assign({}, orig, {
repo: NO_REPO_LABEL,
windows: newWindows,
}));
}
});
}
deriveNoRepoFor(data.tiles, "tiles_by_repo",
function (r) { return r.tile; });
deriveNoRepoFor(data.skills, "skills_by_repo",
function (r) { return r.tile + "\u0000" + r.name; });
deriveNoRepoFor(data.untiled_skills, "untiled_skills_by_repo",
function (r) { return r.name; });
deriveNoRepoFor(data.mcp_tools, "mcp_tools_by_repo",
function (r) { return r.tool; });
// Session aggregates: per-window object → array of
// per-repo rows. Append a synthetic "(no repo)" row to
// each window that has remaining un-attributed activity.
var sabr = data.session_aggregates_by_repo || {};
var newSabr = {};
var SA_FIELDS = ["sessions", "users", "messages", "tool_calls", "skill_calls",
"tessl_skill_calls", "tessl_mcp_calls", "tessl_cli_calls"];
(data.windows || []).forEach(function (d) {
var w = d + "d";
var rows = (sabr[w] || []).slice();
var orig = (data.session_aggregates || {})[w] || {};
var sub = {};
SA_FIELDS.forEach(function (f) { sub[f] = 0; });
rows.forEach(function (r) {
SA_FIELDS.forEach(function (f) { sub[f] += r[f] || 0; });
});
var noRepoRow = { repo: NO_REPO_LABEL };
var any = false;
SA_FIELDS.forEach(function (f) {
var v = nn((orig[f] || 0) - sub[f]);
noRepoRow[f] = v;
if (f === "sessions" && v > 0) any = true;
});
if (any) rows.push(noRepoRow);
newSabr[w] = rows;
});
data.session_aggregates_by_repo = newSabr;
return true;
}
// ── State ────────────────────────────────────────────────────
// `excludedRepos` is a Set of repo IDs the user has unticked.
// When empty the view falls back to the all-repos aggregates
// already in `data` (zero re-aggregation cost). When non-empty
// we recompute `tiles`, `skills`, `untiled_skills`, `mcp_tools`,
// `totals`, and `session_aggregates` from the `*_by_repo`
// arrays. `loaded_skills` always stays all-repos.
// Persist `excludedRepos` in localStorage keyed by report
// identity (project + fetch time). We don't want a stale
// selection from yesterday's report bleeding into today's.
var STORAGE_KEY = "tessl.org-usage.excluded-repos." +
(data.source ? data.source.project_id : "0");
function loadExcluded() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return new Set();
var arr = JSON.parse(raw);
return new Set(Array.isArray(arr) ? arr : []);
} catch (e) { return new Set(); }
}
function saveExcluded(set) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(set)));
} catch (e) { /* localStorage unavailable, no-op */ }
}
var state = {
window: data.primary_window_days + "d",
sort: {},
excludedRepos: loadExcluded(),
repoExpanded: false,
};
function fmtInt(n) {
if (n == null) return "—";
return n.toLocaleString();
}
function pickWindow(obj, w) {
return obj && obj.windows ? (obj.windows[w] || null) : null;
}
function el(tag, attrs, text) {
var e = document.createElement(tag);
if (attrs) for (var k in attrs) {
if (k === "class") e.className = attrs[k];
else e.setAttribute(k, attrs[k]);
}
if (text != null) e.textContent = text;
return e;
}
// ── Effective view (subtractive re-aggregation when filter active) ──
//
// When the user excludes one or more repos, build a view that
// takes the original aggregates and SUBTRACTS the excluded
// repos' contribution. This matches the user's intent ("untick
// monorepo and its activations should disappear"), and — unlike
// re-summing from `*_by_repo` arrays — preserves the events
// that have no `gitRepo` to attribute (they were always part of
// the all-repos totals; they have no repo to remove from).
//
// Caveats embedded in the math:
// - `activations`, `tiled_activations`, `untiled_activations`,
// provider activation counts, and session_aggregates fields
// (sessions, messages, tool_calls, ...) are additive per-repo
// so subtraction is exact.
// - `users` and `sessions` (in WindowTotals) are NOT additive —
// the same user/session can appear in multiple repos. We
// subtract anyway as a "cap" approximation; the displayed
// number is therefore a lower bound on the true count when
// filter is active. The footer mentions this.
//
// Cached on the filter signature.
var _viewCache = null;
var _viewCacheKey = null;
function filterActive() {
return state.excludedRepos.size > 0;
}
function getView() {
if (!filterActive()) return data;
var key = Array.from(state.excludedRepos).sort().join("|");
if (_viewCacheKey === key && _viewCache) return _viewCache;
var excluded = state.excludedRepos;
function isExcluded(row) { return excluded.has(row.repo); }
function nonNeg(n) { return n < 0 ? 0 : n; }
// Subtract excluded repos' contributions from per-tile rollup.
function subtractRollup(originalRows, byRepoArray, identityKey) {
// Build subtraction index keyed by tile/skill/tool/name.
var subBy = {};
(byRepoArray || []).filter(isExcluded).forEach(function (r) {
var k = identityKey(r);
var dst = subBy[k] || (subBy[k] = {});
for (var w in r.windows) {
var sw = dst[w] || (dst[w] = {});
for (var f in r.windows[w]) {
sw[f] = (sw[f] || 0) + (r.windows[w][f] || 0);
}
}
});
var out = [];
(originalRows || []).forEach(function (orig) {
var k = identityKey(orig);
var sub = subBy[k] || {};
var newWindows = {};
var anyNonZero = false;
for (var w in orig.windows) {
var src = orig.windows[w] || {};
var sw = sub[w] || {};
var fields = {};
for (var f in src) {
fields[f] = nonNeg((src[f] || 0) - (sw[f] || 0));
}
newWindows[w] = fields;
if ((fields.activations || 0) > 0) anyNonZero = true;
}
if (anyNonZero) {
out.push(Object.assign({}, orig, { windows: newWindows }));
}
});
return out;
}
var tiles = subtractRollup(data.tiles, data.tiles_by_repo,
function (r) { return r.tile; });
var skills = subtractRollup(data.skills, data.skills_by_repo,
function (r) { return r.tile + "\u0000" + r.name; });
var untiledSkills = subtractRollup(data.untiled_skills, data.untiled_skills_by_repo,
function (r) { return r.name; });
var mcpTools = subtractRollup(data.mcp_tools, data.mcp_tools_by_repo,
function (r) { return r.tool; });
// Totals — subtract excluded repos' per-window contribution.
//
// Activations / tiled / untiled / provider activations are
// additive across repos so subtraction is exact. Users and
// sessions are NOT additive (a single user/session can
// appear in multiple repos), so pure subtraction can
// wildly under-count when the filter is restrictive.
// Example: filtering down to only `tesslio/monorepo`
// produces `48 - sum(67 other repos' users)` ≈ 4, when
// the truthful answer is monorepo's own user count of 18.
//
// To stay defensible, we clamp users / sessions to AT
// LEAST `max(included_repos.field)` — the largest single
// included repo's value. That gives a true lower bound:
// we know the displayed count is at least that high, and
// never higher than `data.totals` (the all-repos truth).
// Provider users get the same treatment.
var totals = {};
data.windows.forEach(function (d) {
var w = d + "d";
var orig = data.totals[w] || {};
var sub = {
activations: 0, users: 0, sessions: 0,
tiled_activations: 0, untiled_activations: 0,
};
var subProvs = {};
var maxIncl = { users: 0, sessions: 0 };
var maxInclProvs = {};
(data.repos || []).forEach(function (r) {
var rw = r.windows[w]; if (!rw) return;
if (isExcluded(r)) {
sub.activations += rw.activations || 0;
sub.tiled_activations += rw.tiled_activations || 0;
sub.untiled_activations += rw.untiled_activations || 0;
sub.users += rw.users || 0;
sub.sessions += rw.sessions || 0;
var pr = rw.providers || {};
for (var pk in pr) {
var dst = subProvs[pk] || (subProvs[pk] = { activations: 0, users: 0 });
dst.activations += (pr[pk] || {}).activations || 0;
dst.users += (pr[pk] || {}).users || 0;
}
} else {
if ((rw.users || 0) > maxIncl.users) maxIncl.users = rw.users;
if ((rw.sessions || 0) > maxIncl.sessions) maxIncl.sessions = rw.sessions;
var pr2 = rw.providers || {};
for (var pk2 in pr2) {
var u = (pr2[pk2] || {}).users || 0;
if (u > (maxInclProvs[pk2] || 0)) maxInclProvs[pk2] = u;
}
}
});
var newProvs = {};
var origProvs = orig.providers || {};
for (var pk in origProvs) {
var op = origProvs[pk] || {};
var sp = subProvs[pk] || { activations: 0, users: 0 };
var subtractedUsers = nonNeg((op.users || 0) - sp.users);
newProvs[pk] = {
activations: nonNeg((op.activations || 0) - sp.activations),
users: Math.max(subtractedUsers, maxInclProvs[pk] || 0),
};
}
var subUsers = nonNeg((orig.users || 0) - sub.users);
var subSessions = nonNeg((orig.sessions || 0) - sub.sessions);
totals[w] = {
activations: nonNeg((orig.activations || 0) - sub.activations),
tiled_activations: nonNeg((orig.tiled_activations || 0) - sub.tiled_activations),
untiled_activations: nonNeg((orig.untiled_activations || 0) - sub.untiled_activations),
users: Math.max(subUsers, maxIncl.users),
sessions: Math.max(subSessions, maxIncl.sessions),
providers: newProvs,
};
});
// Session aggregates — same subtraction. session_aggregates is per-window keyed,
// session_aggregates_by_repo is per-window keyed → array of per-repo rows.
var sessionAggregates = {};
var SA_FIELDS = ["sessions", "users", "messages", "tool_calls", "skill_calls",
"tessl_skill_calls", "tessl_mcp_calls", "tessl_cli_calls"];
data.windows.forEach(function (d) {
var w = d + "d";
var orig = (data.session_aggregates || {})[w] || {};
var sub = {};
SA_FIELDS.forEach(function (f) { sub[f] = 0; });
((data.session_aggregates_by_repo || {})[w] || []).filter(isExcluded).forEach(function (r) {
SA_FIELDS.forEach(function (f) { sub[f] += r[f] || 0; });
});
var out = {};
SA_FIELDS.forEach(function (f) {
out[f] = nonNeg((orig[f] || 0) - sub[f]);
});
sessionAggregates[w] = out;
});
var v = Object.assign({}, data, {
tiles: tiles, skills: skills,
untiled_skills: untiledSkills,
mcp_tools: mcpTools,
totals: totals,
session_aggregates: sessionAggregates,
});
_viewCache = v;
_viewCacheKey = key;
return v;
}
// ── Meta row ─────────────────────────────────────────────────
function renderMeta() {
var row = document.getElementById("meta-row");
row.innerHTML = "";
var parts = [
["fetched", new Date(data.fetched_at).toISOString().slice(0, 16).replace("T", " ") + "Z"],
["source", data.source.kind + " project " + data.source.project_id],
["windows", data.windows.map(function (d) { return d + "d"; }).join(", ")],
["primary", data.primary_window_days + "d"],
["top-tiles-detail", data.top_tiles_detail],
["top-loaded", data.top_loaded],
["tool", data.tool_version],
];
parts.forEach(function (p) {
var span = el("span", null);
span.appendChild(el("strong", null, p[0] + ":"));
span.appendChild(document.createTextNode(" " + p[1]));
row.appendChild(span);
});
}
// ── Filter banner ────────────────────────────────────────────
function renderFilterBanner() {
var wrap = document.getElementById("filter-banner");
wrap.innerHTML = "";
var f = data.filter || { repos: [], email_domains: [], events_per_window: {} };
var fmeta = f.events_per_window[state.window] || {};
var hasRepos = (f.repos || []).length > 0;
var hasEmails = (f.email_domains || []).length > 0;
var anyFilter = hasRepos || hasEmails;
var banner = el("div", { class: "filter-banner" + (anyFilter ? "" : " unfiltered") });
if (!anyFilter) {
banner.appendChild(el("span", { class: "label" }, "No filter"));
banner.appendChild(el("span", { class: "repos" }, "all events in project"));
var meta = el("span", { class: "meta" });
meta.innerHTML = "Showing every <code>cli:agent-signals:*</code> event regardless of which repo or which user it came from.";
banner.appendChild(meta);
} else {
banner.appendChild(el("span", { class: "label" }, "Filter (repo OR email)"));
var pieces = [];
if (hasRepos) pieces.push("repos: " + f.repos.join(", "));
if (hasEmails) pieces.push("emails: @" + f.email_domains.join(", @"));
banner.appendChild(el("span", { class: "repos" }, pieces.join(" ")));
var matched = fmeta.events_matched_filter || 0;
var total = fmeta.events_total || 0;
var pct = total ? Math.round((matched / total) * 100) : 0;
var byRepo = fmeta.events_matched_by_repo || 0;
var byEmail = fmeta.events_matched_by_email || 0;
var ex = fmeta.events_excluded_by_filter || 0;
var noattr = fmeta.events_no_gitrepo || 0;
var both = (hasRepos && hasEmails) ? Math.max(0, byRepo + byEmail - matched) : 0;
var detailParts = [];
if (hasRepos) detailParts.push("<strong>" + byRepo.toLocaleString() + "</strong> via repo");
if (hasEmails) detailParts.push("<strong>" + byEmail.toLocaleString() + "</strong> via email");
if (hasRepos && hasEmails) detailParts.push(both.toLocaleString() + " match both");
var meta2 = el("span", { class: "meta" });
meta2.innerHTML =
"<strong>" + matched.toLocaleString() + "</strong> of " +
total.toLocaleString() + " events match (" + pct + "%) — " +
detailParts.join(", ") + "; " +
ex.toLocaleString() + " excluded, " +
noattr.toLocaleString() + " had no gitRepo (" + state.window + " window)";
banner.appendChild(meta2);
}
wrap.appendChild(banner);
}
// ── Window tabs ──────────────────────────────────────────────
function renderWindowTabs() {
var wrap = document.getElementById("window-tabs");
wrap.innerHTML = "";
data.windows.forEach(function (d) {
var key = d + "d";
var btn = el("button", { class: "window-tab" + (key === state.window ? " active" : "") }, key);
btn.addEventListener("click", function () {
state.window = key;
renderAll();
});
wrap.appendChild(btn);
});
}
// ── Stats grid ───────────────────────────────────────────────
function renderStats() {
var view = getView();
var t = view.totals[state.window] || {};
var sa = (view.session_aggregates || {})[state.window] || {};
var providers = t.providers || {};
var cc = (providers["claude-code"] || {}).activations || 0;
var ci = (providers["cursor-ide"] || {}).activations || 0;
var ratio = cc + ci > 0 ? (cc / (cc + ci)) * 100 : null;
var cards = [
{ label: "Activations", value: fmtInt(t.activations || 0),
sub: fmtInt(t.tiled_activations || 0) + " tiled · " + fmtInt(t.untiled_activations || 0) + " untiled" },
{ label: "Active users", value: fmtInt(t.users || 0),
sub: fmtInt(t.sessions || 0) + " sessions" },
{ label: "Provider split", value: ratio == null ? "—" : Math.round(ratio) + "% cc",
sub: fmtInt(cc) + " cc / " + fmtInt(ci) + " ci" },
{ label: "Tessl-managed share", value: t.activations ? Math.round((t.tiled_activations / t.activations) * 100) + "%" : "—",
sub: "of all skill activations" },
{ label: "Tessl MCP calls", value: fmtInt(sa.tessl_mcp_calls || 0),
sub: fmtInt(sa.tessl_skill_calls || 0) + " resolved skill calls" },
{ label: "Tessl CLI calls", value: fmtInt(sa.tessl_cli_calls || 0),
sub: "via Bash" },
{ label: "Total tool calls", value: fmtInt(sa.tool_calls || 0),
sub: fmtInt(sa.messages || 0) + " agent messages" },
{ label: "Distinct tiles seen", value: fmtInt(view.tiles ? view.tiles.length : 0),
sub: fmtInt(view.skills ? view.skills.length : 0) + " (tile, skill) pairs detailed" },
];
var grid = document.getElementById("stats-grid");
grid.innerHTML = "";
cards.forEach(function (c) {
var card = el("div", { class: "stat-card" });
card.appendChild(el("div", { class: "label" }, c.label));
card.appendChild(el("div", { class: "value" }, c.value));
card.appendChild(el("div", { class: "sub" }, c.sub));
grid.appendChild(card);
});
}
// ── Generic sortable table ───────────────────────────────────
function makeTable(rows, columns, opts) {
opts = opts || {};
var key = opts.key || "tbl";
var sortState = state.sort[key] || (opts.defaultSort || { column: 0, dir: -1 });
var sorted = rows.slice();
sorted.sort(function (a, b) {
var col = columns[sortState.column];
var av = col.value(a), bv = col.value(b);
if (av == null && bv == null) return 0;
if (av == null) return 1;
if (bv == null) return -1;
if (typeof av === "number" && typeof bv === "number") return (av - bv) * sortState.dir;
return String(av).localeCompare(String(bv)) * sortState.dir;
});
var table = el("table", { class: "data" });
var thead = el("thead");
var headRow = el("tr");
columns.forEach(function (col, i) {
var th = el("th", null, col.label);
if (i === sortState.column) th.classList.add(sortState.dir === 1 ? "sort-asc" : "sort-desc");
th.addEventListener("click", function () {
if (sortState.column === i) sortState.dir = -sortState.dir;
else { sortState.column = i; sortState.dir = -1; }
state.sort[key] = sortState;
renderAll();
});
headRow.appendChild(th);
});
thead.appendChild(headRow);
table.appendChild(thead);
var tbody = el("tbody");
if (sorted.length === 0) {
var tr = el("tr");
var td = el("td", { colspan: columns.length, class: "empty-state" }, "No rows.");
tr.appendChild(td);
tbody.appendChild(tr);
} else {
sorted.forEach(function (row) {
var tr = el("tr");
columns.forEach(function (col) {
var td = el("td", { class: col.cellClass ? col.cellClass(row) : "" });
var v = col.display ? col.display(row) : col.value(row);
td.textContent = v == null ? "—" : v;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
table.appendChild(tbody);
return table;
}
// ── Totals table ─────────────────────────────────────────────
function renderTotalsTable() {
var view = getView();
var rows = data.windows.map(function (d) {
var k = d + "d";
var t = view.totals[k] || {};
var p = t.providers || {};
var cc = p["claude-code"] || {};
var ci = p["cursor-ide"] || {};
return {
window: k, t: t,
cc_a: cc.activations || 0, cc_u: cc.users || 0,
ci_a: ci.activations || 0, ci_u: ci.users || 0,
};
});
var table = makeTable(rows, [
{ label: "Window", value: function (r) { return r.window; } },
{ label: "Activations", value: function (r) { return r.t.activations || 0; }, cellClass: function () { return "num"; } },
{ label: "Users", value: function (r) { return r.t.users || 0; }, cellClass: function () { return "num"; } },
{ label: "Sessions", value: function (r) { return r.t.sessions || 0; }, cellClass: function () { return "num"; } },
{ label: "Tiled", value: function (r) { return r.t.tiled_activations || 0; }, cellClass: function () { return "num"; } },
{ label: "Untiled", value: function (r) { return r.t.untiled_activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: "cc acts", value: function (r) { return r.cc_a; }, cellClass: function () { return "num dim"; } },
{ label: "ci acts", value: function (r) { return r.ci_a; }, cellClass: function () { return "num dim"; } },
], { key: "totals", defaultSort: { column: 1, dir: -1 } });
var wrap = document.getElementById("totals-table");
wrap.innerHTML = "";
wrap.appendChild(table);
}
// ── Tiles ────────────────────────────────────────────────────
function renderTilesTable() {
var view = getView();
var filter = (document.getElementById("tiles-filter").value || "").toLowerCase();
var rows = (view.tiles || [])
.filter(function (t) { return !filter || t.tile.toLowerCase().indexOf(filter) >= 0; });
var table = makeTable(rows, [
{ label: "Tile", value: function (r) { return r.tile; }, cellClass: function () { return "tile"; } },
{ label: data.windows[0] + "d acts", value: function (r) { return (pickWindow(r, data.windows[0] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.primary_window_days + "d acts", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).activations || 0; }, cellClass: function () { return "num"; } },
{ label: data.primary_window_days + "d users", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).users || 0; }, cellClass: function () { return "num"; } },
{ label: data.primary_window_days + "d sessions", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).sessions || 0; }, cellClass: function () { return "num dim"; } },
{ label: (data.windows[data.windows.length - 1]) + "d acts", value: function (r) { return (pickWindow(r, data.windows[data.windows.length - 1] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
], { key: "tiles", defaultSort: { column: 2, dir: -1 } });
var wrap = document.getElementById("tiles-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("tiles-meta").textContent =
rows.length + " of " + (view.tiles || []).length + " tiles" +
(filterActive() ? " (filtered by repo)" : "");
}
// ── Skills ───────────────────────────────────────────────────
function renderSkillsTable() {
var view = getView();
var filter = (document.getElementById("skills-filter").value || "").toLowerCase();
var rows = (view.skills || []).filter(function (s) {
if (!filter) return true;
return (s.tile + "/" + s.name).toLowerCase().indexOf(filter) >= 0;
});
var table = makeTable(rows, [
{ label: "Tile", value: function (r) { return r.tile; }, cellClass: function () { return "tile"; } },
{ label: "Skill", value: function (r) { return r.name; }, cellClass: function () { return "skill"; } },
{ label: data.windows[0] + "d acts", value: function (r) { return (pickWindow(r, data.windows[0] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.primary_window_days + "d acts", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).activations || 0; }, cellClass: function () { return "num"; } },
{ label: data.primary_window_days + "d users", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).users || 0; }, cellClass: function () { return "num"; } },
{ label: "cc", value: function (r) { return (r.providers || {})["claude-code"] || 0; }, cellClass: function () { return "num dim"; } },
{ label: "ci", value: function (r) { return (r.providers || {})["cursor-ide"] || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.windows[data.windows.length - 1] + "d acts", value: function (r) { return (pickWindow(r, data.windows[data.windows.length - 1] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
], { key: "skills", defaultSort: { column: 3, dir: -1 } });
var wrap = document.getElementById("skills-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("skills-meta").textContent =
rows.length + " of " + (view.skills || []).length + " skills" +
(filterActive() ? " (filtered by repo)" : "");
}
// ── Loaded ───────────────────────────────────────────────────
// Always all-repos — `installedSkills[]` is per-user, not per-repo.
function renderLoadedTable() {
document.getElementById("loaded-filter-note").style.display =
filterActive() ? "inline" : "none";
var filter = (document.getElementById("loaded-filter").value || "").toLowerCase();
var rows = (data.loaded_skills || []).filter(function (s) {
if (!filter) return true;
var key = (s.tile || "") + "/" + s.name;
return key.toLowerCase().indexOf(filter) >= 0;
});
var table = makeTable(rows, [
{ label: "Tile", value: function (r) { return r.tile || "(no tile)"; }, cellClass: function (r) { return r.tile ? "tile" : "tile dim"; } },
{ label: "Skill", value: function (r) { return r.name; }, cellClass: function () { return "skill"; } },
{ label: "Scope", value: function (r) { return r.scope || ""; }, cellClass: function (r) { return "scope " + (r.scope || ""); } },
{ label: data.windows[0] + "d loaders", value: function (r) { return (pickWindow(r, data.windows[0] + "d") || {}).users || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.primary_window_days + "d loaders", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).users || 0; }, cellClass: function () { return "num"; } },
{ label: data.windows[data.windows.length - 1] + "d loaders", value: function (r) { return (pickWindow(r, data.windows[data.windows.length - 1] + "d") || {}).users || 0; }, cellClass: function () { return "num dim"; } },
], { key: "loaded", defaultSort: { column: 4, dir: -1 } });
var wrap = document.getElementById("loaded-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("loaded-meta").textContent =
rows.length + " of " + (data.loaded_skills || []).length + " (tile, skill, scope) rows";
}
// ── Untiled ──────────────────────────────────────────────────
function renderUntiledTable() {
var view = getView();
var filter = (document.getElementById("untiled-filter").value || "").toLowerCase();
var rows = (view.untiled_skills || []).filter(function (s) {
return !filter || s.name.toLowerCase().indexOf(filter) >= 0;
});
var table = makeTable(rows, [
{ label: "Skill", value: function (r) { return r.name; }, cellClass: function () { return "skill"; } },
{ label: data.windows[0] + "d acts", value: function (r) { return (pickWindow(r, data.windows[0] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.primary_window_days + "d acts", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).activations || 0; }, cellClass: function () { return "num"; } },
{ label: data.primary_window_days + "d users", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).users || 0; }, cellClass: function () { return "num"; } },
{ label: data.windows[data.windows.length - 1] + "d acts", value: function (r) { return (pickWindow(r, data.windows[data.windows.length - 1] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
], { key: "untiled", defaultSort: { column: 2, dir: -1 } });
var wrap = document.getElementById("untiled-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("untiled-meta").textContent =
rows.length + " of " + (view.untiled_skills || []).length + " skills" +
(filterActive() ? " (filtered by repo)" : "");
}
// ── MCP ──────────────────────────────────────────────────────
function renderMcpTable() {
var view = getView();
var rows = (view.mcp_tools || []);
var table = makeTable(rows, [
{ label: "Tool", value: function (r) { return r.tool; }, cellClass: function () { return "skill"; } },
{ label: data.windows[0] + "d calls", value: function (r) { return (pickWindow(r, data.windows[0] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.primary_window_days + "d calls", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).activations || 0; }, cellClass: function () { return "num"; } },
{ label: data.primary_window_days + "d users", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).users || 0; }, cellClass: function () { return "num"; } },
{ label: data.primary_window_days + "d sessions", value: function (r) { return (pickWindow(r, data.primary_window_days + "d") || {}).sessions || 0; }, cellClass: function () { return "num dim"; } },
{ label: data.windows[data.windows.length - 1] + "d calls", value: function (r) { return (pickWindow(r, data.windows[data.windows.length - 1] + "d") || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
], { key: "mcp", defaultSort: { column: 2, dir: -1 } });
var wrap = document.getElementById("mcp-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("mcp-meta").textContent =
rows.length + " distinct tools" +
(filterActive() ? " (filtered by repo)" : "");
}
// ── Sessions ─────────────────────────────────────────────────
function renderSessionsTable() {
var view = getView();
var rows = data.windows.map(function (d) {
var sa = (view.session_aggregates || {})[d + "d"] || {};
return Object.assign({ window: d + "d" }, sa);
});
var keys = ["sessions", "users", "messages", "tool_calls", "skill_calls", "tessl_skill_calls", "tessl_mcp_calls", "tessl_cli_calls"];
var cols = [{ label: "Window", value: function (r) { return r.window; } }];
keys.forEach(function (k) {
cols.push({
label: k,
value: function (r) { return r[k] || 0; },
cellClass: function () { return "num"; },
});
});
var table = makeTable(rows, cols, { key: "sessions", defaultSort: { column: 1, dir: -1 } });
var wrap = document.getElementById("sessions-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("sessions-meta").textContent =
rows.length + " windows";
}
// ── Repo chip filter ─────────────────────────────────────────
function renderRepoChips() {
var wrapper = document.getElementById("repo-filter");
var chipsWrap = document.getElementById("repo-filter-chips");
var summary = document.getElementById("repo-filter-summary");
var showMore = document.getElementById("repo-show-more");
chipsWrap.innerHTML = "";
if (!data.repos || data.repos.length === 0) {
wrapper.style.display = "none";
return;
}
wrapper.style.display = "";
var w = state.window;
// Sort chips by primary-window activations (matches the Repos table order).
var primaryW = data.primary_window_days + "d";
var rows = data.repos.slice().sort(function (a, b) {
var av = (a.windows[primaryW] || {}).activations || 0;
var bv = (b.windows[primaryW] || {}).activations || 0;
return bv - av;
});
var totalRepos = rows.length;
var collapsedLimit = REPO_EXPAND_THRESHOLD;
var showAll = state.repoExpanded || totalRepos <= collapsedLimit;
var visible = showAll ? rows : rows.slice(0, collapsedLimit);
visible.forEach(function (r) {
var excluded = state.excludedRepos.has(r.repo);
var acts = (r.windows[w] || {}).activations || 0;
var classes = "chip"
+ (excluded ? " excluded" : "")
+ (r.synthetic ? " synthetic" : "");
var chip = el("button", {
class: classes,
title: r.synthetic
? "Events that matched the org filter but had a NULL gitRepo. Untick to remove this baseline from totals and rollups."
: r.repo,
});
chip.appendChild(document.createTextNode(r.repo));
chip.appendChild(el("span", { class: "count" }, fmtInt(acts)));
chip.addEventListener("click", function () {
if (state.excludedRepos.has(r.repo)) state.excludedRepos.delete(r.repo);
else state.excludedRepos.add(r.repo);
saveExcluded(state.excludedRepos);
renderAll();
});
chipsWrap.appendChild(chip);
});
if (totalRepos > collapsedLimit) {
showMore.style.display = "inline-block";
showMore.textContent = state.repoExpanded
? "show fewer"
: ("show " + (totalRepos - collapsedLimit) + " more…");
} else {
showMore.style.display = "none";
}
var includedCount = totalRepos - state.excludedRepos.size;
if (state.excludedRepos.size === 0) {
summary.innerHTML = "<strong>" + totalRepos + "</strong> repos · all included";
} else {
summary.innerHTML =
"<strong>" + includedCount + "</strong> of " + totalRepos +
" repos included · <strong>" + state.excludedRepos.size + "</strong> excluded";
}
}
// ── Repos table ──────────────────────────────────────────────
function renderReposTable() {
var filter = (document.getElementById("repos-filter").value || "").toLowerCase();
var rows = (data.repos || [])
.filter(function (r) { return !filter || r.repo.toLowerCase().indexOf(filter) >= 0; })
.map(function (r) {
var excluded = state.excludedRepos.has(r.repo);
return Object.assign({ excluded: excluded }, r);
});
var pw = data.primary_window_days + "d";
var firstW = data.windows[0] + "d";
var lastW = data.windows[data.windows.length - 1] + "d";
var table = makeTable(rows, [
{
label: "Repo",
value: function (r) { return r.repo; },
cellClass: function (r) {
var c = r.excluded ? "tile dim" : "tile";
if (r.synthetic) c += " synthetic-row";
return c;
},
},
{ label: firstW + " acts", value: function (r) { return (r.windows[firstW] || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: pw + " acts", value: function (r) { return (r.windows[pw] || {}).activations || 0; }, cellClass: function () { return "num"; } },
{ label: pw + " users", value: function (r) { return (r.windows[pw] || {}).users || 0; }, cellClass: function () { return "num"; } },
{ label: pw + " sessions", value: function (r) { return (r.windows[pw] || {}).sessions || 0; }, cellClass: function () { return "num dim"; } },
{ label: pw + " tiled", value: function (r) { return (r.windows[pw] || {}).tiled_activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: pw + " untiled", value: function (r) { return (r.windows[pw] || {}).untiled_activations || 0; }, cellClass: function () { return "num dim"; } },
{ label: lastW + " acts", value: function (r) { return (r.windows[lastW] || {}).activations || 0; }, cellClass: function () { return "num dim"; } },
], { key: "repos", defaultSort: { column: 2, dir: -1 } });
var wrap = document.getElementById("repos-table");
wrap.innerHTML = "";
wrap.appendChild(table);
document.getElementById("repos-meta").textContent =
rows.length + " of " + (data.repos || []).length + " repos";
}
// ── Footer (data caveats — facts only, no judgment) ──────────
function renderFooter() {
var foot = document.getElementById("footer");
foot.innerHTML = "";
var caveats = [
"An <strong>activation</strong> here is one <code>cli:agent-signals:skill-activation</code> event, fired when:",
"<ul style='margin: 6px 0 6px 20px;'><li>The Claude Code <code>Skill</code> tool is invoked (covers both user slash commands and agent-organic invocations on modern CC versions)</li>" +
"<li>A user types a <code>/foo</code> slash command in Cursor IDE</li>" +
"<li>The Cursor IDE agent calls <code>read_file*</code> against any <code>.claude/skills/<name>/SKILL.md</code>, <code>.cursor/skills/<name>/SKILL.md</code>, or <code>.tessl/tiles/<ws>/<tile>/skills/<name>/SKILL.md</code></li></ul>",
"It does <span class='caveat'>NOT</span> include: Cursor <code>@SKILL.md</code> mentions; raw Claude Code <code>Read</code> of a SKILL.md (currently a normalizer gap); slash-command flows from Claude Code versions older than the canonical <code>Skill</code> tool; activations from agent harnesses other than claude-code and cursor-ide; or developer reading the file directly in their editor.",
"<strong>Loaded</strong> means the skill appeared in <code>installedSkills[]</code> on at least one activation event in the window. Users with zero activation events contribute no loaded-skill rows even if they have skills installed.",
"When the <strong>repo filter</strong> is active, the math is subtractive for additive fields and clamped-lower-bound for non-additive ones. <em>activations</em>, <em>tiled / untiled</em>, and <em>session_aggregates</em> fields are additive across repos so subtraction is exact. <em>users</em> and <em>sessions</em> (in top-line totals) are <span class='caveat'>not additive</span> — the same user can appear in multiple repos — so we display <code>max(subtractive_total, max(included_repos.users))</code>: a defensible lower bound that's never lower than any single included repo's count. The true count for a multi-repo selection would need a fresh PostHog query. The <code>Loaded skills</code> section ignores the repo filter entirely (install footprint is per-user, not per-repo).",
"The <em><span style='color: var(--purple)'>(no repo)</span></em> chip is a synthetic bucket — events that matched the org filter but had a NULL <code>gitRepo</code> (typically agent runs outside a git checkout, or older clients that didn't tag <code>gitRepo</code>). Untick it to subtract this baseline from every total and rollup. It's derived client-side as <code>totals − sum(repos[])</code>, so no extra PostHog query is needed.",
"Reference dashboard: <a href='https://us.posthog.com/project/" + data.source.project_id + "/dashboard/" + (data.source.dashboard_id_for_reference || "") + "' target='_blank'>posthog dashboard " + (data.source.dashboard_id_for_reference || "—") + "</a>.",
];
caveats.forEach(function (c) {
var p = el("p");
p.innerHTML = c;
foot.appendChild(p);
});
}
function renderAll() {
renderMeta();
renderFilterBanner();
renderRepoChips();
renderWindowTabs();
renderStats();
renderTotalsTable();
renderReposTable();
renderTilesTable();
renderSkillsTable();
renderLoadedTable();
renderUntiledTable();
renderMcpTable();
renderSessionsTable();
renderFooter();
}
document.getElementById("tiles-filter").addEventListener("input", renderTilesTable);
document.getElementById("skills-filter").addEventListener("input", renderSkillsTable);
document.getElementById("loaded-filter").addEventListener("input", renderLoadedTable);
document.getElementById("untiled-filter").addEventListener("input", renderUntiledTable);
document.getElementById("repos-filter").addEventListener("input", renderReposTable);
document.getElementById("repo-include-all").addEventListener("click", function () {
state.excludedRepos.clear();
saveExcluded(state.excludedRepos);
renderAll();
});
document.getElementById("repo-exclude-all").addEventListener("click", function () {
ALL_REPOS.forEach(function (r) { state.excludedRepos.add(r); });
saveExcluded(state.excludedRepos);
renderAll();
});
document.getElementById("repo-show-more").addEventListener("click", function () {
state.repoExpanded = !state.repoExpanded;
renderRepoChips();
});
renderAll();
})();
</script>
</body>
</html>