v0.5: stateless room-based sessions with QR pairing

The single shared mesh is replaced by per-session rooms. Visit /
and the server mints a random 8-hex-char id, redirects to /r/<id>.
That URL IS the session — share the link (or scan the QR code now
shown on the page) on another device to join the same room.

Bus is now sharded per room. Rooms are created implicitly on first
subscribe and GC'd 5 minutes after the last subscriber leaves. No
accounts, no persistence, no server-side state beyond the in-memory
bus map.

Server:
- New endpoints: /, /r/<id>, /api/send?room=, /api/stream?room=
- Room manager with lazy creation + idle GC
- Metrics now labelled by room
- New gauge tether_active_rooms

Client (Go):
- -room flag (accepts bare id OR full /r/<id> URL — paste-friendly)
- All API calls now scope to the room
- The always-on ct210-rtc-peer systemd unit is disabled — sessions
  are user-initiated; the user runs tether-client with -room when
  they want their laptop in a particular session

Browser (HTML):
- Reads room from /r/<id> path
- Shows QR code + URL + "copy link" button at top
- "+ new session" link in header to start a fresh room

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 4.7
2026-05-21 01:13:33 -05:00
parent 7995908c87
commit 618d330682
3 changed files with 280 additions and 122 deletions

View File

@@ -53,6 +53,7 @@ type Message struct {
To string `json:"to,omitempty"`
Role string `json:"role,omitempty"`
Source string `json:"source,omitempty"`
Room string `json:"room,omitempty"`
TS int64 `json:"ts"`
}
@@ -68,16 +69,31 @@ var (
myLabel string
useRTC bool
rtcServer string
myRoom string
)
func main() {
server := flag.String("server", "https://tether.pecord.io", "tether-server base URL")
label := flag.String("label", "", "X-Tether-Client label (default: <os>-sse-<role>)")
sendText := flag.String("send", "", "send this text and exit (otherwise listen)")
roomFlag := flag.String("room", "", "room id to join (or full URL like https://tether.pecord.io/r/<id>)")
flag.BoolVar(&noClipboard, "no-clipboard", false, "don't write incoming messages to the OS clipboard")
flag.BoolVar(&useRTC, "rtc", false, "act as a WebRTC mesh peer")
flag.Parse()
// Accept either bare id or full URL
myRoom = *roomFlag
if strings.Contains(myRoom, "/r/") {
parts := strings.SplitN(myRoom, "/r/", 2)
if len(parts) == 2 {
myRoom = strings.SplitN(parts[1], "/", 2)[0]
}
}
if myRoom == "" {
fmt.Fprintln(os.Stderr, "tether-client: -room is required (e.g. -room abc12345 or paste the /r/<id> URL)")
os.Exit(2)
}
if *label == "" {
if *sendText != "" {
*label = defaultLabel("sender")
@@ -128,8 +144,11 @@ func sendMessage(server, label string, m Message) {
}
func postMessage(server, label string, m Message) {
if m.Room == "" {
m.Room = myRoom
}
body, _ := json.Marshal(m)
req, _ := http.NewRequest("POST", server+"/api/send", bytes.NewReader(body))
req, _ := http.NewRequest("POST", server+"/api/send?room="+myRoom, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Tether-Source", label)
r, err := http.DefaultClient.Do(req)
@@ -149,7 +168,7 @@ func postMessage(server, label string, m Message) {
// listen subscribes to the SSE stream and dispatches messages to handlers.
func listen(server, label string) error {
req, _ := http.NewRequest("GET", server+"/api/stream", nil)
req, _ := http.NewRequest("GET", server+"/api/stream?room="+myRoom, nil)
req.Header.Set("X-Tether-Client", label)
r, err := http.DefaultClient.Do(req)
if err != nil {