From 7cb019974e7174703b4fd75b7c32e5f69e487998 Mon Sep 17 00:00:00 2001 From: shrwnsan <38465+shrwnsan@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:23:11 +0800 Subject: [PATCH 1/2] feat(site): document tunnel stop and start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The man page's Browser tunnels section gains the park/resume verbs: what stop keeps (registry + hosts mapping, parked across logins and reboots), what start restores (same local port, same URL, no sudo), the --all forms, and the restart-refuses-parked rule — with copyable stop/start synopsis lines. The proof section catches up to v0.9.0: the cumulative-release chart gains the 2026-10-27 point (32) with the extended axis and hover mapping to match, the release feed leads with 0.9.0, and the stale counts are refreshed (529 tests, 7,733 sh + 217 Go). llms.txt picks up the lifecycle clause and the current line count. Co-authored-by: ZCode --- site/assets/js/chart.js | 9 +++++---- site/docs/index.html | 14 +++++++++++++- site/index.html | 33 +++++++++++++++++---------------- site/llms.txt | 4 ++-- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/site/assets/js/chart.js b/site/assets/js/chart.js index a98d915..af960ad 100644 --- a/site/assets/js/chart.js +++ b/site/assets/js/chart.js @@ -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 + " · " + best.v + " 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 () { diff --git a/site/docs/index.html b/site/docs/index.html index ceb63b9..fe0a407 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -304,6 +304,18 @@

Browser tunnels

$ tailroute tunnel add mypeer

→ https://mypeer.yourtailnet.ts.net:8443

+

Need a tunnel gone for a while? tailroute tunnel stop mypeer 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. tailroute tunnel start mypeer resumes it + with the same local port and the same URL—no sudo, no re-registration. + Both take --all; tunnel status shows a parked + tunnel as stopped (by request), and tunnel restart + refuses to implicitly start one.

+
+
$ tailroute tunnel stop mypeer
+
$ tailroute tunnel start mypeer
+

What it cannot fix

VPN Network Extensions may still block traffic to Tailscale's CGNAT range @@ -321,7 +333,7 @@

Security

(/var/db/tailroute/installed.checksums) verified before any library loads.
  • No telemetry, no external network calls, no account, no traffic logging.
  • Interface names validated against utun[0-9]+; absolute paths only.
  • -
  • 417 tests as of v0.8.17. Source: github.com/shrwnsan/tailroute-cli
  • +
  • 529 tests as of v0.9.0. Source: github.com/shrwnsan/tailroute-cli
  • Diagnostics

    diff --git a/site/index.html b/site/index.html index 532624d..8ca9d8b 100644 --- a/site/index.html +++ b/site/index.html @@ -251,7 +251,7 @@

    Three states, zero judgment calls

    03Self-verifying install

    An install-time SHA-256 integrity manifest is verified before any library loads—tampered files refuse to start, and say what changed. Strict validation, absolute paths, fail-safe on ambiguity, - 417 tests and counting.

    + 529 tests and counting.

    @@ -292,13 +292,13 @@

    Shipped, not promised.

    Tagged releases—cumulative - 29 of 31 releases shipped since Aug 24 + 30 of 32 releases shipped since Aug 24
    - - - - - + + + + + 0 10 20 @@ -307,11 +307,11 @@

    Shipped, not promised.

    Jun Sep 1 Oct - - - - 31 - + + + + 32 +
    @@ -332,6 +332,7 @@

    Shipped, not promised.

    2026-09-2329 2026-09-2430 2026-09-3031 + 2026-10-2732 @@ -339,11 +340,11 @@

    Shipped, not promised.

    +
    0.9.0

    tunnel stop / tunnel start—park a tunnel without tearing it down: it stays parked across reboots, and start restores the same port and URL.

    2026-10-27 · tests 528 → 529

    0.8.17

    Log rotation actually rotates—staged at daemon startup for every layout; integrity manifest now layout-aware.

    2026-09-30 · tests 408 → 417

    0.8.16

    Second peers stay removable—hosts mappings land inside the managed block.

    2026-09-24 · tests 406 → 408

    0.8.15

    Proxy registry self-heals against reality—and the auth key no longer leaks through argv.

    2026-09-23 · tests 383 → 405

    -
    0.8.14

    Every tunnel verb accepts the ssh alias—no more dead ends on tunnel check prime.

    2026-09-23 · tests 367 → 383

    -
    0.8.11

    tunnel check—the browser’s truth: hosts → listener → TLS → HTTP, zero ssh.

    2026-09-03 · read-only by construction

    +
    0.8.14

    Every tunnel verb accepts the ssh alias—no more dead ends on tunnel check prime.

    2026-09-23 · tests 367 → 383

    22 releases in 72 hours (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: CHANGELOG.md ↗

    @@ -456,7 +457,7 @@

    Asked, answered

    It runs as root—why should I trust it?
    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.
    + 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.
    What happens when I quit the VPN or uninstall? @@ -534,7 +535,7 @@

    Asked, answered

    "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." } }, { diff --git a/site/llms.txt b/site/llms.txt index e43c7dc..876d3f5 100644 --- a/site/llms.txt +++ b/site/llms.txt @@ -1,6 +1,6 @@ # tailroute-cli -> tailroute is a fail-safe macOS daemon that keeps Tailscale and commercial VPNs (NordVPN, Mullvad, corporate VPN apps) working simultaneously. When a VPN connects it disables Tailscale's MagicDNS so internet keeps flowing through the VPN, and serves Tailscale peers through a local SOCKS5 proxy (127.0.0.1:1055) and launchd-managed browser tunnels with valid .ts.net TLS. Apache-2.0, no telemetry, no account. +> tailroute is a fail-safe macOS daemon that keeps Tailscale and commercial VPNs (NordVPN, Mullvad, corporate VPN apps) working simultaneously. When a VPN connects it disables Tailscale's MagicDNS so internet keeps flowing through the VPN, and serves Tailscale peers through a local SOCKS5 proxy (127.0.0.1:1055) and launchd-managed browser tunnels with valid .ts.net TLS—`tunnel stop` parks a tunnel without tearing it down (it stays parked across reboots), `tunnel start` resumes it with the same URL. Apache-2.0, no telemetry, no account. The tool answers one question: "How do I keep Tailscale and my VPN working at the same time on a Mac?" Install is two commands, it needs no configuration, and it reverses itself when the VPN disconnects. @@ -14,7 +14,7 @@ The tool answers one question: "How do I keep Tailscale and my VPN working at th - [Landing page](https://tailroute.app/): product overview, honest limits, FAQ - [Manual page](https://tailroute.app/docs/): the full tailroute manual—states, tunnels, security, diagnostics -- [GitHub repository](https://github.com/shrwnsan/tailroute-cli): source, ~7,400 auditable lines of bash and Go +- [GitHub repository](https://github.com/shrwnsan/tailroute-cli): source, ~7,900 auditable lines of bash and Go - [Changelog](https://github.com/shrwnsan/tailroute-cli/blob/main/CHANGELOG.md): per-release history with test counts - [Issues](https://github.com/shrwnsan/tailroute-cli/issues): bug reports and VPN compatibility reports From 9d7fb46c210e1e6dcc0e7689eba42799499f9eae Mon Sep 17 00:00:00 2001 From: shrwnsan <38465+shrwnsan@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:01:26 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(site):=20every=20version=20surface=20no?= =?UTF-8?q?w=20reads=200.9.0=20=E2=80=94=20and=20stays=20current?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A full-page sweep found the versioned surfaces the content refresh missed: the JSON-LD softwareVersion on both pages, the manual page's version badge, the landing's committed stat-card fallbacks, and the 'tests in five weeks' stat whose window had lapsed (348 at v0.8.11 → 529 at v0.9.0, eight weeks). Rather than re-rot at the next release, the manual page joins the deploy-time refresh: update-metrics.py now also injects the latest version into the docs page's softwareVersion and its badge (new data-metric="cli-version" span), so a release tag is all the site needs. The span helper tolerates attributes beside data-metric, and the landing contract (stat card, updated span) is unchanged — check-site.py still passes 9/9. Co-authored-by: ZCode --- scripts/update-metrics.py | 50 ++++++++++++++++++++++++--------------- site/docs/index.html | 4 ++-- site/index.html | 6 ++--- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/scripts/update-metrics.py b/scripts/update-metrics.py index b7b578e..4da50e2 100755 --- a/scripts/update-metrics.py +++ b/scripts/update-metrics.py @@ -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 @@ -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(): @@ -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 @@ -75,7 +87,7 @@ def _replace_version(html, version): def _replace_span(html, name, text): import re - pattern = re.compile(rf'()(.*?)()', re.S) + pattern = re.compile(rf'(]*\sdata-metric="{name}"[^>]*>)(.*?)()', re.S) if not pattern.search(html): print(f"update-metrics: WARNING span {name!r} not found") return html diff --git a/site/docs/index.html b/site/docs/index.html index fe0a407..c6782f1 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -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", @@ -203,7 +203,7 @@

    tailroute.

    alpha - v0.8.17 + v0.9.0

    tailroute-cli manual · apache-2.0 · macOS 15+ · free & open source

    On this page · diff --git a/site/index.html b/site/index.html index 8ca9d8b..6178103 100644 --- a/site/index.html +++ b/site/index.html @@ -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.", @@ -187,8 +187,8 @@

    Keep working.
    VPN up, tailnet up.

    -
    tagged releases · public since mar ’26
    31
    -
    shell tests · 348 → 417 in five weeks
    417
    +
    tagged releases · public since mar ’26
    32
    +
    shell tests · 348 → 529 in eight weeks
    529
    telemetry · external calls · accounts
    0
    forever · apache-2.0 core
    $0