From 3a96e887fa0ed75e7b0eaea20e194cc3af1521c7 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Wed, 21 Oct 2026 19:57:02 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(app):=20show=20a=20QR=20code=20of=20th?= =?UTF-8?q?is=20host's=20tailnet=20address=20in=20Settings=20=E2=86=92=20H?= =?UTF-8?q?ost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: apps/host/src/web.ts#let address | set only after the HTTPS listener starts, so the phone row never offers an address that isn't served --- apps/app/package.json | 3 ++- apps/app/src/host-settings.tsx | 44 ++++++++++++++++++++++++++++++++++ apps/app/src/qr.tsx | 31 ++++++++++++++++++++++++ apps/host/src/diagnostics.ts | 2 ++ apps/host/src/gateway.ts | 3 ++- apps/host/src/protocol.ts | 2 ++ apps/host/src/web.ts | 7 ++++++ bun.lock | 4 ++++ package.json | 1 + 9 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 apps/app/src/qr.tsx diff --git a/apps/app/package.json b/apps/app/package.json index 31bc9cae..58df358d 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -21,7 +21,8 @@ "electrobun": "catalog:desktop", "pdfjs-dist": "catalog:ui", "react": "catalog:ui", - "react-dom": "catalog:ui" + "react-dom": "catalog:ui", + "uqr": "catalog:ui" }, "devDependencies": { "@tailwindcss/vite": "catalog:ui", diff --git a/apps/app/src/host-settings.tsx b/apps/app/src/host-settings.tsx index 68509063..301580ac 100644 --- a/apps/app/src/host-settings.tsx +++ b/apps/app/src/host-settings.tsx @@ -1,12 +1,14 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { Button } from "@ace/ui"; +import { IconCheck, IconCopy } from "@ace/ui/icons"; import type { HelperAction, HelperState } from "@ace/desktop/protocol"; import type { Diagnostics } from "@ace/host/protocol"; import { CommandLine } from "./command-settings"; import { desktop } from "./desktop"; import { host } from "./host"; +import { Qr } from "./qr"; import { Confirm, Group, Page, Problem, Row, Status, Value } from "./settings-layout"; /** The host's tool and team checks, re-read while `active` and whenever `refresh` is called. */ @@ -190,6 +192,11 @@ export function HostStatus({ active }: { active: boolean }) { )} + {checked?.tailscale.state === "running" && !checked.tailscale.loopback && ( + + + + )} ); } + +/** This host's HTTPS address, which the owner's phone opens over the tailnet. */ +function Phone({ web }: { web?: string }) { + const [copied, setCopied] = useState(false); + if (!web) { + return ( + + ); + } + return ( + + +
+ {web} + +
+ + } + /> + ); +} diff --git a/apps/app/src/qr.tsx b/apps/app/src/qr.tsx new file mode 100644 index 00000000..ba63c434 --- /dev/null +++ b/apps/app/src/qr.tsx @@ -0,0 +1,31 @@ +import { useMemo } from "react"; +import { encode } from "uqr"; + +/** + * Dark modules on a white quiet zone in both themes: phone cameras often fail to read an inverted + * code. + */ +export function Qr( + { value, label, className }: { value: string; label: string; className?: string }, +) { + const { size, path } = useMemo(() => { + const { size, data } = encode(value, { border: 2 }); + const path = data.flatMap((row, y) => + row.flatMap((dark, x) => dark ? [`M${x} ${y}h1v1h-1z`] : []) + ); + return { size, path: path.join("") }; + }, [value]); + + return ( + + + + + ); +} diff --git a/apps/host/src/diagnostics.ts b/apps/host/src/diagnostics.ts index c29087e0..195ec409 100644 --- a/apps/host/src/diagnostics.ts +++ b/apps/host/src/diagnostics.ts @@ -7,6 +7,7 @@ import { shell as environment } from "./environment"; import { latest } from "./keys"; import type { Diagnostics } from "./protocol"; import * as tailnet from "./tailnet"; +import * as web from "./web"; async function git(): Promise { const path = Bun.which("git", { PATH: process.env.PATH }) || undefined; @@ -54,5 +55,6 @@ export async function diagnostics(sync?: boolean): Promise { tailscale: config.loopback ? { ...tailscale, loopback: true } : tailscale, directory: { ...directory.status(), secret: await secret }, shell: { ...environment }, + web: web.address, }; } diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index eeda124a..9ef852b6 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -1126,7 +1126,8 @@ export async function serve(port: number, loopback = false, script?: string): Pr if (tls && !closing) { const listener = { port: config.webPort ?? port + 1000, secure: true }; servers.push(team(machine, listener, tls)); - console.log(`and https://${machine.dns}:${listener.port}`); + web.served(machine, listener); + console.log(`and ${web.address}`); } } else if (!loopback) { console.log("Tailscale is not running; channels stay on this machine"); diff --git a/apps/host/src/protocol.ts b/apps/host/src/protocol.ts index c19dab6b..42726ed8 100644 --- a/apps/host/src/protocol.ts +++ b/apps/host/src/protocol.ts @@ -60,6 +60,8 @@ export type Diagnostics = { }; /** Whether the helper loaded the login shell's environment; `pending` while it retries. */ shell: { state: "adopted" | "pending" | "failed"; error?: string }; + /** This host's HTTPS address on the tailnet; unset without a tailnet certificate. */ + web?: string; }; /** `offline`: the channel's host is unreachable. Hosted channels are never offline. */ diff --git a/apps/host/src/web.ts b/apps/host/src/web.ts index f7c6925b..b0fe6bd5 100644 --- a/apps/host/src/web.ts +++ b/apps/host/src/web.ts @@ -44,6 +44,13 @@ export async function tls(machine: Machine): Promise<{ cert: string; key: string export type Listener = { port: number; secure: boolean }; +/** The HTTPS address the owner's phone opens, once that listener is up. */ +export let address: string | undefined; + +export function served(machine: Machine, listener: Listener): void { + address = `https://${machine.dns}:${listener.port}`; +} + /** Names this host answers to on the tailnet; anything else is DNS rebinding. */ export function hosts(machine: Machine, listener: Listener): Set { const names = listener.secure ? [machine.dns] : [machine.dns, machine.name, machine.address]; diff --git a/bun.lock b/bun.lock index 9ba89d22..9eda6cd3 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "pdfjs-dist": "catalog:ui", "react": "catalog:ui", "react-dom": "catalog:ui", + "uqr": "catalog:ui", }, "devDependencies": { "@tailwindcss/vite": "catalog:ui", @@ -227,6 +228,7 @@ "tailwind-merge": "3.5.0", "tailwindcss": "4.3.0", "tw-animate-css": "1.4.0", + "uqr": "0.1.3", "vite": "7.3.1", }, }, @@ -1699,6 +1701,8 @@ "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="], + "uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], + "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], diff --git a/package.json b/package.json index ae75c995..40b1ba3d 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "tailwind-merge": "3.5.0", "tailwindcss": "4.3.0", "tw-animate-css": "1.4.0", + "uqr": "0.1.3", "vite": "7.3.1", "@vitejs/plugin-react": "5.1.4", "@xterm/addon-fit": "0.11.0", From 5cf1b37c94206b22bf3d03a7f2c39ae0a3d8d234 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Wed, 21 Oct 2026 19:57:08 -0400 Subject: [PATCH 2/3] lane: confirm notes for phone QR --- ...YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md | 6 +++--- ...ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md | 6 +++--- ...D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md | 6 +++--- ...GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md | 8 ++++++++ ...X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md | 6 +++--- ...ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md | 6 +++--- ...5AYWB5A5VY879YM8MPBVT88-the-desktop-script-runs-the.md | 5 +++-- 7 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 .lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md index 2539861f..739b43af 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T03:39:45Z norm: '1' sig: 171797818ef4d836 -body_hash: bbdb9eb136165a77 -raw_hash: db94b1c01ca466d2 -vouched: 2026-10-20T17:10:51Z +body_hash: 347ec5128df25eff +raw_hash: 4f8782ceb985ad81 +vouched: 2026-10-21T23:57:08Z lines: 668-751 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md index 674bc9aa..29d8e176 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T10:17:10Z norm: '1' sig: 171797818ef4d836 -body_hash: bbdb9eb136165a77 -raw_hash: db94b1c01ca466d2 -vouched: 2026-10-20T17:10:51Z +body_hash: 347ec5128df25eff +raw_hash: 4f8782ceb985ad81 +vouched: 2026-10-21T23:57:08Z lines: 830-948 supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md index 4a35c64c..fcef8035 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-20T17:04:56Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: f383b10cec21cd9b -raw_hash: 9875bf699ce7d092 -vouched: 2026-10-20T17:10:51Z +body_hash: c1aca6bd21460ec9 +raw_hash: 0c4c21e9c44f3962 +vouched: 2026-10-21T23:57:08Z lines: 1-1153 --- diff --git a/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md b/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md new file mode 100644 index 00000000..4e37f94f --- /dev/null +++ b/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md @@ -0,0 +1,8 @@ +--- +id: 01M5GEB6HV1WK8X6GAV7E9ZVGG +anchor: let address +created: 2026-10-21T23:57:02Z +norm: '1' +--- + +set only after the HTTPS listener starts, so the phone row never offers an address that isn't served diff --git a/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md b/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md index efbba9b1..90c8c2e0 100644 --- a/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md +++ b/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-14T12:13:47Z norm: '1' sig: 021fb596db81e6d0 -body_hash: 7f91062de99c2f79 -raw_hash: d907dc1c33b7e42f -vouched: 2026-10-19T20:50:34Z +body_hash: 09d9fabe6a70bd56 +raw_hash: 072618b55fe50579 +vouched: 2026-10-21T23:57:08Z lines: 1-92 --- diff --git a/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md b/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md index 8ab9d28c..493f25bc 100644 --- a/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md +++ b/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-15T10:29:13Z norm: '1' sig: 021fb596db81e6d0 -body_hash: 7f91062de99c2f79 -raw_hash: d907dc1c33b7e42f -vouched: 2026-10-19T20:50:34Z +body_hash: 09d9fabe6a70bd56 +raw_hash: 072618b55fe50579 +vouched: 2026-10-21T23:57:08Z lines: 1-94 --- diff --git a/.lane/memory/package.json/01M5AYWB5A5VY879YM8MPBVT88-the-desktop-script-runs-the.md b/.lane/memory/package.json/01M5AYWB5A5VY879YM8MPBVT88-the-desktop-script-runs-the.md index fc4ff99e..73aac9be 100644 --- a/.lane/memory/package.json/01M5AYWB5A5VY879YM8MPBVT88-the-desktop-script-runs-the.md +++ b/.lane/memory/package.json/01M5AYWB5A5VY879YM8MPBVT88-the-desktop-script-runs-the.md @@ -4,8 +4,9 @@ anchor: '@file' created: 2026-10-19T20:50:34Z norm: '1' sig: 021fb596db81e6d0 -body_hash: 7f91062de99c2f79 -raw_hash: d907dc1c33b7e42f +body_hash: 09d9fabe6a70bd56 +raw_hash: 072618b55fe50579 +vouched: 2026-10-21T23:57:08Z lines: 1-95 --- From d34d4a87dc34fbf090bea4991781273fde933656 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Wed, 21 Oct 2026 19:57:09 -0400 Subject: [PATCH 3/3] lane: sync phone-qr memory --- ...01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md b/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md index 4e37f94f..775281de 100644 --- a/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md +++ b/.lane/memory/apps/host/src/web.ts/01M5GEB6HV1WK8X6GAV7E9ZVGG-set-only-after-the-https-lis.md @@ -3,6 +3,10 @@ id: 01M5GEB6HV1WK8X6GAV7E9ZVGG anchor: let address created: 2026-10-21T23:57:02Z norm: '1' +sig: 6745db67e4b91141 +body_hash: e3b0c44298fc1c14 +raw_hash: c379687473cfca71 +lines: 48-48 --- set only after the HTTPS listener starts, so the phone row never offers an address that isn't served