clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

main
forge / site / _includes / insights-body.njk
insights-body.njk 9.5K
1{#- The insights page's body, for the forge (insights.njk) and for a repo2 (repo/insights.njk): stat tiles and charts that insights.ts draws from3 insights.json, each chart with its table, and the Parquet files behind4 them. A repo's page has `p`; the exports are in the page's directory. -#}5{% import "ui.njk" as ui %}6{% set forgeWide = not p %}7{% set base = '/insights/' if forgeWide else p.repo.urls.insights.href %}8{% css %}9/* insights (insights.ts): the chart colours are tokens (--viz-*, checked10 for colour-blind readers against both pages); text keeps the page's own. */11.insights .ins-meta { margin: var(--space-1) 0 var(--space-4); }12/* The totals: tiles in one panel, a hairline between them. */13.insights .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--hairline); background: var(--border); }14.insights .kpis:empty { display: none; }15.insights .kpi { background: var(--raised); padding: var(--space-3); }16.insights .kpi .label { font-size: var(--text-sm); color: var(--muted); }17.insights .kpi .value { font: var(--fw-medium) var(--text-xl)/1.2 var(--mono); margin-top: var(--space-1); font-variant-numeric: tabular-nums; }18.insights .kpi .sub { color: var(--muted); font-size: var(--text-sm); }19/* Each chart is a panel: its name and what it counts in the head strip,20 the plot, then its table folded. */21.insights .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); gap: var(--space-4); margin: 0 0 var(--space-4); }22.insights figure.panel { margin: 0; position: relative; }23/* A repo's activity grid: a chart panel as wide as the page, over them. */24.insights figure.activity-panel { margin-bottom: var(--space-4); }25.insights .plot { position: relative; margin: var(--space-3) var(--space-3) 0; }26.insights .plot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }27.insights svg { display: block; width: 100%; overflow: visible; font: var(--fw-regular) var(--text-xs) var(--mono); font-variant-numeric: tabular-nums; }28.insights svg .grid { stroke: var(--viz-grid); stroke-width: 1; shape-rendering: crispEdges; }29.insights svg .base { stroke: var(--viz-axis); stroke-width: 1; shape-rendering: crispEdges; }30.insights svg .tick { fill: var(--muted); }31.insights svg .s1 { fill: var(--viz-1); }32.insights svg .fail { fill: var(--viz-fail); }33.insights svg .dot { stroke: var(--raised); stroke-width: 2; }34.insights svg .line { fill: none; stroke: var(--viz-1); stroke-width: 1.5; stroke-linejoin: round; }35.insights svg .hair { stroke: var(--muted); stroke-width: 1; shape-rendering: crispEdges; }36.insights svg .on { filter: brightness(1.15); }37.insights svg .mark-label { fill: var(--fg); paint-order: stroke; stroke: var(--raised); stroke-width: 3px; stroke-linejoin: round; }38.insights .empty-chart { color: var(--muted); font-size: var(--text-sm); padding: var(--space-8) 0; text-align: center; }39.insights .legend { display: flex; gap: var(--space-4); font-size: var(--text-sm); color: var(--muted); margin: var(--space-1) var(--space-3) 0; }40.insights .legend i { display: inline-block; width: 10px; height: 10px; margin-right: var(--space-1); vertical-align: -1px; }41.insights .tip { position: absolute; z-index: 2; pointer-events: none; background: var(--raised); border: var(--hairline) solid var(--border); box-shadow: var(--shadow); padding: var(--space-1) var(--space-2); font-size: var(--text-sm); white-space: nowrap; }42.insights .tip b { font-size: var(--text-md); }43.insights .tip .k { display: inline-block; width: 10px; height: 2px; margin-right: var(--space-2); vertical-align: middle; }44.insights details.table { margin-top: var(--space-2); padding: var(--space-2) var(--space-3); border-top: var(--hairline) solid var(--border); font-size: var(--text-sm); }45.insights details.table summary { color: var(--muted); cursor: pointer; font-weight: var(--fw-medium); }46.insights details.table table { border-collapse: collapse; margin-top: var(--space-1); font-variant-numeric: tabular-nums; }47.insights details.table td, .insights details.table th { padding: var(--space-half) var(--space-3) var(--space-half) 0; text-align: left; border-bottom: var(--hairline) solid var(--border); }48.insights details.table th { font-weight: var(--fw-medium); color: var(--muted); }49.insights details.table .scroll { max-height: 240px; overflow: auto; }50.insights .data td.mono { font-size: var(--text-sm); }51.insights pre.cmd { margin: 0; overflow-x: auto; white-space: pre; padding: var(--space-3); border-top: var(--hairline) solid var(--border); font-size: var(--text-sm); }52@media (forced-colors: active) {53 .insights svg .s1, .insights svg .fail { fill: CanvasText; }54 .insights svg .line { stroke: CanvasText; }55}56{% endcss %}57<div class="insights" data-insights="{{ base }}insights.json"{% if forgeWide %} data-forge="1"{% endif %}>58<p class="desc ins-meta" id="ins-meta">From the forge's lake.<noscript> The charts need JavaScript; the numbers are in the files below.</noscript></p>59<div class="panel kpis" id="ins-kpis"></div>60{% if not forgeWide and p.activity %}61{# The repo's activity grid (ui.activity, still), a chart like the others:62 how much happened each day, nothing to pick; its total and what it63 counts in the head strip. #}64{% set a = p.activity %}65<figure class="panel activity-panel"><figcaption class="panel-head"><h3 class="act-total">{{ a.grid.total | thousands }} {{ 'event' if a.grid.total == 1 else 'events' }}</h3><span class="grow">in the last year: pushes, landed reviews, releases, site deploys and ci results</span></figcaption><div class="panel-body">{{ ui.activity(a.grid, 'events a day', 'data-src="activity.json"', true) }}</div></figure>66{% endif %}67<div class="charts">68<figure class="panel" data-chart="builds"><figcaption class="panel-head"><h3>build time</h3><span class="grow">seconds per build of {{ 'the pages' if forgeWide else "this repo's pages" }}</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>69<figure class="panel" data-chart="pushes"><figcaption class="panel-head"><h3>pushes</h3><span class="grow">pushes a <span class="ins-bucket">day</span></span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>70<figure class="panel" data-chart="land"><figcaption class="panel-head"><h3>time to land</h3><span class="grow">hours from a review's first push to landing, by when it landed</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>71<figure class="panel" data-chart="revisions"><figcaption class="panel-head"><h3>revisions per review</h3><span class="grow">reviews by how many revisions they were pushed in</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>72<figure class="panel" data-chart="ci"><figcaption class="panel-head"><h3>ci results</h3><span class="grow">attestations a <span class="ins-bucket">day</span>, passed and failed</span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>73<figure class="panel" data-chart="ciTime"><figcaption class="panel-head"><h3>ci duration</h3><span class="grow">seconds per run of <code>.forge/ci</code></span></figcaption><div class="plot"></div><details class="table"><summary>table</summary></details></figure>74</div>75{% if not forgeWide %}76{# A chart a metric (insights.ts draws them from insights.json's metrics),77 shown once there is one. #}78<section id="ins-metrics" hidden>79{{ ui.heading('metrics', null, 'what .forge/ci reported for each commit on ' + p.repo.defaultBranch) }}80<div class="charts"></div>81</section>82{% endif %}83{% if forgeWide %}84{% call ui.panel('<b>by repo</b>') %}85<table class="list nav" id="ins-repos">86<thead><tr><th>repository</th><th class="num">pushes</th><th class="num">builds</th><th class="num">median build</th><th class="num">landed</th><th class="num">ci passed</th><th>last push</th></tr></thead>87<tbody></tbody>88</table>89{% endcall %}90{% endif %}91{% set head %}<b>data</b><span class="grow">the rows behind the charts{{ ', for public repos only' if forgeWide else '' }}, as Parquet</span>{% endset %}92{% call ui.panel(head, 'data') %}93{% call ui.listTable([], 'data') %}94<tr><td class="mono nowrap"><a href="{{ base }}insights.json">insights.json</a></td><td class="main">what the charts draw</td></tr>95<tr><td class="mono nowrap"><a href="{{ base }}events.parquet">events.parquet</a></td><td class="main">pushes, comments, approvals and builds as they happened</td></tr>96<tr><td class="mono nowrap"><a href="{{ base }}builds.parquet">builds.parquet</a></td><td class="main">each page build: time spent, pages written</td></tr>97<tr><td class="mono nowrap"><a href="{{ base }}reviews.parquet">reviews.parquet</a></td><td class="main">each review's lifecycle: created, revisions, approvals, landed or closed</td></tr>98<tr><td class="mono nowrap"><a href="{{ base }}ci.parquet">ci.parquet</a></td><td class="main">each CI attestation: commit, tree, result, duration, system, signer</td></tr>99<tr><td class="mono nowrap"><a href="{{ base }}ci_metrics.parquet">ci_metrics.parquet</a></td><td class="main">each metric an attestation reported: name, value, unit, with its commit and result</td></tr>100{% endcall %}101<p class="desc">DuckDB reads them over HTTP in byte ranges, so a query fetches only what it needs:</p>102<pre class="cmd">duckdb -c "SELECT result, count(*), median(duration_s) FROM '{{ forge.config.baseUrl or 'https://<this forge>' }}{{ base }}ci.parquet' GROUP BY result"</pre>103{% endcall %}104</div>