From bb972faba1dc9e821287cedde7a7b5490378c492 Mon Sep 17 00:00:00 2001
From: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:48:31 -0500
Subject: [PATCH] GLXY
---
frogpilot/common/frogpilot_variables.py | 2 +-
frogpilot/system/galaxy/galaxy.py | 186 +++
.../the_pond/assets/components/home/home.css | 4 +-
.../the_pond/assets/components/home/home.js | 26 +-
.../the_pond/assets/components/main.css | 172 ++-
.../navigation/navigation_destination.css | 10 +-
.../components/navigation/navigation_keys.css | 6 +-
.../components/recordings/dashcam_routes.js | 190 ++--
.../recordings/screen_recordings.js | 151 +--
.../the_pond/assets/components/router.js | 24 +-
.../the_pond/assets/components/settings.css | 25 +-
.../the_pond/assets/components/sidebar.css | 35 +-
.../the_pond/assets/components/sidebar.js | 31 +-
.../assets/components/tailscale/tailscale.js | 29 +-
.../components/tools/device_settings.css | 336 ++++++
.../components/tools/device_settings.js | 321 ++++++
.../tools/device_settings_layout.json | 1002 +++++++++++++++++
.../assets/components/tools/theme_maker.css | 22 +-
.../the_pond/assets/components/tools/tmux.js | 86 +-
.../assets/components/tools/toggles.js | 43 +-
.../assets/components/tools/tsk_manager.css | 6 +-
.../assets/images/android-chrome-192x192.png | Bin 12386 -> 21101 bytes
.../assets/images/android-chrome-512x512.png | Bin 39968 -> 104448 bytes
.../the_pond/assets/images/favicon-16x16.png | Bin 680 -> 975 bytes
.../the_pond/assets/images/favicon-32x32.png | Bin 1448 -> 3084 bytes
.../system/the_pond/assets/images/favicon.ico | Bin 15406 -> 9662 bytes
.../the_pond/assets/images/main_logo.png | Bin 1032529 -> 339879 bytes
frogpilot/system/the_pond/assets/js/utils.js | 8 +
.../system/the_pond/assets/manifest.json | 4 +-
.../system/the_pond/templates/index.html | 99 +-
frogpilot/system/the_pond/the_pond.py | 135 ++-
selfdrive/ui/qt/offroad/settings.cc | 80 ++
selfdrive/ui/qt/offroad/settings.h | 14 +
system/manager/manager.py | 5 +-
system/manager/process_config.py | 1 +
tools/StarPilot/derive_feasible_params.py | 121 ++
tools/StarPilot/feasibleparams.txt | 338 ++++++
tools/StarPilot/generate_pond_layout.py | 342 ++++++
38 files changed, 3428 insertions(+), 426 deletions(-)
create mode 100644 frogpilot/system/galaxy/galaxy.py
create mode 100644 frogpilot/system/the_pond/assets/components/tools/device_settings.css
create mode 100644 frogpilot/system/the_pond/assets/components/tools/device_settings.js
create mode 100644 frogpilot/system/the_pond/assets/components/tools/device_settings_layout.json
create mode 100755 tools/StarPilot/derive_feasible_params.py
create mode 100644 tools/StarPilot/feasibleparams.txt
create mode 100755 tools/StarPilot/generate_pond_layout.py
diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py
index 2355b651f..4f1fef9d5 100644
--- a/frogpilot/common/frogpilot_variables.py
+++ b/frogpilot/common/frogpilot_variables.py
@@ -456,7 +456,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("WarningSoftVolume", "101", 2, "101"),
("WheelIcon", "frog", 0, "stock"),
("WheelSpeed", "0", 2, "0"),
- ("StopDistance", "6", 3, "6"),
+ ("StopDistance", "6.0", 3, "6.0"),
("RecoveryPower", "1.0", 2, "1.0")
]
diff --git a/frogpilot/system/galaxy/galaxy.py b/frogpilot/system/galaxy/galaxy.py
new file mode 100644
index 000000000..13cedb81e
--- /dev/null
+++ b/frogpilot/system/galaxy/galaxy.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+import platform
+import shutil
+import signal
+import subprocess
+import tarfile
+import time
+import threading
+import urllib.request
+from http.server import HTTPServer, BaseHTTPRequestHandler
+from pathlib import Path
+
+from openpilot.common.params import Params
+
+GALAXY_DIR = Path("/data/galaxy")
+FRPC_VERSION = "0.67.0"
+FRPC_LOG = GALAXY_DIR / "frpc.log"
+AUTH_PORT = 8083
+
+process = None
+auth_server = None
+
+
+class AuthHandler(BaseHTTPRequestHandler):
+ """Serves only GET /glxyauth — returns the PIN hash file contents."""
+ def do_GET(self):
+ if self.path == "/glxyauth":
+ auth_file = GALAXY_DIR / "glxyauth"
+ if auth_file.exists():
+ data = auth_file.read_bytes()
+ self.send_response(200)
+ self.send_header("Content-Type", "text/plain")
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+ return
+ self.send_response(404)
+ self.end_headers()
+
+ def log_message(self, format, *args):
+ pass # suppress request logs
+
+
+def start_auth_server():
+ global auth_server
+ if auth_server is not None:
+ return
+ auth_server = HTTPServer(("127.0.0.1", AUTH_PORT), AuthHandler)
+ thread = threading.Thread(target=auth_server.serve_forever, daemon=True)
+ thread.start()
+ print(f"Galaxy: Auth server listening on 127.0.0.1:{AUTH_PORT}")
+
+
+def cleanup_frpc(*_):
+ global process
+ if process is not None and process.poll() is None:
+ process.terminate()
+ try:
+ process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ process.kill()
+ process = None
+
+
+def get_arch_url():
+ arch = platform.machine()
+ if arch in ("aarch64", "arm64"):
+ return f"https://github.com/fatedier/frp/releases/download/v{FRPC_VERSION}/frp_{FRPC_VERSION}_linux_arm64.tar.gz", f"frp_{FRPC_VERSION}_linux_arm64"
+ elif arch in ("x86_64", "amd64"):
+ return f"https://github.com/fatedier/frp/releases/download/v{FRPC_VERSION}/frp_{FRPC_VERSION}_linux_amd64.tar.gz", f"frp_{FRPC_VERSION}_linux_amd64"
+ return None, None
+
+
+def setup_frpc():
+ GALAXY_DIR.mkdir(parents=True, exist_ok=True)
+ frpc_bin = GALAXY_DIR / "frpc"
+
+ if not frpc_bin.exists():
+ print("Galaxy: Downloading frpc...")
+ url, folder_name = get_arch_url()
+ if not url:
+ print("Galaxy: Unsupported architecture")
+ return False
+
+ tar_path = GALAXY_DIR / "frp.tar.gz"
+ try:
+ urllib.request.urlretrieve(url, tar_path)
+ with tarfile.open(tar_path, "r:gz") as tar:
+ tar.extractall(path=GALAXY_DIR, filter='data')
+
+ # Move binary
+ extracted_bin = GALAXY_DIR / folder_name / "frpc"
+ extracted_bin.rename(frpc_bin)
+ frpc_bin.chmod(0o755)
+
+ # Cleanup
+ tar_path.unlink()
+ shutil.rmtree(GALAXY_DIR / folder_name)
+ print("Galaxy: frpc downloaded and installed.")
+ except Exception as e:
+ print(f"Galaxy: Failed to install frpc: {e}")
+ return False
+
+ return True
+
+
+def main():
+ global process
+ params = Params()
+
+ signal.signal(signal.SIGTERM, cleanup_frpc)
+ signal.signal(signal.SIGINT, cleanup_frpc)
+
+ # Wait for DongleId to be set (usually set on boot/pairing)
+ dongle_id = params.get("DongleId", encoding='utf8')
+ while not dongle_id:
+ print("Galaxy: Waiting for DongleId...")
+ time.sleep(5)
+ dongle_id = params.get("DongleId", encoding='utf8')
+
+ print(f"Galaxy: DongleId: {dongle_id}")
+ print("Galaxy: Starting manager loop...")
+
+ while True:
+ glxyauth_file = GALAXY_DIR / "glxyauth"
+ galaxy_pin = glxyauth_file.read_text().strip() if glxyauth_file.exists() else None
+ is_paired = galaxy_pin and len(galaxy_pin) == 64
+
+ if is_paired:
+ if process is None or process.poll() is not None:
+ if process is not None:
+ print(f"Galaxy: frpc exited with code {process.returncode}. Restarting...")
+
+ print("Galaxy: PIN set. Preparing frpc tunnel...")
+ if not setup_frpc():
+ print("Galaxy: FRPC setup failed. Retrying later...")
+ time.sleep(10)
+ continue
+
+ # Start the tiny auth HTTP server (serves /glxyauth on localhost)
+ start_auth_server()
+
+ frpc_toml = GALAXY_DIR / "frpc.toml"
+ config = f"""\
+serverAddr = "galaxy.firestar.link"
+serverPort = 7000
+
+[transport]
+tls.enable = true
+poolCount = 2
+
+[[proxies]]
+name = "{dongle_id}_pond"
+type = "http"
+localIP = "127.0.0.1"
+localPort = 8082
+customDomains = ["{dongle_id}.devices.local"]
+transport.useCompression = true
+
+[[proxies]]
+name = "{dongle_id}_auth"
+type = "http"
+localIP = "127.0.0.1"
+localPort = {AUTH_PORT}
+customDomains = ["auth-{dongle_id}.devices.local"]
+"""
+ frpc_toml.write_text(config)
+
+ print("Galaxy: Starting frpc tunnel...")
+ log_file = open(FRPC_LOG, 'a')
+ process = subprocess.Popen(
+ [str(GALAXY_DIR / "frpc"), "-c", str(frpc_toml)],
+ stdout=log_file,
+ stderr=log_file
+ )
+ else:
+ if process is not None and process.poll() is None:
+ print("Galaxy: PIN cleared. Stopping frpc tunnel...")
+ cleanup_frpc()
+
+ time.sleep(3)
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/frogpilot/system/the_pond/assets/components/home/home.css b/frogpilot/system/the_pond/assets/components/home/home.css
index 08e8e8a9f..cc80e874c 100644
--- a/frogpilot/system/the_pond/assets/components/home/home.css
+++ b/frogpilot/system/the_pond/assets/components/home/home.css
@@ -6,7 +6,7 @@
}
.disk .progress {
- background: linear-gradient(to right, green 0%, yellow 80%, orange 90%, red 100%);
+ background: linear-gradient(to right, #5ec8c8 0%, #8b6cc5 60%, #e05577 85%, #c04466 100%);
border-radius: var(--border-radius-md);
height: var(--padding-base);
overflow: hidden;
@@ -78,4 +78,4 @@
gap: 0.5em 1em;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/home/home.js b/frogpilot/system/the_pond/assets/components/home/home.js
index 55312b4e2..7a700cb81 100644
--- a/frogpilot/system/the_pond/assets/components/home/home.js
+++ b/frogpilot/system/the_pond/assets/components/home/home.js
@@ -102,18 +102,18 @@ export function Home() {
return html`
${() => {
- if (state.isLoading) {
- return html`
Loading...
`;
- }
+ if (state.isLoading) {
+ return html`
Loading...
`;
+ }
- if (state.error) {
- return html`
Failed to load data: ${state.error}
`;
- }
+ if (state.error) {
+ return html`
Failed to load data: ${state.error}
`;
+ }
- if (state.data) {
- const { driveStats, firehoseStats, softwareInfo } = state.data;
- return html`
-
The Pond
+ if (state.data) {
+ const { driveStats, firehoseStats, softwareInfo } = state.data;
+ return html`
+
Galaxy
${DriveStat("All Time", driveStats?.all, state.unit)}
@@ -139,10 +139,10 @@ export function Home() {
${renderSoftwareInfo(softwareInfo)}
`;
- }
+ }
- return html`
No data available.
`;
- }}
+ return html`
No data available.
`;
+ }}
`;
}
diff --git a/frogpilot/system/the_pond/assets/components/main.css b/frogpilot/system/the_pond/assets/components/main.css
index 89bdfe320..7a360fa49 100644
--- a/frogpilot/system/the_pond/assets/components/main.css
+++ b/frogpilot/system/the_pond/assets/components/main.css
@@ -20,47 +20,49 @@
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
- /* Colors */
- --accent-bg: #673ab7;
- --accent-hover-bg: #512da8;
- --card-bg: #234423;
+ /* Colors — Galaxy palette (cosmic purple · teal · rose · amber) */
+ --accent-bg: #8b6cc5;
+ --accent-hover-bg: #7558b0;
+ --card-bg: #121224;
--color-black: #000000;
- --color-confirm: #1a73e8;
- --color-confirm-hover: #0758ad;
- --color-gray-100: #f5f5f5;
- --color-gray-200: #e0e0e0;
- --color-gray-300: #c2c2c2;
- --color-gray-400: #a4a4a4;
- --color-gray-500: #8f8f8f;
- --color-gray-600: #737373;
- --color-gray-700: #595959;
- --color-gray-800: #333333;
- --color-gray-900: #1a1a1a;
+ --color-confirm: #8b6cc5;
+ --color-confirm-hover: #7558b0;
+ --color-gray-100: #f0f0f8;
+ --color-gray-200: #d8d8e4;
+ --color-gray-300: #b8b8cc;
+ --color-gray-400: #9898b0;
+ --color-gray-500: #7e7e98;
+ --color-gray-600: #636380;
+ --color-gray-700: #4a4a64;
+ --color-gray-800: #2e2e44;
+ --color-gray-900: #1a1a30;
--color-white: #ffffff;
- --danger-bg: #b71c1c;
- --danger-fg: #ef1313;
- --danger-hover-bg: #d32f2f;
- --input-bg: #2f5432;
- --main-bg: #0b1b0b;
- --main-fg: #178643;
- --secondary-bg: #264026;
- --selected-camera-bg: #1f2f1f;
- --sidebar-active-bg: #64c87826;
- --sidebar-bg: #264026;
- --sidebar-border-color: #1e2e1e;
+ --danger-bg: #e05577;
+ --danger-fg: #e05577;
+ --danger-hover-bg: #c04466;
+ --glow-primary: 0 0 0 2px var(--main-fg), 0 0 10px rgba(139, 108, 197, 0.35);
+ --input-bg: #161630;
+ --main-bg: #06060f;
+ --main-fg: #8b6cc5;
+ --secondary-bg: #0e0e1a;
+ --selected-camera-bg: #0b0b18;
+ --sidebar-active-bg: rgba(139, 108, 197, 0.15);
+ --sidebar-bg: #0a0a16;
+ --sidebar-border-color: #1e1e3e;
--sidebar-fg: #ffffff;
- --sidebar-title-fg: #193446;
- --success-bg: #039226;
- --success-fg: #00a100;
- --success-hover-bg: #01be2d;
- --text-color: #ffffff;
- --text-muted: #a0a0a0;
+ --sidebar-title-fg: #8b6cc5;
+ --success-bg: #5ec8c8;
+ --success-fg: #5ec8c8;
+ --success-hover-bg: #4ab3b3;
+ --switch-inactive-bg: #1e1e3e;
+ --text-color: #e8e8f0;
+ --text-muted: #8080a8;
--text-on-primary: var(--sidebar-fg);
--text-on-surface: var(--text-color);
- --thumb-color: #178643;
- --track-color: #1a3a1a;
- --warning-bg: #ff9800;
- --warning-hover-bg: #f57c00;
+ --thumb-color: #8b6cc5;
+ --track-color: #14142e;
+ --warning-bg: #d4a060;
+ --warning-hover-bg: #b8884a;
/* Effects */
--disabled-opacity: 0.5;
@@ -71,7 +73,7 @@
--hover-scale-lg: scale(1.1);
/* Fonts */
- --font-body: "Open Sans", sans-serif;
+ --font-body: "Inter", "Open Sans", sans-serif;
--font-mono: "Courier New", Courier, monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.85rem;
@@ -96,9 +98,9 @@
--width-xxxxl: 1200px;
/* Shadows */
- --shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
- --shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
- --shadow-md: 0 4px 12px rgba(0,0,0,0.2);
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.2);
/* Spacing */
--gap-xxs: 0.125rem;
@@ -172,12 +174,14 @@ input[type="checkbox"],
input[type="radio"],
.manage-keys-link,
.route_card,
-.sidebar .menu_section > li > ul > li,
+.sidebar .menu_section>li>ul>li,
.clickable {
cursor: pointer;
}
-body { padding-bottom: 0 !important; }
+body {
+ padding-bottom: 0 !important;
+}
/* ――― Layout containers ――― */
.content {
@@ -199,9 +203,17 @@ h3 {
}
/* ――― Helpers & states ――― */
-.hidden { display: none; }
-html { cursor: default; }
-.no_scroll { overflow: hidden; }
+.hidden {
+ display: none;
+}
+
+html {
+ cursor: default;
+}
+
+.no_scroll {
+ overflow: hidden;
+}
.not_implemented {
cursor: not-allowed;
@@ -251,7 +263,9 @@ textarea {
cursor: text;
}
-label[for] { cursor: default; }
+label[for] {
+ cursor: default;
+}
/* ――― Snackbar component ――― */
.snackbar {
@@ -297,6 +311,7 @@ a {
bottom: 0;
opacity: 0;
}
+
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -304,13 +319,20 @@ a {
}
@keyframes fadeout {
- from { opacity: 1; }
- to { opacity: 0; }
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
}
/* ――― Breakpoint overrides ――― */
@media only screen and (max-width: var(--breakpoint-md)) {
- .content { margin-left: 0; }
+ .content {
+ margin-left: 0;
+ }
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
@@ -320,6 +342,7 @@ a {
padding-left: 1rem;
padding-right: 1rem;
}
+
#snackbar_wrapper {
left: 50%;
transform: translateX(-50%);
@@ -333,6 +356,7 @@ a {
bottom: 0;
opacity: 0;
}
+
to {
bottom: var(--snackbar-offset, 30px);
opacity: 1;
@@ -340,18 +364,62 @@ a {
}
@-webkit-keyframes fadeout {
- from { opacity: 1; }
- to { opacity: 0; }
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+/* ——— Tunnel notice banner ——— */
+.tunnel-notice {
+ align-items: center;
+ background: linear-gradient(135deg, var(--card-bg), var(--secondary-bg));
+ border: var(--border-width-thin) var(--border-style-base) var(--sidebar-border-color);
+ border-left: 3px solid var(--main-fg);
+ border-radius: var(--border-radius-lg);
+ box-shadow: var(--shadow-md);
+ color: var(--text-color);
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-md);
+ margin: var(--margin-xl) auto;
+ max-width: var(--width-xl);
+ padding: var(--padding-xl) var(--padding-xxl);
+ text-align: center;
+}
+
+.tunnel-notice-icon {
+ font-size: 2.5rem;
+ opacity: 0.8;
+}
+
+.tunnel-notice-title {
+ color: var(--text-color);
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-bold);
+ margin: 0;
+}
+
+.tunnel-notice-body {
+ color: var(--text-muted);
+ font-size: var(--font-size-base);
+ line-height: var(--line-height-base);
+ margin: 0;
}
::-webkit-scrollbar {
height: 8px;
width: 8px;
}
+
::-webkit-scrollbar-track {
background: var(--track-color);
}
+
::-webkit-scrollbar-thumb {
background-color: var(--thumb-color);
border-radius: var(--border-radius-sm);
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css b/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css
index b535df486..ff816fc15 100644
--- a/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css
+++ b/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.css
@@ -109,7 +109,7 @@
.favorites-toggle-button:hover {
background-color: var(--main-fg);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
color: var(--text-color);
font-weight: var(--font-weight-bold);
transform: var(--hover-scale-sm);
@@ -352,7 +352,7 @@
.navigation-summary-widget button.directions:hover {
background-color: var(--success-hover-bg);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
color: var(--text-color);
transform: var(--hover-scale-sm);
}
@@ -466,12 +466,12 @@
}
.search-provider-toggle button.active {
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
.search-provider-toggle button:hover {
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -533,4 +533,4 @@
min-width: 0;
width: calc(100% - var(--padding-xl));
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css b/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css
index 91ee91dfa..641400395 100644
--- a/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css
+++ b/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.css
@@ -47,7 +47,7 @@
transition: opacity 0.5s ease;
}
-.navkeys-group > .navkeys-row:last-of-type {
+.navkeys-group>.navkeys-row:last-of-type {
margin-bottom: 0;
}
@@ -79,7 +79,7 @@
.navkeys-input:hover,
.navkeys-input:focus {
border-color: var(--thumb-color);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -153,4 +153,4 @@
.navkeys-input {
font-size: var(--font-size-sm);
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js b/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js
index 74e0677a7..84ea0c608 100644
--- a/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js
+++ b/frogpilot/system/the_pond/assets/components/recordings/dashcam_routes.js
@@ -1,4 +1,5 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
+import { isGalaxyTunnel } from "/assets/js/utils.js"
import { getOrdinalSuffix } from "/assets/components/navigation/navigation_utilities.js"
import { Modal } from "/assets/components/modal.js";
@@ -124,27 +125,27 @@ async function deleteRoute(route) {
}
async function resetRouteName(route, dlg) {
- const res = await fetch(`/api/routes/reset_name`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name: route.name })
- });
- if (res.ok) {
- const { timestamp } = await res.json();
- closeDialog(dlg);
- const routeInList = state.routes.find(r => r.name === route.name);
- if (routeInList) {
- routeInList.timestamp = formatRouteDate(timestamp);
- }
- route.timestamp = formatRouteDate(timestamp);
- const overlayTitleSpan = overlay.querySelector(".media-player-title span");
- if (overlayTitleSpan) {
- overlayTitleSpan.textContent = formatRouteDate(timestamp);
- }
- showSnackbar("Route name reset!");
- } else {
- showSnackbar("Resetting name failed...", "error");
+ const res = await fetch(`/api/routes/reset_name`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name: route.name })
+ });
+ if (res.ok) {
+ const { timestamp } = await res.json();
+ closeDialog(dlg);
+ const routeInList = state.routes.find(r => r.name === route.name);
+ if (routeInList) {
+ routeInList.timestamp = formatRouteDate(timestamp);
}
+ route.timestamp = formatRouteDate(timestamp);
+ const overlayTitleSpan = overlay.querySelector(".media-player-title span");
+ if (overlayTitleSpan) {
+ overlayTitleSpan.textContent = formatRouteDate(timestamp);
+ }
+ showSnackbar("Route name reset!");
+ } else {
+ showSnackbar("Resetting name failed...", "error");
+ }
}
async function renameRoute(route) {
@@ -317,6 +318,16 @@ async function deleteAllRoutes() {
}
export function RouteRecordings() {
+ if (isGalaxyTunnel()) {
+ return html`
+
+
🛰️
+
Dashcam Routes Unavailable via Galaxy
+
Loading dashcam routes requires a direct connection. Connect to your device's local network to use this feature.
+
+ `;
+ }
+
if (state.selectedRoute && !overlay) openOverlay(state.selectedRoute);
return html`
@@ -332,75 +343,75 @@ export function RouteRecordings() {
${() => {
- const routesToShow = state.routes.filter(r => !state.showPreservedOnly || r.is_preserved);
+ const routesToShow = state.routes.filter(r => !state.showPreservedOnly || r.is_preserved);
- if (routesToShow.length === 0) {
- if (state.loading && state.total > 0) {
- return html`Processing Routes: ${state.progress} of ${state.total}
`;
- }
- if (state.loading && !state.isDeletingAll) {
- return html`Loading...
`;
- }
- if (state.isDeletingAll) {
- return html`Deleting routes...
`;
- }
- if (state.showPreservedOnly) {
- return html`No preserved routes...
`;
- }
- if (state.error) {
- return html`${state.error}
`;
- }
- return html`No routes found...
`;
- }
+ if (routesToShow.length === 0) {
+ if (state.loading && state.total > 0) {
+ return html`Processing Routes: ${state.progress} of ${state.total}
`;
+ }
+ if (state.loading && !state.isDeletingAll) {
+ return html`Loading...
`;
+ }
+ if (state.isDeletingAll) {
+ return html`Deleting routes...
`;
+ }
+ if (state.showPreservedOnly) {
+ return html`No preserved routes...
`;
+ }
+ if (state.error) {
+ return html`${state.error}
`;
+ }
+ return html`No routes found...
`;
+ }
- return html`
+ return html`
${routesToShow.map(
- route => html`
+ route => html`
{
- if (state.selectedRoute) return;
+ if (state.selectedRoute) return;
- const card = e.currentTarget;
- const gif = card.querySelector(".recording-preview-gif");
- const png = card.querySelector(".recording-preview-png");
+ const card = e.currentTarget;
+ const gif = card.querySelector(".recording-preview-gif");
+ const png = card.querySelector(".recording-preview-png");
- if (card.dataset.gifLoaded) {
- png.style.display = "none";
- gif.style.display = "block";
- return;
- }
+ if (card.dataset.gifLoaded) {
+ png.style.display = "none";
+ gif.style.display = "block";
+ return;
+ }
- card.dataset.loadingGif = "true";
- const preloader = new Image();
- preloader.onload = () => {
- if (card.dataset.loadingGif === "true") {
- gif.src = preloader.src;
- png.style.display = "none";
- gif.style.display = "block";
- card.dataset.gifLoaded = true;
- }
- delete card.dataset.loadingGif;
- };
- preloader.onerror = () => {
- console.error("Failed to load preview GIF:", preloader.src);
- delete card.dataset.loadingGif;
- };
+ card.dataset.loadingGif = "true";
+ const preloader = new Image();
+ preloader.onload = () => {
+ if (card.dataset.loadingGif === "true") {
+ gif.src = preloader.src;
+ png.style.display = "none";
+ gif.style.display = "block";
+ card.dataset.gifLoaded = true;
+ }
+ delete card.dataset.loadingGif;
+ };
+ preloader.onerror = () => {
+ console.error("Failed to load preview GIF:", preloader.src);
+ delete card.dataset.loadingGif;
+ };
- preloader.src = gif.dataset.src;
- }}"
+ preloader.src = gif.dataset.src;
+ }}"
@mouseleave="${e => {
- const card = e.currentTarget;
- card.querySelector(".recording-preview-png").style.display = "block";
- card.querySelector(".recording-preview-gif").style.display = "none";
- if (card.dataset.loadingGif === "true") {
- delete card.dataset.loadingGif;
- }
- }}"
+ const card = e.currentTarget;
+ card.querySelector(".recording-preview-png").style.display = "block";
+ card.querySelector(".recording-preview-gif").style.display = "none";
+ if (card.dataset.loadingGif === "true") {
+ delete card.dataset.loadingGif;
+ }
+ }}"
@click="${() => {
- state.selectedRoute = route;
- }}"
+ state.selectedRoute = route;
+ }}"
>
togglePreserved(route, e)}">
${() => html`
`}
@@ -410,6 +421,7 @@ export function RouteRecordings() {
src="${route.png}"
class="recording-preview recording-preview-png"
style="display:block;"
+ loading="lazy"
>
${route.timestamp}
`
- )}
+ )}
`;
- }}
+ }}
${() => {
- if (state.routes.length > 0) {
- return html`
+ if (state.routes.length > 0) {
+ return html`
(state.showDeleteAllModal = true)}"
@@ -435,17 +447,17 @@ export function RouteRecordings() {
${() => (state.isDeletingAll ? "Deleting..." : "Delete All Routes")}
`;
- }
- return "";
- }}
+ }
+ return "";
+ }}
${() => state.showDeleteAllModal ? Modal({
- title: "Confirm Delete All",
- message: "Are you sure you want to delete all routes? This action cannot be undone...",
- onConfirm: deleteAllRoutes,
- onCancel: () => { state.showDeleteAllModal = false; },
- confirmText: "Delete All"
- }) : ""}
+ title: "Confirm Delete All",
+ message: "Are you sure you want to delete all routes? This action cannot be undone...",
+ onConfirm: deleteAllRoutes,
+ onCancel: () => { state.showDeleteAllModal = false; },
+ confirmText: "Delete All"
+ }) : ""}
`;
}
diff --git a/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js b/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js
index 0a4406e43..a2015a546 100644
--- a/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js
+++ b/frogpilot/system/the_pond/assets/components/recordings/screen_recordings.js
@@ -1,4 +1,5 @@
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({
@@ -142,8 +143,8 @@ async function renameFile(rec) {
}
function confirmDeleteFile(rec) {
- state.recordingToDelete = rec;
- state.showDeleteModal = true;
+ state.recordingToDelete = rec;
+ state.showDeleteModal = true;
}
async function deleteFile() {
@@ -152,11 +153,11 @@ async function deleteFile() {
const res = await fetch(`/api/screen_recordings/delete/${encodeURIComponent(rec.filename)}`, { method: "DELETE" })
if (res.ok) {
- closeOverlay();
- refresh();
- showSnackbar("Recording deleted!");
+ closeOverlay();
+ refresh();
+ showSnackbar("Recording deleted!");
} else {
- showSnackbar("Delete failed...", "error");
+ showSnackbar("Delete failed...", "error");
}
state.showDeleteModal = false;
@@ -221,6 +222,16 @@ async function deleteAllRecordings() {
}
export function ScreenRecordings() {
+ if (isGalaxyTunnel()) {
+ return html`
+
+
🛰️
+
Screen Recordings Unavailable via Galaxy
+
Loading screen recordings requires a direct connection. Connect to your device's local network to use this feature.
+
+ `;
+ }
+
if (state.selectedRecording && !overlay) openOverlay(state.selectedRecording)
return html`
@@ -229,77 +240,77 @@ export function ScreenRecordings() {
Screen Recordings
${() => {
- if (state.loading && state.recordings.length === 0) return html`Loading...
`
- if (state.error) return html`${state.error}
`
- if (state.progress > 0 && state.progress < state.total) {
- return html`Processing Recordings: ${state.progress} of ${state.total}
`
- }
- if (state.recordings.length === 0 && !state.loading) {
- return html`No screen recordings found...
`
- }
- return ""
- }}
+ if (state.loading && state.recordings.length === 0) return html`Loading...
`
+ if (state.error) return html`${state.error}
`
+ if (state.progress > 0 && state.progress < state.total) {
+ return html`Processing Recordings: ${state.progress} of ${state.total}
`
+ }
+ if (state.recordings.length === 0 && !state.loading) {
+ return html`No screen recordings found...
`
+ }
+ return ""
+ }}
${() => state.recordings.map(rec => {
- const displayName = rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenRecordingDate(rec.timestamp)
- return html`
+ const displayName = rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenRecordingDate(rec.timestamp)
+ return html`
{
- if (state.selectedRecording) return;
+ if (state.selectedRecording) return;
- const card = e.currentTarget;
- const gif = card.querySelector(".recording-preview-gif");
- const png = card.querySelector(".recording-preview-png");
+ const card = e.currentTarget;
+ const gif = card.querySelector(".recording-preview-gif");
+ const png = card.querySelector(".recording-preview-png");
- if (card.dataset.gifLoaded) {
- png.style.display = "none";
- gif.style.display = "block";
- return;
- }
+ if (card.dataset.gifLoaded) {
+ png.style.display = "none";
+ gif.style.display = "block";
+ return;
+ }
- card.dataset.loadingGif = "true";
- const preloader = new Image();
- preloader.onload = () => {
- if (card.dataset.loadingGif === "true") {
- gif.src = preloader.src;
- png.style.display = "none";
- gif.style.display = "block";
- card.dataset.gifLoaded = true;
- }
- delete card.dataset.loadingGif;
- };
- preloader.onerror = () => {
- console.error("Failed to load preview GIF:", preloader.src);
- delete card.dataset.loadingGif;
- };
+ card.dataset.loadingGif = "true";
+ const preloader = new Image();
+ preloader.onload = () => {
+ if (card.dataset.loadingGif === "true") {
+ gif.src = preloader.src;
+ png.style.display = "none";
+ gif.style.display = "block";
+ card.dataset.gifLoaded = true;
+ }
+ delete card.dataset.loadingGif;
+ };
+ preloader.onerror = () => {
+ console.error("Failed to load preview GIF:", preloader.src);
+ delete card.dataset.loadingGif;
+ };
- preloader.src = gif.dataset.src;
- }}"
+ preloader.src = gif.dataset.src;
+ }}"
@mouseleave="${e => {
- const card = e.currentTarget;
- card.querySelector(".recording-preview-png").style.display = "block";
- card.querySelector(".recording-preview-gif").style.display = "none";
- if (card.dataset.loadingGif === "true") {
- delete card.dataset.loadingGif;
- }
- }}"
+ const card = e.currentTarget;
+ card.querySelector(".recording-preview-png").style.display = "block";
+ card.querySelector(".recording-preview-gif").style.display = "none";
+ if (card.dataset.loadingGif === "true") {
+ delete card.dataset.loadingGif;
+ }
+ }}"
@click="${() => { state.selectedRecording = rec }}"
>
${displayName}
`
- })}
+ })}
${() => {
- if (state.recordings.length > 0) {
- return html`
+ if (state.recordings.length > 0) {
+ return html`
(state.showDeleteAllModal = true)}"
@@ -307,24 +318,24 @@ export function ScreenRecordings() {
Delete All Recordings
`
- }
- return ""
- }}
+ }
+ return ""
+ }}
${() => state.showDeleteModal ? Modal({
- title: "Confirm Delete",
- message: `Are you sure you want to delete ${state.recordingToDelete.filename} ?`,
- onConfirm: deleteFile,
- onCancel: () => { state.showDeleteModal = false; state.recordingToDelete = null; },
- confirmText: "Delete"
- }) : ""}
+ title: "Confirm Delete",
+ message: `Are you sure you want to delete ${state.recordingToDelete.filename} ?`,
+ onConfirm: deleteFile,
+ onCancel: () => { state.showDeleteModal = false; state.recordingToDelete = null; },
+ confirmText: "Delete"
+ }) : ""}
${() => state.showDeleteAllModal ? Modal({
- title: "Confirm Delete All",
- message: "Are you sure you want to delete all screen recordings? This action cannot be undone...",
- onConfirm: deleteAllRecordings,
- onCancel: () => { state.showDeleteAllModal = false; },
- confirmText: "Delete All"
- }) : ""}
+ title: "Confirm Delete All",
+ message: "Are you sure you want to delete all screen recordings? This action cannot be undone...",
+ onConfirm: deleteAllRecordings,
+ onCancel: () => { state.showDeleteAllModal = false; },
+ confirmText: "Delete All"
+ }) : ""}
`
}
diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js
index 361496da4..657c2e5a9 100644
--- a/frogpilot/system/the_pond/assets/components/router.js
+++ b/frogpilot/system/the_pond/assets/components/router.js
@@ -1,6 +1,7 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { createBrowserHistory, createRouter } from "https://esm.sh/@remix-run/router@1.3.1"
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 { Home } from "/assets/components/home/home.js"
@@ -11,7 +12,6 @@ import { SettingsView } from "/assets/components/settings.js"
import { ScreenRecordings } from "/assets/components/recordings/screen_recordings.js"
import { Sidebar } from "/assets/components/sidebar.js"
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
-import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
import { TmuxLog } from "/assets/components/tools/tmux.js"
import { ToggleControl } from "/assets/components/tools/toggles.js"
@@ -23,13 +23,14 @@ function createRoute(id, path, component) {
return {
id,
path,
- loader: () => {},
+ loader: () => { },
element: component,
}
}
function Root() {
let routes = [
+ createRoute("device_settings", "/device_settings", DeviceSettings),
createRoute("doors", "/lock_or_unlock_doors", DoorControl),
createRoute("errorLogs", "/manage_error_logs", ErrorLogs),
createRoute("navdestination", "/set_navigation_destination", NavDestination),
@@ -39,7 +40,6 @@ function Root() {
createRoute("screen_recordings", "/screen_recordings", ScreenRecordings),
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
- createRoute("tailscale", "/manage_tailscale", TailscaleControl),
createRoute("thememaker", "/theme_maker", ThemeMaker),
createRoute("tmux", "/manage_tmux", TmuxLog),
createRoute("toggles", "/manage_toggles", ToggleControl),
@@ -76,17 +76,17 @@ function Root() {
${() => Sidebar(routerState.activePathFull)}
${() => {
- if (!routerState.initialized || routerState.navigation.state === "loading") {
- return html`
Loading...
`
- }
+ if (!routerState.initialized || routerState.navigation.state === "loading") {
+ return html`
Loading...
`
+ }
- if (routerState.errors?.root?.status === 404) {
- return html`
Not Found `
- }
+ if (routerState.errors?.root?.status === 404) {
+ return html`
Not Found `
+ }
- const match = routes.find(r => r.path === routerState.activePath)
- return match.element({ params: routerState.params })
- }}
+ const match = routes.find(r => r.path === routerState.activePath)
+ return match.element({ params: routerState.params })
+ }}
`
}
diff --git a/frogpilot/system/the_pond/assets/components/settings.css b/frogpilot/system/the_pond/assets/components/settings.css
index 1bf43a43b..2ccc37f00 100644
--- a/frogpilot/system/the_pond/assets/components/settings.css
+++ b/frogpilot/system/the_pond/assets/components/settings.css
@@ -30,7 +30,7 @@
position: relative;
}
-.dropdown > select {
+.dropdown>select {
appearance: none;
background-color: var(--sidebar-bg);
border: var(--border-width-thin) solid var(--sidebar-border-color);
@@ -74,19 +74,19 @@ input.searchfield:focus {
outline: none;
}
-input:checked + .slider {
+input:checked+.slider {
background-color: var(--success-bg);
}
-input:checked + .slider:before {
+input:checked+.slider:before {
transform: translateX(26px);
}
-input:checked + .slider.loading:before {
+input:checked+.slider.loading:before {
animation: rotationChecked 1s linear infinite;
}
-input:focus + .slider {
+input:focus+.slider {
box-shadow: 0 0 var(--border-width-thin) var(--success-bg);
}
@@ -133,11 +133,11 @@ input:focus + .slider {
z-index: 1;
}
-.options > input {
+.options>input {
display: none;
}
-.options > input:checked + label {
+.options>input:checked+label {
background-color: var(--success-bg);
color: var(--text-color);
font-weight: bold;
@@ -220,14 +220,13 @@ input:focus + .slider {
grid-column: span 2;
}
-.setting.subsetting_link {
-}
+.setting.subsetting_link {}
.setting.subtoggle {
margin: 0 0 0 var(--padding-xl);
}
-.setting.subtoggle + .setting {
+.setting.subtoggle+.setting {
margin-top: var(--border-radius-xl);
}
@@ -300,7 +299,7 @@ input.searchfield {
animation: rotation 1s linear infinite;
background-color: none !important;
border: var(--border-width-thick) solid var(--sidebar-fg);
- border-bottom-color: #46439b;
+ border-bottom-color: var(--main-fg);
box-sizing: border-box;
}
@@ -337,6 +336,7 @@ i.switch {
0% {
transform: rotate(0deg);
}
+
100% {
transform: rotate(360deg);
}
@@ -346,7 +346,8 @@ i.switch {
0% {
transform: translateX(26px) rotate(0deg);
}
+
100% {
transform: translateX(26px) rotate(360deg);
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/sidebar.css b/frogpilot/system/the_pond/assets/components/sidebar.css
index c2395e8a8..9f627227d 100644
--- a/frogpilot/system/the_pond/assets/components/sidebar.css
+++ b/frogpilot/system/the_pond/assets/components/sidebar.css
@@ -60,7 +60,10 @@
}
.sidebar {
- background-color: var(--sidebar-bg);
+ background:
+ radial-gradient(ellipse at 30% 80%, rgba(139, 108, 197, 0.06) 0%, transparent 60%),
+ radial-gradient(ellipse at 70% 20%, rgba(94, 200, 200, 0.04) 0%, transparent 50%),
+ var(--sidebar-bg);
border-right: var(--border-width-thin) solid var(--sidebar-border-color);
display: flex;
flex-direction: column;
@@ -102,7 +105,7 @@
padding: 0;
}
-.sidebar .menu_section > li > a span {
+.sidebar .menu_section>li>a span {
color: var(--text-color);
display: inline-block;
font-size: var(--font-size-lg);
@@ -111,13 +114,13 @@
padding-left: var(--padding-sm);
}
-.sidebar .menu_section > li > ul {
+.sidebar .menu_section>li>ul {
list-style: none;
margin: 0;
padding: 0;
}
-.sidebar .menu_section > li > ul > li {
+.sidebar .menu_section>li>ul>li {
border-radius: var(--border-radius-sm);
margin-bottom: var(--padding-sm);
overflow: hidden;
@@ -125,23 +128,23 @@
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
-.sidebar .menu_section > li > ul > li > a.menu-item-link {
+.sidebar .menu_section>li>ul>li>a.menu-item-link {
border-radius: inherit;
}
-.sidebar .menu_section > li > ul > li.active,
-.sidebar .menu_section > li > ul > li:hover {
+.sidebar .menu_section>li>ul>li.active,
+.sidebar .menu_section>li>ul>li:hover {
background-color: var(--sidebar-active-bg);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
-.sidebar .menu_section > li > ul > li.active > a {
+.sidebar .menu_section>li>ul>li.active>a {
color: var(--color-white);
font-weight: var(--font-weight-bold);
}
-.sidebar .menu_section > li > ul > li:hover > a {
+.sidebar .menu_section>li>ul>li:hover>a {
color: var(--color-white);
font-weight: var(--font-weight-demi-bold);
}
@@ -203,9 +206,13 @@
}
.sidebar_header p {
- color: var(--success-hover-bg);
+ background: linear-gradient(135deg, #8b6cc5 0%, #5ec8c8 55%, #d4789c 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
+ letter-spacing: 0.05em;
}
.sidebar_widget {
@@ -234,7 +241,7 @@
transition: var(--transition-fast);
}
- .sidebar .menu_section > li > a {
+ .sidebar .menu_section>li>a {
font-size: var(--padding-sm);
}
@@ -262,7 +269,7 @@
transition: var(--transition-fast);
}
- .sidebar .menu_section > li > a {
+ .sidebar .menu_section>li>a {
font-size: var(--padding-sm);
}
@@ -277,4 +284,4 @@
#menu_button {
display: none;
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js
index bd23d0519..5fbcca1e0 100644
--- a/frogpilot/system/the_pond/assets/components/sidebar.js
+++ b/frogpilot/system/the_pond/assets/components/sidebar.js
@@ -14,10 +14,8 @@ const MenuItems = {
{ name: "Dashcam Routes", link: "/dashcam_routes", icon: "bi-camera-reels" },
{ name: "Screen Recordings", link: "/screen_recordings", icon: "bi-record-circle" },
],
- tailscale: [
- { name: "Tailscale", link: "/manage_tailscale", icon: "bi-wifi" },
- ],
tools: [
+ { 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: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
@@ -88,10 +86,9 @@ export function Sidebar() {
${() => state.selectorAction
- ? TmuxLogSelector({
- action: state.selectorAction,
- closeFn: () => (state.selectorAction = null)
- })
- : ""
- }
+ ? TmuxLogSelector({
+ action: state.selectorAction,
+ closeFn: () => (state.selectorAction = null)
+ })
+ : ""
+ }
${() => logSelectorState.showDeleteAllModal ? Modal({
- title: "Delete All Logs",
- message: "Are you sure you want to delete all of your session logs?",
- onConfirm: deleteAllSessions,
- onCancel: () => { logSelectorState.showDeleteAllModal = false },
- confirmText: "Delete All"
- }) : ""}
+ title: "Delete All Logs",
+ message: "Are you sure you want to delete all of your session logs?",
+ onConfirm: deleteAllSessions,
+ onCancel: () => { logSelectorState.showDeleteAllModal = false },
+ confirmText: "Delete All"
+ }) : ""}
`;
}
diff --git a/frogpilot/system/the_pond/assets/components/tools/toggles.js b/frogpilot/system/the_pond/assets/components/tools/toggles.js
index e3f8c8dfb..db685061a 100644
--- a/frogpilot/system/the_pond/assets/components/tools/toggles.js
+++ b/frogpilot/system/the_pond/assets/components/tools/toggles.js
@@ -1,7 +1,8 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { Modal } from "/assets/components/modal.js"
+import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
-export function ToggleControl () {
+export function ToggleControl() {
const state = reactive({
showResetDefaultModal: false,
showResetStockModal: false,
@@ -14,7 +15,7 @@ export function ToggleControl () {
fileInput.addEventListener("change", restoreToggles)
document.body.appendChild(fileInput)
- async function backupToggles () {
+ async function backupToggles() {
const response = await fetch("/api/toggles/backup", { method: "POST" })
const blob = await response.blob()
@@ -26,7 +27,7 @@ export function ToggleControl () {
URL.revokeObjectURL(downloadUrl)
}
- async function restoreToggles (event) {
+ async function restoreToggles(event) {
const uploadedFile = event.target.files[0]
if (uploadedFile) {
const fileContents = await uploadedFile.text()
@@ -45,11 +46,11 @@ export function ToggleControl () {
}
}
- function confirmResetDefault () {
+ function confirmResetDefault() {
state.showResetDefaultModal = true;
}
- async function resetTogglesToDefault () {
+ async function resetTogglesToDefault() {
state.showResetDefaultModal = false;
showSnackbar("Resetting toggles to their default values...");
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -58,11 +59,11 @@ export function ToggleControl () {
await fetch("/api/toggles/reset_default", { method: "POST" });
}
- function confirmResetStock () {
+ function confirmResetStock() {
state.showResetStockModal = true;
}
- async function resetTogglesToStock () {
+ async function resetTogglesToStock() {
state.showResetStockModal = false;
showSnackbar("Resetting toggles to stock openpilot values...");
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -71,7 +72,7 @@ export function ToggleControl () {
await fetch("/api/toggles/reset_stock", { method: "POST" });
}
- function triggerRestorePrompt () {
+ function triggerRestorePrompt() {
fileInput.click()
}
@@ -98,20 +99,22 @@ export function ToggleControl () {
Reset Toggles to Stock
+
+ ${TailscaleControl()}
${() => state.showResetDefaultModal ? Modal({
- title: "Reset Toggles",
- message: "Are you sure you want to reset all toggles to their default FrogPilot values?",
- onConfirm: resetTogglesToDefault,
- onCancel: () => { state.showResetDefaultModal = false; },
- confirmText: "Reset to Default"
- }) : ""}
+ title: "Reset Toggles",
+ message: "Are you sure you want to reset all toggles to their default FrogPilot values?",
+ onConfirm: resetTogglesToDefault,
+ onCancel: () => { state.showResetDefaultModal = false; },
+ confirmText: "Reset to Default"
+ }) : ""}
${() => state.showResetStockModal ? Modal({
- title: "Reset Toggles",
- message: "Are you sure you want to reset all toggles to stock openpilot values?",
- onConfirm: resetTogglesToStock,
- onCancel: () => { state.showResetStockModal = false; },
- confirmText: "Reset to Stock"
- }) : ""}
+ title: "Reset Toggles",
+ message: "Are you sure you want to reset all toggles to stock openpilot values?",
+ onConfirm: resetTogglesToStock,
+ onCancel: () => { state.showResetStockModal = false; },
+ confirmText: "Reset to Stock"
+ }) : ""}
`
}
diff --git a/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css b/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css
index 5a2f51743..da08a009f 100644
--- a/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css
+++ b/frogpilot/system/the_pond/assets/components/tools/tsk_manager.css
@@ -73,7 +73,7 @@
transition: opacity var(--transition-slow);
}
-.tskkeys-group > .tskkeys-row:last-of-type {
+.tskkeys-group>.tskkeys-row:last-of-type {
margin-bottom: 0;
}
@@ -105,7 +105,7 @@
.tskkeys-input:hover,
.tskkeys-input:focus {
border-color: var(--thumb-color);
- box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
+ box-shadow: var(--glow-primary);
transform: var(--hover-scale-sm);
}
@@ -211,4 +211,4 @@
.tskkeys-input {
font-size: var(--font-size-sm);
}
-}
+}
\ No newline at end of file
diff --git a/frogpilot/system/the_pond/assets/images/android-chrome-192x192.png b/frogpilot/system/the_pond/assets/images/android-chrome-192x192.png
index 4ad74d653d61c17f59d84df1a0d6d728bfc97a12..f0b018fc76bb85d36d7246d512efd23b9919312d 100644
GIT binary patch
literal 21101
zcmXte18^nX^Y+b+ZQHhO+qTV(ag%Ip+jg?CZEv_6C%f^++~9uu{_6kr)H&zr=jlE*
z)m1ZRs-|PqRAiA62oL}O0J6NCl*Yex=)VC6`ERxc=5qr8kN`C$E$M$QA}SX=GCK+u
z9|pe2$NLEmkrWO#FFFQ0JTemztp*OIJTX2$ACCnExdI)7R!mG81FHo&i_!c0UjKRz
zC%Zm}xTmnVt-7qYgsi`$h#M`D$ic5MBefVQ1s6tsdtMFD^7d%wT&s>nw4t{5`Nfoq
zZm^bS+VO*K4u7{0+A1{~7=t!TfesENH
zZCGvus5Z7}EVUxEF`*}3TtC7mpux&1LBXr&OH`|hQh0<{gS~w~pjV-YVvMg%#OT3b
zX-0jLUv*M-w0~Z5n0HLoVtValxwf@m5U9L0rp?Pft+cHzJi0T+Cy#?l{)@&}1wOOu
z>!bA8hUUuF){drpzpCxv9AEtm4eKm7@3P$H*1`2L3BpWb
zK{2s@NkiEY#dgV27Lld%MaA`5>7%vnDXv95C54{;840HKwkGtTN_&lIkh>Efp2{Ua7(RKl?%pC#Zz9zM5B@6wKsX7wU+}l@1T)SJo61
z#|YZzXBbB}CzoV_()yFr6;(~A4jP-fHqwf+zy7PCk?&>qNPFjGt4msKSGrbo^5W&h
z)Ysv(z;$Mz3)?|u&E
z{i{KAz-Uil-TF+HW3yFo@=W`xUr|d!Q@4YTy>V`KQ+0w@VrgFeg1?9N)#h6E)PB8b
zWPXYVHHDa^cSrn8QCQoWl8mv5Rh@#QiJD27ftGV$WnkV?BQv9Xl24j-ZEnrxiH?YR
z_1x{q)^_{A4JVWALetS|?X+3JfKO*Z?cU{jZ|dpRjDWh&$lY#x`^9DdxU!Omr=Ckl
z!*I5HY(QMKvsZp(#&lgu()Wg%@q(J-u=?))7TNefW&i*J;H9CW3Hbc{{CNNP`1t%c
z{u$Ok_rL3({g3?*z5nC>1^l=C5B=x-2mUYaf5HDr|MUOH|BLv~{-^v0-v6P0QBz~%`LUC$>p%MH*Yt|B
zjQ{`@B6%rsE#IG)%c=@vY*;Wip(!+N;-q0@zu<(UY-c7gp|q`0jhVU!mT9K!2&7u7-ty#w_B)|Z7f!w{Zm^hy(n(}9Q1$k9s
zI6s&29{D=A1@42jA&5uV-!Vf&cR7>sqJk$=+?HG|+gNF8
zVvX8(p_5l8W_+t`|H2|&o+pLLz_hGF&mh;%sch@wt{;y?PH`KMl#n#hx>6W)HhtfB
z4R+==p&%PZZ*$D6cOI&3$JTQFv!(o)mGl<#G0RHIV*PL(%Dj)@%fxXNEiP$}x`eW#
zUR2l|WfAgrBzu`E)~D50QSLpiH4@u;z)y0BDwG5c!Ho>LY#S{=OTUoiKw5dq+v+UK
zbns5);2_#F8F)u{BY{amTRPNoqzJ8xT4%VeXS!NKDqWNj^56M9MqZw*MKz-OzQ$q-
z5gp9a=v1U?>NAK1K~+NB4j#@~Xhnf-5oNXwL|Rtmv2W)o-s$-B^atg;+Da4`QgzKM
z^J0Z^RInkqB(|@%*28u>(tKP$QYeTE)St8!(B;h!OvK7aKfTe_@UoKT8GQ*H=n*V0
zei%RMd;?p)wuYl%ptT*!&piZI1yRDm+}|PGAs{>*Z`b$XCde?>{V*Ovyr%TE&}7Ka
z=%mr0Z_z3%u{V%jk=XX-a-Cxm5yjQ|eaCNkfc!epg^aB%80h<>F-_n;JWXJx*>She
zPoRK>XjgYr3?sJ%xuT*F_m6EP_()gPHZA<6h-FL@=htk(jVd@?gn+ntPFSly)VDRNW{83@-FFLlxVYGo-3t0rXQ+tINc_T(CNFNjt|SJfIQV#8Qv
z%hp^vzmEwhvGlGRVL_!{J+ZOO*dEEC&`A7aQJ^
zzE&S-(j;A7L82;lPzxV9angxkK^Iw(76UHZbYgFUT(VxNI(!N(v_y%5kmgRVf;f1m
zS8PZT2*n410k3?rZ<-T7`y22hwZ48%KQP~h(ye>jWv1WzH})X7c(1?v{paHjHMW7X
zW}5b5yw%iA$(V8&@f|xfUo8K|S@RieEvkq0>A(mwU5$Gd_y+b5fgifaXes>0gd)Jk7^W!BL
zSb7bA5pj<%EDvrVIN`=)n_4WbKHo@?XfI4PThWiwRH}Y}9MAX?(}A-^T0h&I2N0c|
ztzroXsc#l@#HlJ$K|c|}hMnFtKsw(qfyA|XXlgO6%j`~|OA3l+$hhf4S)-KbJT||z
z*XgYou+*shJ5IjvR(YpE&g&1^$taX;b->Jq(bi6H*a`1l@GI{SIEdf`7ulS)u1IT>
zXS3sA_qM(zMM?b`W=GxMqU3sWe%gI`sqKV)=jEjrm4{L~?(7)_o;;!yV7
z_!XR+G>ho~k-O<`o5R}hBcgjYG7Mz!M6843Ntu}GX7g6yJ^+bHx4i`PPCzE>fWb9GH|I*!b
z_!9p)4xq=tWX6sG9Mc`3LM+@KBp!cQ?j84%_N_Q{ZaspGZF$9jOa5J~
z@kv!&YA82Lme`UB?u49J#7{3R|(0zJ8#?{!`V=gHJ-qT)x
z(9?HQObs1`4#J?_zM>9=b(R3Yw#pQ!kG!kp#E!{!Q#o+JO5?_s7D|(*tv}w*y7WHR
zI<@;^=6ixc>&{zO@o}SPs}Aihttx1&Oem%M&EuPn5AU{=AxE^3rwv`sJ&?%FW}goO
zL42bne@T%c;(p{nMuVQotYn8E=%*AlHO}KS5MBaD3QhZA_G$pW$qm@H_gj7(@*&DU
zo$V(L@~8+o}VR$M3zD>cYJt@UE!7RRImyv$?3WkVCjW5?36Fci%pH{
zZz^tH-i^qi)KD>ADQ-_Qgl>)b+{m8#kY+kT;`)BI=~6+T{ERwb!@9NG_)^iGM)nxMzU-$UC6V1Qx=V#
z-hfCZwgfue_fiy>4a^MIr)S<%W{wiIY<507zlO;$U831V-aN6&t!02_6$!I7R_x!u
zCq;W=XP#N7u!?Zdh3vyX0t$PDpnf0*7DACK`L71!JUI|qj6mZ{4IrhuSeTw(zRa(W
zxnQY?gcYBJ#5<`Q0vAJz0P%1Z&V;eFr8?w9MVJUJ(Q1kaN7C4sb@#gqN6bLW7w;w+
zkc|d}&9==c}@>=R2rfD+Mmh4F83s8xmn?FFh4_^>x72Bb6?hnr1gIHJW6KGy?MU
z7lV3~oK)oE(}NfPX_B&PS=6LTVnPagt{)=&f+bem&l%AGn`ahbO~bA4Vefs5$UK0E=XlikBRQpz*Mg_p-hMqqW0UzuQlW_NiV8*?-gazy?C9)^Oj)t-=zp3o
z;<6>mqs?KEzX+ok>AiotA`Wox4G4I6pQm^CXz7+$7uf&c(w)_xquTwzk(@qHw19d-qVch@u~Q*X7^3#E?(&de
zY^GeClls<}q+y38LQS$@Nr(H9l{oLS`cV%8`?
zH{(Am6p5A`jyQrpKm%En*eTWxxo~Taak)|hsZuxmVcahsFBrib09;Vjd+X2IU%!m3
zTeqC9kRZBvJ_+2IO&(4opb^E9I37~x10w?|0V->qv`b%(N7UL
za*Jqmvxm`vL>E1w2P{RSG^%I*
zv-X3**fV=h>)ugnwv1|EY+o~1F9$9d)Hq3hl-3v|+Mm9E+Qb`x?-9}@
zurGu-Dok8R3R(mf0J24Lz#=TvrBZj$`@ct;yW
z#Tur;Q3KIr!INPjra=lG_L-ipYJ^RHCprW;5SWoZ4LB{Zq<99_7h<-
zV>`OA*TQxPK}g!gmUDJXg@zIN6XLjCHrcbu&1^V=o}Ps9_#0cw_E}TXZLRFknBkIB
zX)K08%@=082&Dl&=J+aJ8%d&{FL4GpueGXOr@&G(XOEft@vcm5{sIRpI=4Rq=T!{3
z#p(?@y?(9yta)w{VK9v$ckX2b9nzkZdO&XW9ZIxfM&PT}$$}W*xDC2V1$px3&>Ej<
zLv8Ar?1oSD;o%aR3*Qb!s;(!Om~ANdgChf5&t^XeC)f)a_^E?9@Se&imK8(3(K6j!
zJQjoWXkyadhbU`9DgJs~J73l2mjVGnX+B*15s^Ui^}QOypWv3&ihIcnzPlN4OAbVW
zO(bSRa^Q0Q1;e!?4INJ@7XJI_2t%1)UdO(jD;*&k{oDl&jhM7>DApQrAQH6KY-dL7
zARbGPKecn^WLPL!H*iNG{@OL;XUH`1A;)v7=E~^8q|6r=K}7=>KTwMB1&P*9)*cZ21`wFQ&*ro)^VKVvR=kneS!{-5ZZV9N
zl->_eEtTR$t~VC8daqKK^Y4YO?@b1-0|uG#MjPtiAAm?iT8a2@SaqH)VFGC00lg+xJrLupkD1Rnp!RjEK<`96Ji0!X<@XG73@?{{q%57=@m+%~7#
zCHX#iVFgShY+o>hYw42kq+1y2nZ*ZjUhN&){#1{^dNhf;2I8rBP=VFXeGNE%Ll2eZ+M1RNRBl}{yij2eXjg2l~_*pyetjxFAzL@-Fhk#>nj?iUr9nT78rgh%6c
z4wOG0A)UBy-^j4=h>RZ11fo2-;EGS=MON8s=&^Ep1N~fr?D``r*CfoLLiD6cS1L!{
z+$sl*7Ki1Mt@wzxiTeuR03RhEn3$Mt55w0du{OK@#jL{Dmh&CWZf1k|xHEN2se!h3Bx|-B7jTlfkDd{EN
z%7x3YfnMBnMxMyv_y!-3C$Yiv5pr(@FT@%{+RkkW^52sPjg6p1{$%yTQJNnRfdXyA
zg`!HRt&P>Ay@YMo#xU4wb9^6B#EQB!4-d~YN2V5Ca&qt?iOB{EK`Gi0zz@B3;!G5?
zoTgMY@72Pvc#+OwWu-;`E19ar-<_=i9qknUy^jQ}R-d4Aa<{O3U|r`#mm+sc2=$&w
z2&E>i9Ih)IAY3@z{)Iwgb0lNx_$hC}CC#dkZwg1cJ;%Wbjv|?DnQ@W!vxUy9#oIcG%Ke@`P??JLYCBmAGnDS_DXcH;VrTQQ4adv70eE&-FyWkFL;t;9h=YC$xb#O_)c5z@G6hl{
z>0`Yx^hZ!&SLQ0Ibgnt^gn!j*iM4h
z3<6EfDi4?xjg?g)3dJZ8X>c>^MjG>ITc6TH?3F~uFBI=DSHz16idY&Or$Ycw7bF%)i%)1No-uM*djwU~JoYY_WR`Vep>0q!Xm6
z&<*k8i2-EaN7ahuit!{iViB|`6myxh2Fe-sYzc`ufO1W+z6{PRy0bTj0|x|aET`6T
zl99pjYZ2o1VqP?eNZy@E+?+7NXOhCgc%a7zC8#G+G3<^-95Z{Q_>We#N9S9(C71+ozY@k
zKNsrj>iCnd@bSqJ;-v_H$>V>oM0C^xY)$R;?X$vv9a$q&+`mk5{LU>UNFa+*`WR-pO^tOY$c@7_PEKZJw?BBTfBm844?`r-*TkD}fG-MY5oS*w62Z@%v@gSO<
zz0Bp1L*Vk2FRfv~ONe6-0F2o^Rq5Q4LAQYiiJgym_{<8RHLs%vNU!
zoI?JA;N%vUS1+lgQ0rntR>RAO#z64+iPD0~8wPgWnMj
zqJzN?9XB5l#m_v44TcJKiV+_Q$D??!16Cozg!NU|VnB4%M&rnT?Fvg-vy(A{j4^BL
zj`WQ3R0^-XgstcaXXksRMj)7LKO}2L!;dkd0@6;i4NZJNJe>(PGCD5ul<3?~5`Vj6
zJ0zX-{2_Qx9^sOg#*G;DDjee1X-FmobX+v%0}jg*s;0CaC%~L<&+!%
z@kY0|{|pYEjWh<8n$=h|rBwuSxxxuY`;-mEjh%(xAB!-T)zwD!;sW6FXh8l|DgWxX
z(|T-Eo>m%qbcF*FQ#?bdOg}k9!U6oakKa3to^&?loWR~
zv3H|BK4~bx=YnN>d%KVBgR(V8lF(gtqUW!BI&+!xq;HpY--m`v=zNdj#ozQ&)HEh#mET1Npc5uf9J+JTev`^zA|d&1o5G3K@6Ogq?L-aO}IkTX5>|
zNBU3n4%H)obNK198}
z7O+^=+5STJ<}gR2M@Fw_fhHJcwa#(G)~ghZ1Xo_#I|Jia@#EDn<150u9W>8}jl5Ue
zvX2V|iv
z$@`BTP?GkjO%}R2-BM$j!-dtCiXe8GPT7EG-*DwC=GoC+50-tlo|xxi;>xBWXfd1vq)ctMTcJatD5RISUb|{0~Vh>
z3s3S_p7T#0*)%y)5Wy3O30Ou%7z>_bz(MVG;&1MTKjX$wuCncJa9wXo;4bx|n*H%5XGHoQ49Mo)c`
zGw2o=*Tp1KJGp36;gEqZcHkvpZhL!dRoyP83w#UgetJA1-MS?6_ZfhZiyT%jg9D0j
zpt@2Y5(HEW0VKfIyKLJ?mnH)>{WkwD_pcRLSLe9Q=#uaU)a<@w7D`0eFn|BBvmkH?
z1Mu=J&wmz3PWQ7QG()WTF9X0ILhs2j;|R^E%CiNaP^i|s<@cmJX%cvY52}ZJn3(mVTUo>FdQl3$f(k;N_
zm7&d+%tzHF_9D$a^FG~3A}IM1nlReg
z4(wiMf%!1XOSgw;EdNv+qx8Q9HjFR?aG~R;uXCJ+yJC-g{ByAW64lamKcK~aD?dLN
zFWe$azJvvnL*>4UtTO?WT7L2asKl=rbQe6jt2K2I4|b<&*=&S`K8Mg53aYPA1V#?b
z@+(*V?7FsMKbDqKnC}`GFaXe8fo6`5_K4tU0xdu$>6)F(mtqoNfg%)>5nEi}Lw!$t
zqpmO{B(j&N^)-hwF$&OZDHbr*lZ7Wuivt1EA`ri5tb&0KMDgi$-KtpDh1lPmEB0E;
z9`|bX7isHJAg9QyYIOyuf8O*cYPD5H5TT@q&2U(79}8F=I*`DDR|*Pr`Wm#a<760kwhh%s;5!{D*06K
zLy}+spl8b;ZG7g3W-T|xyeA$B9voOeld%&mq6jkfpG5e}#q9~L13xcY`E0v^+^#nEV8g4~|+cw63)#+q=n>bF-$UX|^3*&5ag_=yEd3T{hU^5PrZW~_a0~d%8QL~lnXl&MSMg4X
zRuLf3i;rq@Q?6ZaXo<(iITxDmdqn2>+p2P=^rhH24;df^az;U;IP_^=CvHm!k>0aU`;2Qq
zEjOgU#U6PLD}9Lor#grNel7j{8M`R4O
zoMR~)Jq?F@_hgghZ&u)8k5s1Ea)1WV&MBCyxZ0lFVQ&k0=
zG#sAKOH>Uv75KZmMpexG)A8zFU|+8YKFA40v=GcK0CjcP;b;|HF4=YMj|6ZnVVQ7C
z#lqcb<+sAJ=bBA|lDob1SzKxU!>kueto^LKuA02->(kS7
zgUjm?a5QgK@@Nh8G;3@6Jf155U40Sj{x@{w`<$%p?}M7{;6u@O#amHyu*x0+U~Jk!
z*_g$~$
z+rP(>!8KLF7cMdBhHqhqJ+>qUt@>x>`N%c2k?u1b@mWWAfxOCP-A5C~(xZAPAk=gI
ztThY(XN<-fkZjo*@aO4o(mc^g2q|=Rd|W|JAqlZ@
zexRGXyL;_mNG-)BE0^I&lE`;Z6)dY~hJP
z#nB#S7F@Dq0+}}z;hXh2YU%xk414+X>R&7T32asbJa`KI%i}AS?
z@T#rdCq=T-S}7h0-J(Q)<}s;;dVaeBH|^?@qe1CtuR-8Vquk>d9IN!s}gLLl?e=
zTl+IE9Nt;GwxdG88S+Wb9a?$}mfn|2K|)a5n5(CPFLb`U;9~b*R+8%oBZ5{&gi)YM
z&v_%BbmcpJ^xyg_N8XALN*RRIss%hPBlq0{&DfKi1kyJi?MqF>dltk+b2;g{Y!mzg
zPk(hG9~6n(_X2Jz1ibvdGSbrv3xE84cqpeTgw}#rh{(@^7>@7pei=m#2pX{5h7}I@
zOVDqEHSM+Vx@<-}zqG(z=wKwWK7E7Bg6Z0KhR#|ikB=Z~v_BzKbF{{9sui>lx3r<{s}9%;
z^!F#ne{S-eCYXqY1AEqsr+zZserL=l>->Fi$X?PnF)?0~cyi0iMXqrgHwbuWp@sxJ
z7HYk2{UIbYKDe&R?mvSx*ts@B^Be2Oq+8$cG%j
zIzau6rgpWbXI1o%;xL-HneYWP1Uk$#U}%}wKAhO^djyO<-}j(CRRM1$p1b;$YJ7qF
zsX*sLZc$l?!hoVb)A?ul_!exIMVp(Oc7<3s^J(+*Y+MM$VE_OW0E)<)+%RP;mg4ET
zU-~;@4IFg5#OkYGDilB*jg4Z=NZZJW%B%d85>JW+;%2|pDKv1hFZp3{adm=%5lc1C
zSt)zoYWZELok8^IeS}zl0B@Yq3im=v7Odq7)r%i021OGizFXSmKUt#*sD4^=U3#gy
z*Sw|Ksr#(6YA=_7E;8pkH)Uui0APuuAw@|upaam&;p|dz@*PKAf4wxBcje_Lgpb}l9e8<
zM39{P_nZK>I2qVs(!hM@XeD*LNyQYb(hKSYK1>Ho`HSO*z?<*!Ui6tRKbK?YoaroN
zlvBS+a^|Q_Fk>0dUd6jt#YE@W`G~wwaC6g}Qomji4!j7T_~JK#b6Q(_xt~8nPW%@5
zeHMgmMbad%(qiMlUa*ruz{|m?c1>isT-1L0z_z8P%J=dmUhqY|e<#>lLnu;riz+1G
z?rU!!+3oe)3>*~P3>4tE>sp_#q@;Y}B@(G~eLVqS!Gs$?1*8)P0Px$=$0f>kwp2yx
zb9wC7(+C0O-Nkx-!~i%kG8nl}s9N$Ge+A4ogO|mud&hMbK^OXR2(%|;>3~-GAcaHw
zF!1@`=M=OtbamfC*}x^CAzE+>L7I#_;5*!9?WcZdmvq$rfG$IEer&boNxDmxxxM3)|jFT-MMIp{4E(!;&$FqvPP_BUAM
z3~6-VWN6J=%;qMLuj`ZiGe`4P@|x9C(nH4;71K1x>B|XTKq?k3#N>_SOL>J@qPtoU@0L0G@=%l7&u@0dOwx^cbPYW@hJqo#k~cQt`F|U!v1Sx?UD5
z;{a1PiWf#6ek&~gYTeN&1}=(IqjRUct*;*^DaJ
z;<`MW#Zd*W;lLE!@ncW~NyqY0CK(e$5EI+E5rG{QlvoeC=mx0_++?})=*
z_9z7cq%hLxTSAO7^$0K{5MOG0aX}h2etu4-q@U<-Og>O%Re;k$IHZX1^}F3_*#}MC
z9HA1w&aE%T?}`xL2tCSg{=U7b5@iR0q44!JW;~-azdaclaLW=Zm=1lieUkgGMVkF;
zJDMcdQ^sDd*L$F-AQR)viYZiX9o#b%+!aCcz@dU^f5zZWxG_Q(qBP
zsCF_a_9PB9MTpbA{2dff(5pwQJ+A^9TgGXfA6#_$&==$W+1a&6_dfl~jut(7^#r#M
zq{%%*Aj6Hxdy`{*nQE-Fd_yeQ;qixy#sJjL4?kMBkyBApwb78wM_WDP_dwrxGNrk)
z?sq7zntFg-g9AOF97-9^$q-@**9(7{(2e#y*6T8gcc4DCBwO%X;GL3`)hBEt-3`)4
zaY1!3m*coDDpo8IXa#X9hpv1g|6G~{$;3CA(oD50e
z$%~;xVn2-v2}Camx3WLv_9RadC|PNJ!KJ$7iq{)Q9y6Fbx%`~neVSvv--RkJF8^w)
zs;ZBId2J(z1D+dr7^Kj`{1n?O4*Qc8=4pe6HI+n9x!2YZ#4Di^g2r6bqAN8Z!Elk)
zzx|l9?D9%nSHCRQk-&iB3?
z$O}qk52OU@5pfqJNBu${{_&Nrtc@kq*;M}|c5>Ew@^J>qZ~Sb((2bql9x!$8
zjgmX7yeEIS2LXB5)*zoO=gjrg2Czq
z|Nj59;$@AnA(drjuB)#u|FnpqMv~8nEveA{ZV7L_)J7ngj$RsuB~EA%rdKD&gli=c
zCRdN{$U2s($6^#MM`hH7PO}abbMofcU+J2klbGW*@VvM1ItUKf^NWQnS7R$Nu-j_a
zAwWjZoxxg=y(3Ej#Xu30dyk*=nB(Ew|2;E-rknD${nx678^g6m_nqXtt_aFur`Mp=
zm?F5e>K2@aezBsPw4%^o{=m_(D;UJaM~M|Llp-NEl6VudcsEF9)#rJ*K*t5ALfw{<}VjJg?Zkz=5f5IRx)R3I#AM-`4S
zsO}7h0Z(cE4z2UNgmv5xx@AdN9L&A=zW)^k6MN9r*3}r3J>~Rd{BLT?{a%pcIO1cg
zT?o}Dhnj|ZjewT}on9n4bJC;q8a^dn8Dec6GPWp43VB`?w)*x2QewEXwQ6PkTa5!I
z%VXhGgVn4aywhuRMX9mm`OEB0xS|r){<5{>QosZv^4L!a8auY!uo8Yxt87fi`S34q
zWJ5ovb)iU>h?%vMK7&dBW?&c=Ks+>3K*=!*bW607-_$Y=Lr3x8LHQ~zNm}KQX^nUs-Xinkqavn
z20AO{UwhKmV(yei;bC5d5}MF}(fom85lvS*W%^wXc;<(BKa(Ete>*F)v#L?+J98KC
zt}Q!yul{K;)RPa?0kVEWyFnU2hK!;K0)!V33$Vc&z6COPM{Igwi^lbA+gUH`BBDVB
zI>e?A*8!q<381YtxNF_#@aCQ49UjEu6ds3K;3@D3ea$QBu!#~)Iy`W_EXlm6KF_qt
z7SGFY=94!VSF+rXDk$MdzOtRv%xI1!PwKGftZiG^$5K}ATl(ML6i^M($Y#Bc<88z&}K(YWv@`fkR
z=@Rpy@w%qai%tPrs~(;7d?jY
zRz@*U%Y-po-Pu_|3iILgzbB(OSjK|m3*>0r&DQ8+M^1r61_s@Nm$gNGUUwD##=7%B
z2wl&MVcrCW@Qq&OhWs6_Ue0}=v*O*Lv+v&(uh!nq!U9W?NI+JzBhZbb`~FBU+Xpk0
zOeE+zR}jHJ$A8g&NzHA>p|x;|CRPe>>^XMVZ_ujHg+W$SrlHN)+ilPcXcf%6^fpz9
z!F7$D1ko+JlVPCXs^DyT(!(WX53!9zDtRjK=%9y$p@_|Q3TBMJrqG~m5_uCwHml^d
zV|pWI498{IXu?)S9h&ts622=x)2f;vbXe2g!)2PvygQkXMA%EQ2u}CyyKEc=52k{A~za4JK^NyQ~|dI3pK0G{%P^1=5qI>eBw
zm_G`2HWUiiD?b&8)qdYt4A$81rJ(eCW35*WfF(Z!nScu}@APq$=t1
zd$FL{!H5H35cn~wt&D0hR{{>U+bp3t%``oR-tm18pZC-ZM7QiaecaodHDoj)hys?E
zmTnF=<7kz0z3TqhcqTqYEhF$a>edRD!YscK_qsbYm+K}+!Hk5Kton(ej?l9EV~j
zg&K9~(iRFc9i4%k7}AN@8o6&>nHrKF?l+B%ebG_}Z%RHLCc(hzn8*#ER=mlP}cqMg0Cc&4S67D-RwOoJ&NoMlL=~Y~iLfBW-$-fIyX`PFn`(G$ZpzKG
zr~h((fn*V#%UDPC&Tk@c^kFau&5XmN1iySe`4A0*+By3O^ZI-E`GLEFV@9wxd2rsn
za{arWCM&*dt&h8e&*{5w-&W68PWxFG{#nDJ{};azK<+}+y|bKVK!HQ#a{OtFe%%{r
zVY#?t#I)?T6%&&sQLHYHYMCp2JobJ8F?O_aj