From 625143f87ad200e8edbf475b03f656296c46bc08 Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.7" Date: Thu, 21 May 2026 01:19:14 -0500 Subject: [PATCH] ua-labels: derive source from User-Agent, persist peerID in localStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server now parses the User-Agent header on /api/send when no explicit source is provided, producing labels like 'iphone-safari', 'macos-chrome', 'windows-firefox' so the feed shows where messages came from at a glance. Browser-side peerID is now stored in localStorage instead of being freshly random on each page load — refreshing or reopening the tab keeps the same identity for the same browser profile. Co-Authored-By: Claude Opus 4.7 --- go.mod | 1 + go.sum | 2 ++ server/main.go | 36 +++++++++++++++++++++++++++++++++++- server/web/index.html | 11 ++++++++--- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c21df36..c5828a1 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/mileusna/useragent v1.3.5 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pion/datachannel v1.6.0 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect diff --git a/go.sum b/go.sum index 3ec9271..ca7ef97 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= +github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= diff --git a/server/main.go b/server/main.go index 4dd46a8..dca23ab 100644 --- a/server/main.go +++ b/server/main.go @@ -31,11 +31,45 @@ import ( "sync" "time" + "github.com/mileusna/useragent" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) +// labelFromUA returns a short, human-friendly platform label like +// "iphone-safari", "macos-chrome", "windows-firefox". Falls back to "web". +func labelFromUA(uaStr string) string { + ua := useragent.Parse(uaStr) + platform := strings.ToLower(ua.OS) + switch { + case ua.IsIOS() && strings.Contains(strings.ToLower(ua.Device), "ipad"): + platform = "ipad" + case ua.IsIOS(): + platform = "iphone" + case ua.IsAndroid(): + platform = "android" + case ua.IsMacOS(): + platform = "macos" + case ua.IsWindows(): + platform = "windows" + case ua.IsLinux(): + platform = "linux" + } + browser := strings.ToLower(ua.Name) + browser = strings.ReplaceAll(browser, " ", "") + if platform == "" && browser == "" { + return "web" + } + if browser == "" { + return platform + } + if platform == "" { + return browser + } + return platform + "-" + browser +} + //go:embed web var webFS embed.FS @@ -251,7 +285,7 @@ func main() { if m.Source == "" { m.Source = r.Header.Get("X-Tether-Source") if m.Source == "" { - m.Source = "web" + m.Source = labelFromUA(r.Header.Get("User-Agent")) } } m.TS = time.Now().UnixMilli() diff --git a/server/web/index.html b/server/web/index.html index 2961d2c..8ab65ae 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -151,7 +151,12 @@ if (!roomMatch) { window.location.replace("/"); } const room = roomMatch ? roomMatch[1] : ""; const fullUrl = window.location.origin + "/r/" + room; - const peerID = "browser-" + Array.from(crypto.getRandomValues(new Uint8Array(6))).map(b=>b.toString(16).padStart(2,"0")).join(""); + // Stable per-device id — persists across reloads (unless cleared). + let peerID = localStorage.getItem("tether-peerID"); + if (!peerID) { + peerID = "browser-" + Array.from(crypto.getRandomValues(new Uint8Array(6))).map(b=>b.toString(16).padStart(2,"0")).join(""); + localStorage.setItem("tether-peerID", peerID); + } $("roomTag").textContent = room; $("sessionUrl").textContent = fullUrl; @@ -218,16 +223,16 @@ const r = await fetch("/api/send?room=" + room, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ type: "clipboard", text, source: "web", from: peerID, room }), + body: JSON.stringify({ type: "clipboard", text, from: peerID, room }), }); status(r.ok ? "delivered ✓" : "server returned " + r.status, r.ok ? "ok" : "err"); } catch (e) { status("network error", "err"); } }); // ── Signaling ───────────────────────────────────────────────────────── + // Let the server fill in source from User-Agent unless caller specifies. async function postMessage(m) { m.from = m.from || peerID; - m.source = m.source || "web"; m.room = room; await fetch("/api/send?room=" + room, { method: "POST",