From 10ee88577599905ac7807d7f5a47b2cdd29a8f66 Mon Sep 17 00:00:00 2001 From: Patrick Bertsch Date: Sat, 2 May 2026 18:01:38 -0600 Subject: [PATCH] feat(studio): WiFi token memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persist (deviceName โ†’ token) in localStorage after a successful ConnectWiFi. On the next mDNS discovery for that device, prefill the token input and show a "๐Ÿ”‘ saved" tag. Each remembered device gets a small "โœ•" button to forget its token. Keyed by the Bonjour instance name (typically the device hostname) since the IP can change across DHCP leases but the name is stable. Tokens for an LAN-only test agent inside a desktop app are an acceptable trust boundary; this is not a credential vault. Pure frontend change โ€” no Wails methods, no agent changes. --- CHANGELOG.md | 3 ++ studio/frontend/src/main.ts | 69 ++++++++++++++++++++++++++++++++++- studio/frontend/src/style.css | 3 ++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0790b0b..ad686dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- **Studio: WiFi token memory** โ€” Studio now remembers the agent token per discovered device (keyed by Bonjour instance name) in localStorage. After a successful WiFi connect, subsequent mDNS-discovered sessions for that device prefill the token automatically and show a "๐Ÿ”‘ saved" tag. Each remembered device gets a small "โœ•" button to forget the token. + ## [0.7.0] - 2026-05-02 ### Added diff --git a/studio/frontend/src/main.ts b/studio/frontend/src/main.ts index 3f8c96b..50fe8da 100644 --- a/studio/frontend/src/main.ts +++ b/studio/frontend/src/main.ts @@ -309,6 +309,47 @@ $("help-overlay").addEventListener("click", (e) => { type WiFiDevice = { name: string; host: string; port: number; version: string }; +const WIFI_TOKENS_KEY = "fp.studio.wifi-tokens"; + +// Token store: deviceName โ†’ token. Persisted in localStorage so a user who +// has connected once doesn't need to paste the token again on subsequent +// launches. Keyed by the Bonjour instance name (typically the device's +// hostname) since the IP can change across DHCP leases but the name is +// stable. Tokens for an LAN-only test agent inside a desktop app are an +// acceptable trust boundary; this is not a credential vault. +function loadTokenStore(): Record { + try { + const raw = localStorage.getItem(WIFI_TOKENS_KEY); + return raw ? (JSON.parse(raw) as Record) : {}; + } catch { + return {}; + } +} + +function saveTokenStore(store: Record) { + try { + localStorage.setItem(WIFI_TOKENS_KEY, JSON.stringify(store)); + } catch { + // localStorage full or denied โ€” non-fatal, user just re-enters next time + } +} + +function rememberToken(deviceName: string, token: string) { + const store = loadTokenStore(); + store[deviceName] = token; + saveTokenStore(store); +} + +function forgetToken(deviceName: string) { + const store = loadTokenStore(); + delete store[deviceName]; + saveTokenStore(store); +} + +function recalledToken(deviceName: string): string | undefined { + return loadTokenStore()[deviceName]; +} + let wifiSelected: WiFiDevice | null = null; const wifiSeen = new Map(); @@ -337,9 +378,26 @@ function selectWiFiDevice(dev: WiFiDevice) { for (const li of Array.from($("wifi-devices").children) as HTMLElement[]) { li.classList.toggle("selected", li.dataset.key === `${dev.host}:${dev.port}`); } + const remembered = recalledToken(dev.name); $("wifi-selected-label").textContent = `Connecting to ${dev.name} (${dev.host}:${dev.port})`; ($("wifi-token-row") as HTMLElement).hidden = false; - ($("wifi-token-input") as HTMLInputElement).focus(); + const input = $("wifi-token-input") as HTMLInputElement; + input.value = remembered ?? ""; + input.focus(); +} + +function renderForgetButton(deviceName: string, container: HTMLElement) { + const btn = document.createElement("button"); + btn.className = "btn-mini wifi-forget"; + btn.title = `Forget remembered token for ${deviceName}`; + btn.textContent = "โœ•"; + btn.addEventListener("click", (e) => { + e.stopPropagation(); + forgetToken(deviceName); + btn.remove(); + toast(`Forgot token for ${deviceName}`, "info", 1500); + }); + container.appendChild(btn); } EventsOn("wifi:device-found", (dev: WiFiDevice) => { @@ -360,6 +418,14 @@ EventsOn("wifi:device-found", (dev: WiFiDevice) => { hostEl.className = "wifi-host"; hostEl.textContent = `${dev.host}:${dev.port}${dev.version ? ` ยท v${dev.version}` : ""}`; li.append(nameEl, hostEl); + if (recalledToken(dev.name)) { + const savedTag = document.createElement("span"); + savedTag.className = "wifi-saved-tag"; + savedTag.textContent = "๐Ÿ”‘ saved"; + savedTag.title = "Token remembered from a previous connection"; + li.appendChild(savedTag); + renderForgetButton(dev.name, li); + } li.addEventListener("click", () => selectWiFiDevice(dev)); list.appendChild(li); }); @@ -380,6 +446,7 @@ $("btn-wifi-connect").addEventListener("click", async () => { setStatus("connecting", `Connecting to ${wifiSelected.host}โ€ฆ`); try { await ConnectWiFi(wifiSelected.host, wifiSelected.port, token); + rememberToken(wifiSelected.name, token); setStatus("connected", `Connected ยท ${wifiSelected.name}`); closeWiFiOverlay(); startStream(); diff --git a/studio/frontend/src/style.css b/studio/frontend/src/style.css index b24093d..8aab4c4 100644 --- a/studio/frontend/src/style.css +++ b/studio/frontend/src/style.css @@ -386,6 +386,9 @@ body { display: flex; flex-direction: column; overflow: hidden; } margin-top: 6px; } .wifi-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } +.wifi-saved-tag { color: var(--fg-dim); font-size: 10px; margin-left: 8px; padding: 1px 6px; background: var(--bg-1); border-radius: var(--radius-sm); } +.wifi-device-list .btn-mini.wifi-forget { float: right; opacity: 0.5; padding: 0 6px; font-size: 11px; } +.wifi-device-list .btn-mini.wifi-forget:hover { opacity: 1; } kbd { display: inline-block; padding: 1px 6px;