From 0622afcae9adb597bdb843c3c290a4f5f247cb6b Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:29:58 +1000 Subject: [PATCH] feat(ui): add family controls, casting and native notifications --- README.md | 6 +- app/main.js | 19 ++- app/preload.js | 1 + docs/family-comfort.md | 109 ++++++++++++++++++ docs/img/comfort-notification-ios.png | Bin 0 -> 72233 bytes docs/iphone.md | 13 ++- docs/testing.md | 9 ++ ios/FrameControl.xcodeproj/project.pbxproj | 4 + ios/FrameControl/Web/WebShell.swift | 46 +++++++- .../ComfortNotificationTests.swift | 14 +++ ui/index.html | 102 +++++++++++++++- 11 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 docs/family-comfort.md create mode 100644 docs/img/comfort-notification-ios.png create mode 100644 ios/FrameControlTests/ComfortNotificationTests.swift diff --git a/README.md b/README.md index 6dd32be..79021a7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,11 @@ SSH, SFTP, Steam Link, remote desktop, volume, sleep, restart and shut down. -Nothing is installed on the Frame for any of this: the app uses what SteamOS +The optional [Family and comfort](docs/family-comfort.md) card adds session +limits, breaks, local alerts and one-click casting. A session copies a small +Frame Control worker into your headset user account. + +For the other features, nothing is installed on the Frame: the app uses what SteamOS already ships (sideloading a game copies Valve's own devkit scripts to `~/devkit-utils`, as Valve's Devkit Client does). [How each feature works](docs/frame-control.md). diff --git a/app/main.js b/app/main.js index bf6ff66..f4de569 100644 --- a/app/main.js +++ b/app/main.js @@ -1,7 +1,7 @@ // Frame Control as a desktop app (macOS, Windows, Linux): starts ui/server.py on // a free loopback port and shows it in a native window. The server does all the // work over the `frame` SSH alias; this file only hosts it. -const { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, shell } = require("electron"); +const { app, BrowserWindow, Menu, Notification, clipboard, dialog, ipcMain, shell } = require("electron"); const { execFile, spawn } = require("child_process"); const { promisify } = require("util"); const fs = require("fs"); @@ -245,6 +245,21 @@ function fromUi(e) { ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : ""); ipcMain.handle("connection:setup", (e) => { if (fromUi(e)) setUpConnection(); }); +ipcMain.handle("comfort:notify", (e, message) => { + if (!fromUi(e) || typeof message !== "string" || message.length > 500) throw new Error("Invalid notification"); + if (!Notification.isSupported()) throw new Error("System notifications are unavailable"); + return new Promise((resolve, reject) => { + const notification = new Notification({title: "Frame Control", body: message}); + const timer = setTimeout(() => reject(new Error("Notification delivery was not confirmed. Check system notification settings.")), 5000); + notification.once("show", () => { clearTimeout(timer); resolve(true); }); + notification.once("failed", (_event, error) => { + clearTimeout(timer); + reject(new Error("Notification delivery failed. Check system notification settings: " + error)); + }); + notification.show(); + }); +}); + // frame-control://install links from websites (docs/web-install.md). They can // arrive before the window or server exists (macOS open-url on a cold launch), // so they wait here until the page asks for them. The page checks the link with @@ -292,7 +307,7 @@ function createWindow() { title: "Frame Control", backgroundColor: BG, show: false, ...(IS_MAC ? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 18, y: 26 } } : { icon: path.join(__dirname, "build", "icon.png") }), - webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, + webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false, preload: path.join(__dirname, "preload.js") }, }); win.once("ready-to-show", () => win.show()); diff --git a/app/preload.js b/app/preload.js index 43bc34d..09f801b 100644 --- a/app/preload.js +++ b/app/preload.js @@ -8,6 +8,7 @@ const { contextBridge, ipcRenderer, webUtils } = require("electron"); contextBridge.exposeInMainWorld("frameApp", { + notify: (message, request) => ipcRenderer.invoke("comfort:notify", message, request), readClipboard: () => ipcRenderer.invoke("clipboard:read"), setUpConnection: () => ipcRenderer.invoke("connection:setup"), pathForFile: (file) => { try { return webUtils.getPathForFile(file) || ""; } catch { return ""; } }, diff --git a/docs/family-comfort.md b/docs/family-comfort.md new file mode 100644 index 0000000..b2e9aa8 --- /dev/null +++ b/docs/family-comfort.md @@ -0,0 +1,109 @@ +# Family and comfort + +Frame Control's Home tab has a **Family and comfort** card, on desktop and +on iPhone. No third-party notification or parental-control app is needed. +This is Frame Control code using Python, Steam and SteamVR already on the Frame. + +## Sessions + +Set a limit of 1–240 minutes, optional break and check-in intervals, then +**Start session**. Break and check-in intervals of 0 turn those reminders off. +**Cancel session** cancels the timer and monitoring without changing the game. +Cancel before starting a session with different settings. + +The Frame shows a one-minute warning, then opens Steam Home in its dashboard. +**Games stay running**: save and pause before the limit. Some games pause when +the dashboard opens; others do not. There is no kill, power-off, Steam restart, +account restriction or parental lock. The wearer can return to the game. + +**Documented implementation:** the timer is a single, opt-in Python worker in +the Frame user's account. Desktop and iPhone share its state. It keeps going +when the companion disconnects, closes or is suspended. It exits after +completion or cancellation (normally within five seconds); it is not a boot +service. A Frame reboot invalidates the session. Suspend counts toward the +limit, using Linux's boot-time clock. If a warning was delayed by suspend or a +SteamVR failure, Home waits until at least a full minute after a successful +warning. A failed Home transition remains active and retries, with an error +shown in the companion. A stale worker is reported as unverified enforcement. + +## Alerts and breaks + +During a session: + +- **Low battery:** 15% or below while discharging. One alert until charging or + recovery to 20%, so values around 15% do not produce repeated notifications. +- **Overheating:** a thermal zone reaches its own kernel-reported hot/critical + trip, or the battery reports `Overheat`. Missing sensors mean unknown, not + safe. These are status alerts, not medical advice or an extra thermal governor. +- **Check in:** an alert after the chosen number of active minutes. +- **Breaks:** a SteamVR reminder and companion notification at the chosen interval. + +**Inferred:** SteamVR activity levels 1 and 2 are a useful proxy for use, not +proof someone is wearing the headset. Inactive readings reset continuous use; +missing readings add no time. Long gaps count at most 30 seconds. Breaks and +check-ins are distinct from the elapsed-time session limit. + +Click **Enable / test notifications** on each companion. iOS asks for permission; +macOS, Windows and Linux follow their notification settings. The page also shows +recent events and errors. Keep Frame Control open and connected for companion +alerts. **Phone alerts are local, not push notifications:** iOS suspension, +force-quit or a lost SSH connection prevents live delivery. Old alerts are not +replayed as a notification burst on reconnect. Headset warnings and the session +limit continue without the phone. A physical iPhone's background delivery has +not been verified and is not guaranteed. + +## Casting + +**Cast headset view** starts the existing headset Live view and requests full +screen where supported. Show that screen to people in the room, or use the +computer/phone's own screen mirroring. It creates no new stream transport, +public URL or LAN server. iPhone uses the inline viewer if full screen is not +available. The image includes private content visible to the wearer. + +## What is installed + +The shared authenticated `/api/comfort` endpoint copies three bundled Python +files to `~/.cache/frame-control/comfort//`. Session state and +locks live in `~/.local/state/frame-control/comfort/`, with a private directory +and 0600 state file. There is no network listener or system service. Cancel a +session before removing these directories. The iPhone's normal server still +exits on disconnect; the explicitly started comfort worker is the exception. + +## Verification + +**Verified 2026-09-28**, SteamOS 0.4.1, build `20260925.6191901`: shipped +`/opt/steamvr/bin/linuxarm64/vrcmd --notify TEXT` reported success for a custom +reminder. Steam's CDP `SteamUIStore.Navigate('/library/home')` and +`SteamClient.OpenVR.VROverlay.ShowDashboard('valve.steam.gamepadui.main')` +opened Home while the running app ID stayed unchanged. Prior page and dashboard +visibility were restored. Kernel hot/critical trips and SteamVR activity were +read from the real device. No temperature or battery fault was induced. + +**Verified locally:** deterministic fake-Frame tests cover late warnings, +failed warnings/Home actions, cancellation, activity gaps, thresholds, duplicate +suppression, reboot invalidation, shared session state and the exact Home +JavaScript. `python3 -m unittest discover -s tests` runs them. The iOS Simulator +build tests notification content and bounds. Physical iPhone delivery and +wearer-perceived headset notification visibility remain unverified. + +**Verified end to end on the same Frame:** a two-minute session with no companion +connection for 135 seconds emitted its warning, break and check-in, then opened +Home. The running app ID was unchanged; the test restored the previous page and +dashboard visibility and confirmed the worker exited. Casting through the Home +shortcut decoded the existing headset stream at 30 fps. + +**Verified on the iOS 26.5 Simulator:** connected to the real Frame, approved the +notification prompt, and saw the native Frame Control test banner. Seven iOS +tests passed. + +![Native test notification in the iOS Simulator](img/comfort-notification-ios.png) + +Desktop and 390-pixel phone layouts had no horizontal overflow. +On macOS the development Electron app's real notification attempt was denied +(`UNErrorDomain` 1); the bridge now returns that failure instead of reporting +success. Successful macOS/Windows/Linux notification display remains unverified. + +**Verified on the real Frame:** its naturally discharging 15% battery produced +one low-battery event during a short session; the test then cancelled the +session. Overheating alerts use fake sensor samples in tests: the shared +headset was not deliberately overheated. diff --git a/docs/img/comfort-notification-ios.png b/docs/img/comfort-notification-ios.png new file mode 100644 index 0000000000000000000000000000000000000000..7ed15d0d018e2a395f795b115b57c14b0ad76102 GIT binary patch literal 72233 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ⅇdi)7?M2BQ1XCZ7TrB>D$1^ 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�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 DC;N=v literal 0 HcmV?d00001 diff --git a/docs/iphone.md b/docs/iphone.md index b09920d..332a447 100644 --- a/docs/iphone.md +++ b/docs/iphone.md @@ -25,7 +25,10 @@ as its transport too), so the desktop and phone share one code path. Android display settings use `podman exec` into each Lepton container instead of adb, which the Frame doesn't have. -Nothing is left running on the Frame after the phone disconnects; the copied +The app server stops after the phone disconnects. An explicitly started +[comfort session](family-comfort.md) keeps its timer and headset reminders running +until the session ends or is cancelled; phone notifications require the app to +remain connected and running. The copied files stay in `~/.cache/frame-control` (delete it any time). ## Pairing @@ -107,3 +110,11 @@ running), a real sleep/restart/shut down on the Frame, and a physical iPhone. Debug builds have Simulator test hooks (`FRAME_TEST_HOST`, `FRAME_TEST_PAGE`, `FRAME_TEST_JS`, and the tunnel URL in the app's Caches folder); release builds don't. + +## Family and comfort + +The shared Home card sets session limits, breaks and check-ins, and offers +**Cast headset view**. **Enable / test notifications** requests iOS notification +permission and sends a local test. These are local notifications, not APNs push; +iOS background suspension can interrupt phone alerts. The headset timer still +runs. See [the behavior and verification limits](family-comfort.md). diff --git a/docs/testing.md b/docs/testing.md index b06577d..7052a06 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -148,3 +148,12 @@ For example, on 2026-09-27 the smoke test found that Steam's `create-shortcut` refuses ids with a hyphen (`missing/invalid arguments`), which the fake had accepted. The fake now refuses them the same way, and Frame Control makes ids Steam accepts. + +## Family and comfort + +`tests/test_comfort.py` uses an injected clock, fake headset sensor readings and +actions, plus a Node fake of Steam's Home API. It covers warnings before Home, +late/suspended sessions, cancellation, failed actions, duplicate alerts, reboot +invalidation, per-zone thermal trips and shared on-headset state. The server +guards reject invalid session settings before SSH. See +[real-device evidence and limits](family-comfort.md#verification). diff --git a/ios/FrameControl.xcodeproj/project.pbxproj b/ios/FrameControl.xcodeproj/project.pbxproj index d23f1b8..9ea9586 100644 --- a/ios/FrameControl.xcodeproj/project.pbxproj +++ b/ios/FrameControl.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 78427FC66780623F31E7501E /* FrameControlApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C8E0D7C3F4F628941B3D5A /* FrameControlApp.swift */; }; 84423CB45629465420180A64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F2CB550FC81C01E6BDD5A71 /* Assets.xcassets */; }; 9657F7BC23E3352E5AB30777 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB544223FC60A59CC3E8EF5F /* SetupView.swift */; }; + A0C5B00E257230A38DBD9E54 /* ComfortNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81218B75FEEE47B8D8BAE20 /* ComfortNotificationTests.swift */; }; A8C7AED25A6280682FCE45DC /* Citadel in Frameworks */ = {isa = PBXBuildFile; productRef = 6BA549B6CC0A0CB847126456 /* Citadel */; }; DC043FB74BE2D23F3A5826BF /* FrameControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1740B691F9C25E5FB6F9EFC3 /* FrameControlTests.swift */; }; E6898C714A92D3979F73B6E1 /* FrameFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F288DF6636A417F0CA3A6CD /* FrameFinder.swift */; }; @@ -48,6 +49,7 @@ BF0FCA7117DA3ABA449B4EE0 /* InstallLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallLink.swift; sourceTree = ""; }; D6C4E6C28315CA8729FCAAEA /* WebShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebShell.swift; sourceTree = ""; }; DB544223FC60A59CC3E8EF5F /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; + E81218B75FEEE47B8D8BAE20 /* ComfortNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComfortNotificationTests.swift; sourceTree = ""; }; EDC7BA8014DBC302D08FD397 /* HeadsetServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadsetServer.swift; sourceTree = ""; }; F3E2F5607DD877272483D64E /* FrameControl.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = FrameControl.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -107,6 +109,7 @@ 75A17B1C79C8C3C60FEABBA6 /* FrameControlTests */ = { isa = PBXGroup; children = ( + E81218B75FEEE47B8D8BAE20 /* ComfortNotificationTests.swift */, 1740B691F9C25E5FB6F9EFC3 /* FrameControlTests.swift */, ); path = FrameControlTests; @@ -274,6 +277,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A0C5B00E257230A38DBD9E54 /* ComfortNotificationTests.swift in Sources */, DC043FB74BE2D23F3A5826BF /* FrameControlTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/FrameControl/Web/WebShell.swift b/ios/FrameControl/Web/WebShell.swift index fee7df2..3ddbe4d 100644 --- a/ios/FrameControl/Web/WebShell.swift +++ b/ios/FrameControl/Web/WebShell.swift @@ -1,6 +1,7 @@ import SwiftUI import UIKit import WebKit +import UserNotifications /// The Frame Control page, served by the server on the headset, in a web view. /// window.frameApp (the same bridge the desktop app's preload.js provides) lets @@ -48,6 +49,7 @@ struct WebShell: UIViewRepresentable { let installCb = null; window.frameApp = { platform: "ios", + notify: (message, request) => call("notify", { message, request }), readClipboard: () => call("readClipboard"), setUpConnection: () => call("setUpConnection"), open: (what) => call("open", what), @@ -58,13 +60,31 @@ struct WebShell: UIViewRepresentable { })(); """ - final class Coordinator: NSObject, WKScriptMessageHandlerWithReply, WKNavigationDelegate, WKUIDelegate { + final class Coordinator: NSObject, WKScriptMessageHandlerWithReply, WKNavigationDelegate, WKUIDelegate, UNUserNotificationCenterDelegate { let model: AppModel weak var web: WKWebView? var loaded: URL? private var installReady = false - init(model: AppModel) { self.model = model } + init(model: AppModel) { + self.model = model + super.init() + UNUserNotificationCenter.current().delegate = self + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.banner, .sound, .list]) + } + + static func notificationContent(_ message: String) -> UNMutableNotificationContent? { + guard !message.isEmpty, message.count <= 500 else { return nil } + let content = UNMutableNotificationContent() + content.title = "Frame Control" + content.body = message + content.sound = .default + return content + } // MARK: bridge @@ -76,6 +96,28 @@ struct WebShell: UIViewRepresentable { } let arg = body["arg"] switch name { + case "notify": + guard message.frameInfo.isMainFrame, + message.frameInfo.securityOrigin.host == "127.0.0.1", + let args = arg as? [String: Any], let text = args["message"] as? String, + let content = Self.notificationContent(text) else { + return replyHandler(nil, "Invalid notification") + } + let center = UNUserNotificationCenter.current() + let send: (Bool, Error?) -> Void = { allowed, error in + guard allowed else { + return replyHandler(nil, error?.localizedDescription ?? "Notifications are off. Enable them in iOS Settings.") + } + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + center.add(request) { error in replyHandler(error == nil, error?.localizedDescription) } + } + if args["request"] as? Bool == true { + center.requestAuthorization(options: [.alert, .sound], completionHandler: send) + } else { + center.getNotificationSettings { settings in + send(settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional, nil) + } + } case "readClipboard": replyHandler(UIPasteboard.general.string ?? "", nil) case "setUpConnection": diff --git a/ios/FrameControlTests/ComfortNotificationTests.swift b/ios/FrameControlTests/ComfortNotificationTests.swift new file mode 100644 index 0000000..68fe4c1 --- /dev/null +++ b/ios/FrameControlTests/ComfortNotificationTests.swift @@ -0,0 +1,14 @@ +import XCTest +import UserNotifications +@testable import Frame_Control + +final class ComfortNotificationTests: XCTestCase { + func testNotificationContentAndBounds() { + let content = WebShell.Coordinator.notificationContent("Time for a break") + XCTAssertEqual(content?.title, "Frame Control") + XCTAssertEqual(content?.body, "Time for a break") + XCTAssertNotNil(content?.sound) + XCTAssertNil(WebShell.Coordinator.notificationContent("")) + XCTAssertNil(WebShell.Coordinator.notificationContent(String(repeating: "x", count: 501))) + } +} diff --git a/ui/index.html b/ui/index.html index 8ac1265..6ad9328 100644 --- a/ui/index.html +++ b/ui/index.html @@ -318,7 +318,7 @@ .disp .k2 { color: var(--muted); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; margin: 12px 0 5px; } .disp .seg { flex-wrap: wrap; } .disp .seg button { padding: 0 10px; } - .disp input[type=number] { width: 72px; background: rgba(0,0,0,.28); color: var(--text); border: 1px solid transparent; + .disp input[type=number], #comfort input[type=number] { width: 72px; background: rgba(0,0,0,.28); color: var(--text); border: 1px solid transparent; border-radius: 3px; height: 32px; padding: 0 8px; font: inherit; font-size: 13px; } /* Touch screens can't hover: keep the library's name and Play button showing. */ @media (hover: none) { .capsule .over { opacity: 1; } .capsule:hover { transform: none; } } @@ -471,6 +471,34 @@ +
+

Family and comfort

+ +
+

Share this screen with people in the room. Casting shows everything the wearer sees, including private content.

+
+
+ + + +
+
+ + + + + +
+
+

Checking session…

+

A one-minute warning, then Steam Home. Games stay running: save and pause first. Keep this app open and connected for notifications.

+
How sessions and alerts work +

This is a reminder, not a parental lock. The timer continues on the Frame if you disconnect; a headset restart cancels it. Cancel before changing settings. Set break/check-in to 0 to turn them off.

+

Alerts run during a session. iOS may suspend phone notifications in the background. Breaks and check-ins count SteamVR activity, not confirmed wear time.

+
+
+
+

Screenshots

@@ -897,6 +925,78 @@ function refresh() { return refreshing; } +// The Frame owns the clock. Poll independently of status and the selected tab. +let comfortBusy = false, comfortSeen = new Set(), comfortSession = null; +async function localNotification(message, request = false) { + if (window.frameApp?.notify) return window.frameApp.notify(message, request); + if (!("Notification" in window)) throw new Error("This browser has no notifications; use the Frame Control app."); + const permission = request ? await Notification.requestPermission() : Notification.permission; + if (permission !== "granted") throw new Error("Notifications are off. Enable them in system settings."); + new Notification("Frame Control", {body: message}); +} +function renderComfort(s) { + $("sessionStart").disabled = !!s.active; + for (const id of ["sessionMinutes", "breakMinutes", "stillMinutes", "batteryAlert", "heatAlert"]) + $(id).disabled = !!s.active; + $("sessionCancel").disabled = !s.active; + if (s.id !== comfortSession) { + comfortSession = s.id; + comfortSeen.clear(); + if (s.options) for (const [key, value] of Object.entries(s.options)) { + const el = $(key === "minutes" ? "sessionMinutes" : key); + if (el.type === "checkbox") el.checked = value; else el.value = value; + } + } + $("comfortStatus").textContent = s.error || (s.active + ? `${Math.ceil(s.remaining / 60)} min until Steam Home · ${s.activity == null ? "activity unknown" : s.activity === 3 ? "headset in standby" : "monitoring"}` + : "No session running."); + if (s.active && s.unavailable?.length) + $("comfortStatus").textContent += " · No readings: " + s.unavailable.join(", "); + for (const e of s.events || []) { + if (comfortSeen.has(e.id)) continue; + comfortSeen.add(e.id); + // Historical events remain visible but never produce a burst on reconnect. + if (s.time - e.time >= 0 && s.time - e.time < 30 && !["started", "cancelled"].includes(e.kind)) { + log(e.message); toast(e.message, e.kind === "error"); + localNotification(e.message).catch(err => { $("comfortStatus").textContent += " · " + err.message; }); + } + } + $("comfortEvents").textContent = (s.events || []).slice(-3).map(e => e.message).join(" · "); +} +async function pollComfort() { + if (!comfortBusy) { + comfortBusy = true; + try { renderComfort(await api("/api/comfort", {action: "status"})); } + catch (e) { $("comfortStatus").textContent = "Session status unavailable: " + e.message; } + finally { comfortBusy = false; } + } + setTimeout(pollComfort, 5000); +} +$("comfortForm").onsubmit = async e => { + e.preventDefault(); + const result = await act("Start session", () => api("/api/comfort", { + action: "start", minutes: Number($("sessionMinutes").value), breakMinutes: Number($("breakMinutes").value), + stillMinutes: Number($("stillMinutes").value), batteryAlert: $("batteryAlert").checked, heatAlert: $("heatAlert").checked, + }), $("sessionStart")); + if (result) renderComfort(result); +}; +$("sessionCancel").onclick = async () => { + const result = await act("Cancel session", () => api("/api/comfort", {action: "cancel"}), $("sessionCancel")); + if (result) renderComfort(result); +}; +$("testNotification").onclick = () => act("Test notification", async () => { + await localNotification("Comfort notifications are enabled on this device.", true); + return {message: "Test notification sent. Check your device's notification settings if it didn't appear."}; +}); +$("castBtn").onclick = () => { + setView("headset"); + if (!live) toggleLive(true); + $("viewer").scrollIntoView({behavior: "smooth", block: "center"}); + // Fullscreen is a direct user gesture; iPhone falls back to its inline viewer. + $("viewer").requestFullscreen?.().catch(() => {}); +}; +pollComfort(); + // ---- battery ---- function battery(b, power) { const pct = b?.percent;