ua-labels: derive source from User-Agent, persist peerID in localStorage

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 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 4.7
2026-05-21 01:19:14 -05:00
parent 618d330682
commit 625143f87a
4 changed files with 46 additions and 4 deletions

View File

@@ -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()

View File

@@ -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",