From c84acea993fc6aba832ae9a0d4fab1c712a3530e Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:28:27 -0500 Subject: [PATCH] Galaxy Pair Locally --- .../the_pond/assets/components/router.js | 2 + .../the_pond/assets/components/sidebar.js | 1 + .../assets/components/tools/galaxy.css | 137 +++++++++++++++ .../assets/components/tools/galaxy.js | 166 ++++++++++++++++++ .../system/the_pond/templates/index.html | 1 + frogpilot/system/the_pond/the_pond.py | 39 ++++ 6 files changed, 346 insertions(+) create mode 100644 frogpilot/system/the_pond/assets/components/tools/galaxy.css create mode 100644 frogpilot/system/the_pond/assets/components/tools/galaxy.js diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js index c17e80575..2cec5de07 100644 --- a/frogpilot/system/the_pond/assets/components/router.js +++ b/frogpilot/system/the_pond/assets/components/router.js @@ -4,6 +4,7 @@ import { hideSidebar } from "/assets/js/utils.js" import { DeviceSettings } from "/assets/components/tools/device_settings.js" import { DoorControl } from "/assets/components/tools/doors.js" import { ErrorLogs } from "/assets/components/tools/error_logs.js" +import { GalaxyPairing } from "/assets/components/tools/galaxy.js" import { Home } from "/assets/components/home/home.js" import { NavDestination } from "/assets/components/navigation/navigation_destination.js" import { NavKeys } from "/assets/components/navigation/navigation_keys.js" @@ -34,6 +35,7 @@ function Root() { createRoute("device_settings", "/device_settings", DeviceSettings), createRoute("doors", "/lock_or_unlock_doors", DoorControl), createRoute("errorLogs", "/manage_error_logs", ErrorLogs), + createRoute("galaxy", "/galaxy", GalaxyPairing), createRoute("navdestination", "/set_navigation_destination", NavDestination), createRoute("navkeys", "/manage_navigation_keys", NavKeys), createRoute("root", "/", Home), diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js index 7dc930fb1..008a83dd6 100644 --- a/frogpilot/system/the_pond/assets/components/sidebar.js +++ b/frogpilot/system/the_pond/assets/components/sidebar.js @@ -18,6 +18,7 @@ const MenuItems = { { name: "Device Settings", link: "/device_settings", icon: "bi-sliders" }, { name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" }, { name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" }, + { name: "Galaxy", link: "/galaxy", icon: "bi-globe2" }, { name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" }, { name: "Model Manager", link: "/manage_models", icon: "bi-cpu" }, { name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" }, diff --git a/frogpilot/system/the_pond/assets/components/tools/galaxy.css b/frogpilot/system/the_pond/assets/components/tools/galaxy.css new file mode 100644 index 000000000..2363d7d2c --- /dev/null +++ b/frogpilot/system/the_pond/assets/components/tools/galaxy.css @@ -0,0 +1,137 @@ +/* ――― Galaxy Pairing Widget ――― */ +.galaxy-wrapper { + max-width: var(--width-xxxl); + padding: var(--padding-base) var(--padding-lg) var(--padding-xxl); +} + +.galaxy-loading { + color: var(--text-muted); + padding: var(--padding-xl); + text-align: center; +} + +.galaxy-widget { + align-items: center; + background-color: var(--secondary-bg); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + gap: var(--gap-lg); + max-width: var(--width-lg); + padding: var(--padding-xl); + transition: box-shadow var(--transition-fast), transform var(--transition-fast); +} + +.galaxy-widget:hover { + box-shadow: var(--shadow-md); + transform: var(--hover-scale-sm); +} + +/* ――― Status Badge ――― */ +.galaxy-status-badge { + align-items: center; + border-radius: 2rem; + display: inline-flex; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + gap: var(--gap-xs); + padding: 0.3rem 1rem; +} + +.galaxy-paired { + background-color: rgba(94, 200, 200, 0.15); + color: var(--success-fg); +} + +.galaxy-unpaired { + background-color: rgba(224, 85, 119, 0.15); + color: var(--danger-fg); +} + +/* ――― Text ――― */ +.galaxy-text { + color: var(--text-color); + line-height: var(--line-height-base); + text-align: center; +} + +/* ――― URL Link ――― */ +.galaxy-url { + color: var(--main-fg); + font-size: var(--font-size-base); + font-weight: var(--font-weight-demi-bold); + text-decoration: underline; + text-underline-offset: 3px; + transition: color var(--transition-fast); + word-break: break-all; +} + +.galaxy-url:hover { + color: var(--accent-hover-bg); +} + +/* ――― Input Group ――― */ +.galaxy-input-group { + display: flex; + flex-direction: column; + gap: var(--gap-sm); + width: 100%; +} + +.galaxy-input { + background-color: var(--input-bg); + border: var(--border-style-input); + border-radius: var(--border-radius-lg); + box-sizing: border-box; + color: var(--text-color); + font-family: var(--font-body); + font-size: var(--font-size-base); + outline: none; + padding: var(--padding-sm) var(--padding-base); + transition: box-shadow var(--transition-fast); + width: 100%; +} + +.galaxy-input:focus { + box-shadow: 0 0 0 2px var(--main-fg); +} + +/* ――― Buttons ――― */ +.galaxy-button { + background-color: var(--input-bg); + border: none; + border-radius: var(--border-radius-lg); + color: var(--text-color); + font-size: var(--font-size-base); + font-weight: var(--font-weight-demi-bold); + padding: var(--padding-sm) var(--padding-base); + text-align: center; + transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); + width: 100%; +} + +.galaxy-button:hover:not(:disabled) { + box-shadow: var(--shadow-md); + transform: var(--hover-scale-sm); +} + +.galaxy-button:disabled { + opacity: var(--disabled-opacity); +} + +.galaxy-button-danger { + background-color: rgba(224, 85, 119, 0.2); + color: var(--danger-fg); +} + +.galaxy-button-danger:hover:not(:disabled) { + background-color: rgba(224, 85, 119, 0.35); +} + +/* ――― Mobile ――― */ +@media only screen and (max-width: 768px) and (orientation: portrait) { + .galaxy-wrapper { + padding: var(--padding-sm) var(--padding-base) var(--padding-xl); + } +} diff --git a/frogpilot/system/the_pond/assets/components/tools/galaxy.js b/frogpilot/system/the_pond/assets/components/tools/galaxy.js new file mode 100644 index 000000000..0c37c5c94 --- /dev/null +++ b/frogpilot/system/the_pond/assets/components/tools/galaxy.js @@ -0,0 +1,166 @@ +import { html, reactive } from "https://esm.sh/@arrow-js/core" +import { isGalaxyTunnel } from "/assets/js/utils.js" +import { Modal } from "/assets/components/modal.js" + +const state = reactive({ + paired: false, + url: "", + password: "", + loading: true, + submitting: false, + showUnpairModal: false, + fetched: false, +}) + +async function fetchStatus() { + state.loading = true + try { + const res = await fetch("/api/galaxy/status") + const data = await res.json() + state.paired = data.paired + state.url = data.url + } catch (e) { + console.error("Failed to fetch Galaxy status:", e) + } + state.loading = false +} + +async function pair() { + if (state.submitting) return + const pw = state.password.trim() + if (pw.length < 6) { + showSnackbar("Password must be at least 6 characters.") + return + } + + state.submitting = true + try { + const res = await fetch("/api/galaxy/pair", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: pw }), + }) + const data = await res.json() + if (res.ok) { + state.paired = true + state.url = data.url + state.password = "" + // Clear the DOM input value directly (Arrow.js doesn't two-way bind) + const input = document.querySelector(".galaxy-input") + if (input) input.value = "" + showSnackbar(data.message || "Paired!") + } else { + showSnackbar(data.error || "Pairing failed.") + } + } catch (e) { + showSnackbar("Network error — is the device reachable?") + } + state.submitting = false +} + +async function unpair() { + state.showUnpairModal = false + state.submitting = true + try { + const res = await fetch("/api/galaxy/unpair", { method: "POST" }) + const data = await res.json() + if (res.ok) { + state.paired = false + state.url = "" + showSnackbar(data.message || "Unpaired!") + } else { + showSnackbar(data.error || "Unpairing failed.") + } + } catch (e) { + showSnackbar("Network error — is the device reachable?") + } + state.submitting = false +} + +export function GalaxyPairing() { + if (isGalaxyTunnel()) { + return html` +
+
🛰️
+

Galaxy Pairing Unavailable via Galaxy

+

Galaxy pairing requires a direct connection.
Connect to your device's local network to use this feature.

+
+ `; + } + + if (!state.fetched) { + state.fetched = true + fetchStatus() + } + + return html` +
+

Galaxy

+ + ${() => { + if (state.loading) { + return html`
Checking pairing status…
` + } + + if (state.paired) { + return html` +
+
+ Paired +
+

+ Your device is paired with Galaxy. Access it remotely at: +

+ + ${state.url} + + +
+ + ${() => state.showUnpairModal ? Modal({ + title: "Confirm Unpair", + message: "Are you sure you want to unpair from Galaxy? You will lose remote access until you pair again.", + onConfirm: unpair, + onCancel: () => { state.showUnpairModal = false }, + confirmText: "Unpair", + }) : ""} + ` + } + + return html` +
+
+ Not Paired +
+

+ Pair your device with Galaxy to access The Pond remotely from anywhere. + Enter a password to secure your connection. +

+
+ + +
+
+ ` + }} +
+ ` +} diff --git a/frogpilot/system/the_pond/templates/index.html b/frogpilot/system/the_pond/templates/index.html index a731efdf2..dbb30f792 100644 --- a/frogpilot/system/the_pond/templates/index.html +++ b/frogpilot/system/the_pond/templates/index.html @@ -34,6 +34,7 @@ + diff --git a/frogpilot/system/the_pond/the_pond.py b/frogpilot/system/the_pond/the_pond.py index 04c4d81ec..3a9f4f134 100644 --- a/frogpilot/system/the_pond/the_pond.py +++ b/frogpilot/system/the_pond/the_pond.py @@ -1153,6 +1153,45 @@ def setup(app): }, } + # ── Galaxy pairing (mirrors settings.cc L262-282) ────────────────── + GALAXY_DIR = Path("/data/galaxy") + GALAXY_AUTH_FILE = GALAXY_DIR / "glxyauth" + + @app.route("/api/galaxy/status", methods=["GET"]) + def galaxy_status(): + try: + paired = GALAXY_AUTH_FILE.is_file() and len(GALAXY_AUTH_FILE.read_text().strip()) == 64 + except Exception: + paired = False + dongle_id = params.get("DongleId", encoding="utf8") or "" + return jsonify({ + "paired": paired, + "url": f"https://galaxy.firestar.link/{dongle_id}" if dongle_id else "", + }) + + @app.route("/api/galaxy/pair", methods=["POST"]) + def galaxy_pair(): + data = request.get_json() or {} + password = (data.get("password") or "").strip() + if len(password) < 6: + return jsonify({"error": "Password must be at least 6 characters."}), 400 + + pw_hash = hashlib.sha256(password.encode()).hexdigest() + GALAXY_DIR.mkdir(parents=True, exist_ok=True) + GALAXY_AUTH_FILE.write_text(pw_hash) + + dongle_id = params.get("DongleId", encoding="utf8") or "" + return jsonify({ + "message": "Pairing successful!", + "url": f"https://galaxy.firestar.link/{dongle_id}" if dongle_id else "", + }) + + @app.route("/api/galaxy/unpair", methods=["POST"]) + def galaxy_unpair(): + if GALAXY_AUTH_FILE.is_file(): + GALAXY_AUTH_FILE.unlink() + return jsonify({"message": "Galaxy unpaired successfully."}) + @app.route("/api/tailscale/installed", methods=["GET"]) def tailscale_installed(): base = "/data/tailscale"