Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions scripts/update-metrics.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
"""Inject live release metrics into site/index.html at deploy time.
"""Inject live release metrics into the site at deploy time.

Counts CLI tagged releases (excludes app-v* releases, which are the menu bar
app's separate tag space) and the latest CLI version from the GitHub API, then
updates the stat card and JSON-LD in place. A failure leaves the page as-is:
slightly stale numbers are this page's failure mode, a broken deploy is not.
Idempotent. Stdlib only.
updates the pages in place: the landing's stat card and JSON-LD, and the
manual page's JSON-LD plus its version badge. A failure leaves the pages
as-is: slightly stale numbers are this site's failure mode, a broken deploy
is not. Idempotent. Stdlib only.
"""
import json
import os
Expand All @@ -15,6 +16,7 @@

REPO = "shrwnsan/tailroute-cli"
PAGE = os.path.join(os.path.dirname(__file__), "..", "site", "index.html")
DOCS_PAGE = os.path.join(os.path.dirname(__file__), "..", "site", "docs", "index.html")


def fetch_releases():
Expand All @@ -33,25 +35,35 @@ def main():
count = len(releases)
latest = releases[0]["tag_name"].lstrip("v") if releases else None
if not (0 < count < 500) or not latest:
print(f"update-metrics: implausible metrics (count={count}, latest={latest}) — leaving page as-is")
print(f"update-metrics: implausible metrics (count={count}, latest={latest}) — leaving pages as-is")
return 0

with open(PAGE) as f:
html = f.read()
original = html

html = _replace_releases(html, count)
html = _replace_version(html, latest)

today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
html = _replace_span(html, "updated", f" \u00b7 site updated {today}")

if html == original:
print(f"update-metrics: page already current (releases={count}, version={latest})")
def landing(html):
html = _replace_releases(html, count)
html = _replace_version(html, latest)
return _replace_span(html, "updated", f" \u00b7 site updated {today}")

def manual(html):
html = _replace_version(html, latest)
return _replace_span(html, "cli-version", f"v{latest}")

touched = 0
for path, inject in ((PAGE, landing), (DOCS_PAGE, manual)):
with open(path) as f:
html = f.read()
updated = inject(html)
if updated == html:
continue
with open(path, "w") as f:
f.write(updated)
touched += 1

if not touched:
print(f"update-metrics: pages already current (releases={count}, version={latest})")
return 0
with open(PAGE, "w") as f:
f.write(html)
print(f"update-metrics: injected releases={count}, version={latest}, updated={today}")
print(f"update-metrics: injected releases={count}, version={latest}, updated={today} ({touched} page(s))")
return 0


Expand All @@ -75,7 +87,7 @@ def _replace_version(html, version):

def _replace_span(html, name, text):
import re
pattern = re.compile(rf'(<span data-metric="{name}">)(.*?)(</span>)', re.S)
pattern = re.compile(rf'(<span[^>]*\sdata-metric="{name}"[^>]*>)(.*?)(</span>)', re.S)
if not pattern.search(html):
print(f"update-metrics: WARNING span {name!r} not found")
return html
Expand Down
9 changes: 5 additions & 4 deletions site/assets/js/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@
{ x: 520.2, y: 143.8, d: "2026-09-01", v: 11 }, { x: 525.9, y: 67.1, d: "2026-09-02", v: 23 },
{ x: 531.6, y: 47.9, d: "2026-09-03", v: 26 }, { x: 555.5, y: 41.5, d: "2026-09-11", v: 27 },
{ x: 588.3, y: 35.1, d: "2026-09-22", v: 28 }, { x: 591.4, y: 28.7, d: "2026-09-23", v: 29 },
{ x: 594.4, y: 22.4, d: "2026-09-24", v: 30 }, { x: 612.3, y: 16, d: "2026-09-30", v: 31 }
{ x: 594.4, y: 22.4, d: "2026-09-24", v: 30 }, { x: 612.3, y: 16, d: "2026-09-30", v: 31 },
{ x: 698, y: 9.6, d: "2026-10-27", v: 32 }
];
var svg = document.querySelector(".chart-card svg");
var tip = document.getElementById("chart-tip"), xh = document.getElementById("xhair"), hd = document.getElementById("hoverdot");
if (svg) {
svg.addEventListener("mousemove", function (ev) {
var r = svg.getBoundingClientRect();
var sx = (ev.clientX - r.left) * (640 / r.width);
var sx = (ev.clientX - r.left) * (728 / r.width);
var best = PTS[0], bd = Math.abs(sx - PTS[0].x);
for (var i = 1; i < PTS.length; i++) { var d = Math.abs(sx - PTS[i].x); if (d < bd) { bd = d; best = PTS[i]; } }
xh.setAttribute("x1", best.x); xh.setAttribute("x2", best.x); xh.setAttribute("opacity", "1");
hd.setAttribute("cx", best.x); hd.setAttribute("cy", best.y); hd.setAttribute("opacity", "1");
tip.innerHTML = best.d + " · <b>" + best.v + "</b> release" + (best.v > 1 ? "s" : "");
tip.style.left = (best.x / 640 * r.width) + "px";
tip.style.top = (best.y / 250 * r.height) + "px";
tip.style.left = (best.x / 728 * r.width) + "px";
tip.style.top = ((best.y + 10) / 260 * r.height) + "px";
tip.style.opacity = "1";
});
svg.addEventListener("mouseleave", function () {
Expand Down
18 changes: 15 additions & 3 deletions site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"operatingSystem": "macOS 15+",
"applicationCategory": "UtilitiesApplication",
"description": "Fail-safe macOS daemon that toggles Tailscale MagicDNS when a VPN connects—internet through the VPN, peers through the mesh. Includes a SOCKS5 mesh proxy and launchd-managed browser tunnels.",
"softwareVersion": "0.8.17",
"softwareVersion": "0.9.0",
"license": "https://github.com/shrwnsan/tailroute-cli/blob/main/LICENSE",
"url": "https://tailroute.app/docs/",
"codeRepository": "https://github.com/shrwnsan/tailroute-cli",
Expand Down Expand Up @@ -203,7 +203,7 @@
<div class="title-row">
<h1><a href="../" aria-label="tailroute home">tailroute<span class="dot">.</span></a></h1>
<span class="badge" title="pre-v1.0 software">alpha</span>
<span class="badge free">v0.8.17</span>
<span data-metric="cli-version" class="badge free">v0.9.0</span>
</div>
<p class="meta">tailroute-cli manual · apache-2.0 · macOS 15+ · free &amp; open source</p>
<p class="toc"><span class="toc-label">On this page</span> ·
Expand Down Expand Up @@ -304,6 +304,18 @@ <h2 id="browser-tunnels">Browser tunnels</h2>
<div><span class="p">$</span> tailroute tunnel add mypeer <button data-copy="tailroute tunnel add mypeer">copy</button></div>
</div>
<p class="note indent">→ https://mypeer.yourtailnet.ts.net:8443</p>
<p>Need a tunnel gone for a while? <code>tailroute tunnel stop mypeer</code> parks
it without tearing anything down: the URL refuses connections, but the
registry entry and hosts mapping survive, and the parked state outlasts
logins and reboots. <code>tailroute tunnel start mypeer</code> resumes it
with the same local port and the same URL—no sudo, no re-registration.
Both take <code>--all</code>; <code>tunnel status</code> shows a parked
tunnel as <code>stopped (by request)</code>, and <code>tunnel restart</code>
refuses to implicitly start one.</p>
<div class="synopsis">
<div><span class="p">$</span> tailroute tunnel stop mypeer <button data-copy="tailroute tunnel stop mypeer">copy</button></div>
<div><span class="p">$</span> tailroute tunnel start mypeer <button data-copy="tailroute tunnel start mypeer">copy</button></div>
</div>

<h2 id="what-it-cannot-fix">What it cannot fix</h2>
<p>VPN Network Extensions may still block traffic to Tailscale's CGNAT range
Expand All @@ -321,7 +333,7 @@ <h2 id="security">Security</h2>
(<code>/var/db/tailroute/installed.checksums</code>) verified before any library loads.</li>
<li>No telemetry, no external network calls, no account, no traffic logging.</li>
<li>Interface names validated against <code>utun[0-9]+</code>; absolute paths only.</li>
<li>417 tests as of v0.8.17. Source: <a href="https://github.com/shrwnsan/tailroute-cli">github.com/shrwnsan/tailroute-cli</a></li>
<li>529 tests as of v0.9.0. Source: <a href="https://github.com/shrwnsan/tailroute-cli">github.com/shrwnsan/tailroute-cli</a></li>
</ul>

<h2 id="diagnostics">Diagnostics</h2>
Expand Down
39 changes: 20 additions & 19 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"name": "tailroute-cli",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "macOS",
"softwareVersion": "0.8.17",
"softwareVersion": "0.9.0",
"license": "https://www.apache.org/licenses/LICENSE-2.0",
"url": "https://tailroute.app/",
"description": "A fail-safe macOS daemon that toggles Tailscale's MagicDNS when your VPN connects—internet through the VPN, peers through the mesh. No telemetry. Apache-2.0 CLI.",
Expand Down Expand Up @@ -187,8 +187,8 @@ <h1 class="reveal d1"><em>Keep working.</em><br>VPN up, tailnet&nbsp;up.</h1>
</div>

<dl class="band reveal">
<div class="stat"><dt>tagged releases · public since mar ’26</dt><dd data-metric="releases"><i>31</i></dd></div>
<div class="stat"><dt>shell tests · 348 → 417 in five weeks</dt><dd>417</dd></div>
<div class="stat"><dt>tagged releases · public since mar ’26</dt><dd data-metric="releases"><i>32</i></dd></div>
<div class="stat"><dt>shell tests · 348 → 529 in eight weeks</dt><dd>529</dd></div>
<div class="stat"><dt>telemetry · external calls · accounts</dt><dd><i>0</i></dd></div>
<div class="stat"><dt>forever · apache-2.0 core</dt><dd><i>$0</i></dd></div>
</dl>
Expand Down Expand Up @@ -251,7 +251,7 @@ <h3>Three states, zero judgment calls</h3>
<div class="resp"><h3><i>03</i>Self-verifying install</h3>
<p>An install-time <b>SHA-256 integrity manifest is verified before any library loads</b>—tampered files
refuse to start, and say what changed. Strict validation, absolute paths, fail-safe on ambiguity,
417 tests and counting.</p></div>
529 tests and counting.</p></div>
</div>

<div class="b-card b12 reveal d1">
Expand Down Expand Up @@ -292,13 +292,13 @@ <h2 class="reveal d1">Shipped, not <em>promised.</em></h2>
<div class="chart-card reveal d1">
<div class="chart-head">
<span class="chart-title">Tagged releases—cumulative</span>
<span class="chart-hi">29 of 31 releases shipped since Aug 24</span>
<span class="chart-hi">30 of 32 releases shipped since Aug 24</span>
</div>
<svg viewBox="0 0 640 250" role="img" aria-label="Cumulative tagged releases, March to October 2026: two releases in spring, then 29 more from late August, ending at 31">
<line class="axis" x1="46" y1="214" x2="616" y2="214"/>
<line class="axis" x1="46" y1="150.1" x2="616" y2="150.1" opacity="0.5"/>
<line class="axis" x1="46" y1="86.1" x2="616" y2="86.1" opacity="0.5"/>
<line class="axis" x1="46" y1="22.4" x2="616" y2="22.4" opacity="0.5"/>
<svg viewBox="0 -10 728 260" role="img" aria-label="Cumulative tagged releases, March to October 2026: two releases in spring, then 30 more from late August, ending at 32">
<line class="axis" x1="46" y1="214" x2="702" y2="214"/>
<line class="axis" x1="46" y1="150.1" x2="702" y2="150.1" opacity="0.5"/>
<line class="axis" x1="46" y1="86.1" x2="702" y2="86.1" opacity="0.5"/>
<line class="axis" x1="46" y1="22.4" x2="702" y2="22.4" opacity="0.5"/>
<text class="axtext" x="38" y="217" text-anchor="end">0</text>
<text class="axtext" x="38" y="153" text-anchor="end">10</text>
<text class="axtext" x="38" y="89" text-anchor="end">20</text>
Expand All @@ -307,11 +307,11 @@ <h2 class="reveal d1">Shipped, not <em>promised.</em></h2>
<text class="axtext" x="290" y="232" text-anchor="middle">Jun</text>
<text class="axtext" x="520" y="232" text-anchor="middle">Sep 1</text>
<text class="axtext" x="612" y="232" text-anchor="end">Oct</text>
<path d="M46,207.6 L84.8,201.2 L493.6,194.8 L514.5,188.4 L520.2,143.8 L525.9,67.1 L531.6,47.9 L555.5,41.5 L588.3,35.1 L591.4,28.7 L594.4,22.4 L612.3,16 L612.3,214 L46,214 Z" fill="rgba(116,164,26,0.10)" stroke="none"/>
<path d="M46,207.6 L84.8,201.2 L493.6,194.8 L514.5,188.4 L520.2,143.8 L525.9,67.1 L531.6,47.9 L555.5,41.5 L588.3,35.1 L591.4,28.7 L594.4,22.4 L612.3,16" fill="none" stroke="var(--mark)" stroke-width="2" stroke-linejoin="round"/>
<circle cx="612.3" cy="16" r="4.5" fill="var(--mark)" stroke="var(--card)" stroke-width="2"/>
<text x="612" y="10" text-anchor="end" style="font-family:var(--mono);font-size:11px;font-weight:600" fill="var(--text)">31</text>
<line id="xhair" x1="0" y1="16" x2="0" y2="214" stroke="var(--line2)" stroke-width="1" opacity="0"/>
<path d="M46,207.6 L84.8,201.2 L493.6,194.8 L514.5,188.4 L520.2,143.8 L525.9,67.1 L531.6,47.9 L555.5,41.5 L588.3,35.1 L591.4,28.7 L594.4,22.4 L612.3,16 L698,9.6 L698,214 L46,214 Z" fill="rgba(116,164,26,0.10)" stroke="none"/>
<path d="M46,207.6 L84.8,201.2 L493.6,194.8 L514.5,188.4 L520.2,143.8 L525.9,67.1 L531.6,47.9 L555.5,41.5 L588.3,35.1 L591.4,28.7 L594.4,22.4 L612.3,16 L698,9.6" fill="none" stroke="var(--mark)" stroke-width="2" stroke-linejoin="round"/>
<circle cx="698" cy="9.6" r="4.5" fill="var(--mark)" stroke="var(--card)" stroke-width="2"/>
<text x="698" y="3.6" text-anchor="end" style="font-family:var(--mono);font-size:11px;font-weight:600" fill="var(--text)">32</text>
<line id="xhair" x1="0" y1="9.6" x2="0" y2="214" stroke="var(--line2)" stroke-width="1" opacity="0"/>
<circle id="hoverdot" r="4" fill="var(--text)" stroke="var(--card)" stroke-width="2" opacity="0"/>
</svg>
<div class="chart-tip" id="chart-tip"></div>
Expand All @@ -332,18 +332,19 @@ <h2 class="reveal d1">Shipped, not <em>promised.</em></h2>
<tr><td>2026-09-23</td><td>29</td></tr>
<tr><td>2026-09-24</td><td>30</td></tr>
<tr><td>2026-09-30</td><td>31</td></tr>
<tr><td>2026-10-27</td><td>32</td></tr>
</tbody>
</table>
</details>
</div>

<div class="reveal d2">
<div class="feed">
<div class="feed-item"><span class="feed-v">0.9.0</span><div><p class="feed-t"><b><code>tunnel stop</code> / <code>tunnel start</code></b>—park a tunnel without tearing it down: it stays parked across reboots, and start restores the same port and URL.</p><p class="feed-d">2026-10-27 · tests 528 → 529</p></div></div>
<div class="feed-item"><span class="feed-v">0.8.17</span><div><p class="feed-t"><b>Log rotation actually rotates</b>—staged at daemon startup for every layout; integrity manifest now layout-aware.</p><p class="feed-d">2026-09-30 · tests 408 → 417</p></div></div>
<div class="feed-item"><span class="feed-v">0.8.16</span><div><p class="feed-t"><b>Second peers stay removable</b>—hosts mappings land inside the managed block.</p><p class="feed-d">2026-09-24 · tests 406 → 408</p></div></div>
<div class="feed-item"><span class="feed-v">0.8.15</span><div><p class="feed-t"><b>Proxy registry self-heals against reality</b>—and the auth key no longer leaks through argv.</p><p class="feed-d">2026-09-23 · tests 383 → 405</p></div></div>
<div class="feed-item"><span class="feed-v">0.8.14</span><div><p class="feed-t"><b>Every tunnel verb accepts the ssh alias</b>—no more dead ends on <code>tunnel check prime</code>.</p><p class="feed-d">2026-09-23 · tests 367 → 383</p></div></div>
<div class="feed-item last"><span class="feed-v">0.8.11</span><div><p class="feed-t"><b><code>tunnel check</code></b>—the browser&rsquo;s truth: hosts → listener → TLS → HTTP, zero ssh.</p><p class="feed-d">2026-09-03 · read-only by construction</p></div></div>
<div class="feed-item last"><span class="feed-v">0.8.14</span><div><p class="feed-t"><b>Every tunnel verb accepts the ssh alias</b>—no more dead ends on <code>tunnel check prime</code>.</p><p class="feed-d">2026-09-23 · tests 367 → 383</p></div></div>
</div>
<p class="sprint"><b>22 releases in 72 hours</b> (Sep 1–3)—the v0.7→v0.8 hardening sprint: a burst of fixes after the 0.8.0 rewrite, each one tagged and tested. Full history: <a href="https://github.com/shrwnsan/tailroute-cli/blob/main/CHANGELOG.md" target="_blank" rel="noopener">CHANGELOG.md ↗</a></p>
</div>
Expand Down Expand Up @@ -456,7 +457,7 @@ <h2 class="reveal d1">Asked, answered</h2>
<summary>It runs as root—why should I trust it? <span class="pm" aria-hidden="true">+</span></summary>
<div class="a">Because it shows its work: install-time <b>SHA-256 integrity manifest</b> verified before any library loads,
strict input validation, absolute paths only, fail-safe on ambiguous state.
The whole thing is <a href="https://github.com/shrwnsan/tailroute-cli" target="_blank" rel="noopener">~7,400 lines of auditable bash and Go (7,194 sh + 217 Go, wc -l, Oct 2026)</a>. Prefer the manual? Read the <a href="docs/">full man page</a>—shipped with this site.</div>
The whole thing is <a href="https://github.com/shrwnsan/tailroute-cli" target="_blank" rel="noopener">~7,900 lines of auditable bash and Go (7,733 sh + 217 Go, wc -l, Oct 2026)</a>. Prefer the manual? Read the <a href="docs/">full man page</a>—shipped with this site.</div>
</details>
<details class="q reveal d2">
<summary>What happens when I quit the VPN or uninstall? <span class="pm" aria-hidden="true">+</span></summary>
Expand Down Expand Up @@ -534,7 +535,7 @@ <h2 class="reveal d1">Asked, answered</h2>
"name": "It runs as root—why should I trust it?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Because it shows its work: install-time SHA-256 integrity manifest verified before any library loads, strict input validation, absolute paths only, fail-safe on ambiguous state. The whole thing is ~7,400 lines of auditable bash and Go (7,194 sh + 217 Go, wc -l, Oct 2026). Prefer the manual? Read the full man page, shipped with this site."
"text": "Because it shows its work: install-time SHA-256 integrity manifest verified before any library loads, strict input validation, absolute paths only, fail-safe on ambiguous state. The whole thing is ~7,900 lines of auditable bash and Go (7,733 sh + 217 Go, wc -l, Oct 2026). Prefer the manual? Read the full man page, shipped with this site."
}
},
{
Expand Down
Loading
Loading