CtrlK
BlogDocsLog inGet started
Tessl Logo

tessleng/skill-insights

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

1.44x
Quality

90%

Does it follow best practices?

Impact

97%

1.44x

Average score across 2 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

org-usage-report-template.htmlreferences/

<!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/&lt;name&gt;/SKILL.md</code>, <code>.cursor/skills/&lt;name&gt;/SKILL.md</code>, or <code>.tessl/tiles/&lt;ws&gt;/&lt;tile&gt;/skills/&lt;name&gt;/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>

README.md

tile.json