From 41424d267bbecf321f568c8b3cf71f4c8ebeae13 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:06:14 -0400 Subject: [PATCH] Revert "Fix Galaxy" This reverts commit 4dac64e666f520078763d48190900a75a7a116c3. --- .../the_pond/assets/components/home/home.css | 6 - .../the_pond/assets/components/home/home.js | 249 +++++++++--------- .../the_pond/assets/components/modal.js | 2 +- .../navigation/navigation_destination.js | 2 +- .../components/navigation/navigation_keys.js | 2 +- .../components/recordings/dashcam_routes.js | 120 ++------- .../recordings/screen_recordings.js | 2 +- .../the_pond/assets/components/router.js | 73 +---- .../the_pond/assets/components/settings.js | 2 +- .../the_pond/assets/components/sidebar.js | 156 +++++------ .../assets/components/tailscale/tailscale.js | 2 +- .../components/tools/device_settings.css | 52 ---- .../components/tools/device_settings.js | 71 +---- .../the_pond/assets/components/tools/doors.js | 2 +- .../assets/components/tools/error_logs.js | 2 +- .../assets/components/tools/galaxy.js | 2 +- .../tools/longitudinal_maneuvers.js | 2 +- .../assets/components/tools/model_manager.js | 2 +- .../the_pond/assets/components/tools/plots.js | 2 +- .../assets/components/tools/speed_limits.js | 2 +- .../assets/components/tools/testing_ground.js | 2 +- .../assets/components/tools/theme_maker.js | 2 +- .../the_pond/assets/components/tools/tmux.js | 2 +- .../assets/components/tools/toggles.js | 2 +- .../assets/components/tools/troubleshoot.js | 2 +- .../assets/components/tools/tsk_manager.js | 2 +- .../assets/components/tools/update_manager.js | 2 +- .../components/tools/vehicle_features.js | 2 +- .../the_pond/assets/vendor/arrow-core.js | 3 - .../assets/vendor/remix-router-1.3.1.js | 17 -- .../system/the_pond/templates/index.html | 15 +- 31 files changed, 241 insertions(+), 563 deletions(-) delete mode 100644 frogpilot/system/the_pond/assets/vendor/arrow-core.js delete mode 100644 frogpilot/system/the_pond/assets/vendor/remix-router-1.3.1.js diff --git a/frogpilot/system/the_pond/assets/components/home/home.css b/frogpilot/system/the_pond/assets/components/home/home.css index 9ab6684fb..2b707acfd 100644 --- a/frogpilot/system/the_pond/assets/components/home/home.css +++ b/frogpilot/system/the_pond/assets/components/home/home.css @@ -35,12 +35,6 @@ margin: 0; } -.softwareGrid p { - min-width: 0; - overflow-wrap: anywhere; - word-break: break-word; -} - .drivingStat { display: grid; grid-template-columns: 1fr 1fr 1fr; diff --git a/frogpilot/system/the_pond/assets/components/home/home.js b/frogpilot/system/the_pond/assets/components/home/home.js index f59432f62..adc3c27c7 100644 --- a/frogpilot/system/the_pond/assets/components/home/home.js +++ b/frogpilot/system/the_pond/assets/components/home/home.js @@ -1,152 +1,145 @@ -import { html } from "/assets/vendor/arrow-core.js"; +import { html, reactive } from "https://esm.sh/@arrow-js/core"; -const HOME_STATE = { - status: "loading", // loading | ready | error - data: null, - unit: "miles", - error: "", - initialized: false, -}; +function DiskUsage(disk) { + const used = parseFloat(disk.usedPercentage) || 0; + const rightRadius = used >= 100 ? "0" : "var(--border-radius-md)"; -function withTimeout(promise, timeoutMs, label) { - return new Promise((resolve, reject) => { - const timerId = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs); - promise.then((value) => { - clearTimeout(timerId); - resolve(value); - }).catch((err) => { - clearTimeout(timerId); - reject(err); - }); - }); + return html` +
+

${disk.used} used of ${disk.size}

+
+
+
+
+ `; } -function formatInt(value) { - const n = Number(value || 0); - return Number.isFinite(n) ? n.toLocaleString("en-US", { maximumFractionDigits: 0 }) : "0"; -} +function DriveStat(title, stats = {}, defaultUnit) { + const format = (n) => + n?.toLocaleString("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }) ?? "0"; -function renderDiskUsageSection(state) { - const { data } = state; - const shell = document.getElementById("home_shell"); - if (!shell) return; - - if (state.status === "error") { - shell.innerHTML = `

Failed to load data: ${state.error}

`; - return; - } - - if (state.status !== "ready" || !data) { - shell.innerHTML = "

Loading...

"; - return; - } - - const driveStats = data.driveStats || {}; - const softwareInfo = data.softwareInfo || {}; - const diskUsage = Array.isArray(data.diskUsage) ? data.diskUsage : []; - const diskError = Array.isArray(data.diskError) ? data.diskError : []; - - const statBlock = (title, stats = {}) => ` + return html`

${title}

-

${formatInt(stats.drives)}

drives

-

${formatInt(stats.distance)}

${stats.unit || state.unit}

-

${formatInt(stats.hours)}

hours

-
- `; - - const diskBlock = (disk = {}) => { - const usedPct = Number.parseFloat(disk.usedPercentage) || 0; - const rightRadius = usedPct >= 100 ? "0" : "var(--border-radius-md)"; - return ` -
-

${disk.used || "0 GB"} used of ${disk.size || "0 GB"}

-
-
-
-
- `; - }; - - const softwareFields = [ - ["Branch Name", softwareInfo.branchName], - ["Build", softwareInfo.buildEnvironment], - ["Commit Hash", softwareInfo.commitHash], - ["Fork Maintainer", softwareInfo.forkMaintainer], - ["Update Available", softwareInfo.updateAvailable], - ["Version Date", softwareInfo.versionDate], - ]; - - const softwareMarkup = softwareFields - .map(([label, value]) => `

${label}: ${value ?? "Unknown"}

`) - .join(""); - - const diskMarkup = diskError.length - ? `

${diskError.join("
")}

` - : (diskUsage.length ? diskUsage.map(diskBlock).join("") : diskBlock({})); - - shell.innerHTML = ` -
-

Galaxy

- -
- ${statBlock("All Time", driveStats.all)} - ${statBlock("Past Week", driveStats.week)} - ${statBlock("FrogPilot", driveStats.frogpilot)} -
- -

Disk Usage

-
- ${diskMarkup} -
- -

Software Info

-
-
${softwareMarkup}
-
+

${format(stats.drives)}

drives

+

${format(stats.distance)}

${stats.unit ?? defaultUnit}

+

${format(stats.hours)}

hours

`; } -async function initializeHome() { +function renderSoftwareInfo(info = {}) { + const fields = [ + ["Branch Name", info.branchName], + ["Build", info.buildEnvironment], + ["Commit Hash", info.commitHash], + ["Fork Maintainer", info.forkMaintainer], + ["Update Available", info.updateAvailable], + ["Version Date", info.versionDate], + ]; + + return fields.map( + ([label, value]) => + html`

${label}: ${value ?? "Unknown"}

` + ); +} + +function renderDiskUsageSection({ diskError, diskUsage }) { + if (diskError) { + return html`

${diskError.join("
")}

`; + } + if (diskUsage?.length) { + return diskUsage.map(DiskUsage); + } + return DiskUsage({ size: "0 GB", used: "0 GB", usedPercentage: "0" }); +} + +const state = reactive({ + data: null, + unit: "miles", + isLoading: true, + error: null, +}); + +let initialized = false; + +async function initialize() { try { const [statsResponse, unitResponse] = await Promise.all([ - withTimeout(fetch("/api/stats"), 5000, "stats request"), - withTimeout(fetch("/api/params?key=IsMetric"), 5000, "metric request"), + fetch("/api/stats"), + fetch("/api/params?key=IsMetric"), ]); - if (!statsResponse.ok) throw new Error(`stats API error: ${statsResponse.status}`); - if (!unitResponse.ok) throw new Error(`params API error: ${unitResponse.status}`); + if (!statsResponse.ok) throw new Error(`API error: ${statsResponse.statusText}`); + if (!unitResponse.ok) throw new Error(`API error: ${unitResponse.statusText}`); - const statsJson = await withTimeout(statsResponse.json(), 5000, "stats JSON parse"); - const isMetricText = (await withTimeout(unitResponse.text(), 5000, "metric read")).trim(); + const statsJson = await statsResponse.json(); + const isMetricText = (await unitResponse.text()).trim(); + const isMetric = isMetricText === "1"; - HOME_STATE.data = statsJson; - HOME_STATE.unit = isMetricText === "1" ? "kilometers" : "miles"; - HOME_STATE.status = "ready"; + state.data = statsJson; + state.unit = isMetric ? "kilometers" : "miles"; + localStorage.setItem("isMetric", isMetricText); } catch (err) { - HOME_STATE.status = "error"; - HOME_STATE.error = err?.message || String(err); + console.error("Failed to initialize component:", err); + state.error = err.message; + } finally { + state.isLoading = false; } - - renderDiskUsageSection(HOME_STATE); } export function Home() { - setTimeout(() => { - renderDiskUsageSection(HOME_STATE); - if (!HOME_STATE.initialized) { - HOME_STATE.initialized = true; - initializeHome(); - } - }, 0); + if (!initialized) { + initialized = true; + initialize(); + } - return html`

Loading...

`; + return html` +
+ ${() => { + if (state.isLoading) { + return html`

Loading...

`; + } + + if (state.error) { + return html`

Failed to load data: ${state.error}

`; + } + + if (state.data) { + const { driveStats, softwareInfo } = state.data; + return html` +

Galaxy

+ +
+ ${DriveStat("All Time", driveStats?.all, state.unit)} + ${DriveStat("Past Week", driveStats?.week, state.unit)} + ${DriveStat("FrogPilot", driveStats?.frogpilot, state.unit)} +
+ +

Disk Usage

+
+ ${renderDiskUsageSection(state.data)} +
+ +

Software Info

+
+
${renderSoftwareInfo(softwareInfo)}
+
+ `; + } + + return html`

No data available.

`; + }} +
+ `; } diff --git a/frogpilot/system/the_pond/assets/components/modal.js b/frogpilot/system/the_pond/assets/components/modal.js index bb2bdb03d..cd3af9e46 100644 --- a/frogpilot/system/the_pond/assets/components/modal.js +++ b/frogpilot/system/the_pond/assets/components/modal.js @@ -1,4 +1,4 @@ -import { html } from "/assets/vendor/arrow-core.js"; +import { html } from "https://esm.sh/@arrow-js/core"; export function Modal({ title, diff --git a/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.js b/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.js index 755308fc9..6be8d6406 100644 --- a/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.js +++ b/frogpilot/system/the_pond/assets/components/navigation/navigation_destination.js @@ -1,4 +1,4 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js"; +import { html, reactive } from "https://esm.sh/@arrow-js/core"; import { addRouteToMap, formatMetersToHuman, diff --git a/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.js b/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.js index 9ce319038..3fd881811 100644 --- a/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.js +++ b/frogpilot/system/the_pond/assets/components/navigation/navigation_keys.js @@ -1,4 +1,4 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js" +import { html, reactive } from "https://esm.sh/@arrow-js/core" import { Modal } from "/assets/components/modal.js"; export function NavKeys() { 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 0eec5f34e..7f82fdb91 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,4 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js" +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"; @@ -13,18 +13,8 @@ const state = reactive({ total: 0, showDeleteAllModal: false, isDeletingAll: false, - truncated: false, }) -const MAX_RENDERED_ROUTES = 250 -const ROUTE_FLUSH_INTERVAL_MS = 120 - -let routesAbortController = null -let routesRequestToken = 0 -let pendingRoutes = [] -let flushTimerId = null -let seenRouteNames = new Set() - function formatRouteDate(dateString) { const date = new Date(dateString) if (isNaN(date.getTime())) { @@ -42,73 +32,10 @@ function formatRouteDate(dateString) { return `${month} ${day}${getOrdinalSuffix(day)}, ${year} - ${hour}:${minuteStr}${ampm}` } -function resetRouteStreamState() { - pendingRoutes = [] - if (flushTimerId !== null) { - clearTimeout(flushTimerId) - flushTimerId = null - } - seenRouteNames = new Set() -} - -function flushPendingRoutes() { - if (pendingRoutes.length === 0) return - - const availableSlots = Math.max(MAX_RENDERED_ROUTES - state.routes.length, 0) - if (availableSlots <= 0) { - pendingRoutes = [] - state.truncated = true - return - } - - const toAppend = pendingRoutes.slice(0, availableSlots) - pendingRoutes = [] - if (toAppend.length > 0) { - state.routes = [...state.routes, ...toAppend] - } - if (state.routes.length >= MAX_RENDERED_ROUTES) { - state.truncated = true - } -} - -function enqueueRoutes(rawRoutes) { - if (!Array.isArray(rawRoutes) || rawRoutes.length === 0) return - - const nextRoutes = [] - for (const route of rawRoutes) { - const name = String(route?.name || "") - if (!name || seenRouteNames.has(name)) continue - seenRouteNames.add(name) - nextRoutes.push({ - ...route, - timestamp: formatRouteDate(route.timestamp), - }) - } - - if (nextRoutes.length === 0) return - pendingRoutes.push(...nextRoutes) - - if (flushTimerId === null) { - flushTimerId = setTimeout(() => { - flushTimerId = null - flushPendingRoutes() - }, ROUTE_FLUSH_INTERVAL_MS) - } -} - async function fetchRoutes() { - const requestToken = ++routesRequestToken - if (routesAbortController) { - routesAbortController.abort() - } - const controller = new AbortController() - routesAbortController = controller - try { const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const response = await fetch(`/api/routes?timezone=${encodeURIComponent(userTimezone)}`, { - signal: controller.signal, - }); + const response = await fetch(`/api/routes?timezone=${encodeURIComponent(userTimezone)}`); if (!response.ok) throw new Error(); const reader = response.body.getReader(); @@ -119,24 +46,24 @@ async function fetchRoutes() { const { value, done } = await reader.read(); if (done) break; - if (requestToken !== routesRequestToken) return - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split(/\r?\n\r?\n/); + const lines = buffer.split("\n\n"); buffer = lines.pop(); for (const line of lines) { - if (line.startsWith("data:")) { + if (line.startsWith("data: ")) { try { - const payload = line.substring(5).trim() - if (!payload) continue - const data = JSON.parse(payload); + const data = JSON.parse(line.substring(6)); if (data.progress !== undefined && data.total !== undefined) { state.progress = data.progress; state.total = data.total; } if (data.routes) { - enqueueRoutes(data.routes) + const routes = data.routes.map(route => ({ + ...route, + timestamp: formatRouteDate(route.timestamp), + })); + state.routes.push(...routes); } } catch (e) { console.error("Failed to parse JSON:", e); @@ -144,35 +71,21 @@ async function fetchRoutes() { } } } - flushPendingRoutes() - } catch (error) { - if (error?.name !== "AbortError") { - state.error = "Couldn't load routes. Please try again later..." - } + } catch (_) { + state.error = "Couldn't load routes. Please try again later..." } finally { - if (requestToken === routesRequestToken) { - flushPendingRoutes() - state.loading = false - if (routesAbortController === controller) { - routesAbortController = null - } - } + state.loading = false } } +fetchRoutes() + function refresh() { state.loading = true - state.error = null state.routes = [] - state.progress = 0 - state.total = 0 - state.truncated = false - resetRouteStreamState() fetchRoutes() } -refresh() - let overlay = null function openDialog(htmlStr) { @@ -478,9 +391,6 @@ export function RouteRecordings() { `; }} - ${() => state.truncated ? html` -

Showing first ${MAX_RENDERED_ROUTES} routes to keep the UI responsive.

- ` : ""} ${() => { if (state.routes.length > 0) { return html` 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 09958e47c..9ed184988 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,4 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js" +import { html, reactive } from "https://esm.sh/@arrow-js/core" import { isGalaxyTunnel } from "/assets/js/utils.js" import { Modal } from "/assets/components/modal.js"; diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js index 68d6659f6..a0ff96bc6 100644 --- a/frogpilot/system/the_pond/assets/components/router.js +++ b/frogpilot/system/the_pond/assets/components/router.js @@ -1,5 +1,5 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js" -import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js" +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 { ErrorLogs } from "/assets/components/tools/error_logs.js" @@ -25,15 +25,6 @@ import { UpdateManager } from "/assets/components/tools/update_manager.js" let router, routerState -function toRouterHref(href) { - try { - const url = new URL(href, window.location.origin) - return `${url.pathname}${url.search}${url.hash}` - } catch { - return href - } -} - function createRoute(id, path, component) { return { id, @@ -43,20 +34,6 @@ function createRoute(id, path, component) { } } -function SafeHome() { - return html` -
-

Galaxy

-

Safe mode is active while UI rendering is being repaired.

-

- Galaxy Pairing | - Toggles | - Software -

-
- ` -} - function Root() { let routes = [ createRoute("device_settings", "/device_settings/:section?", DeviceSettings), @@ -86,11 +63,6 @@ function Root() { history: createBrowserHistory(), }).initialize() - window.__thePondNavigate = (href) => { - router.navigate(toRouterHref(href)) - window.scrollTo(0, 0) - } - routerState = reactive({ activePath: "/", activePathFull: "/", @@ -98,13 +70,10 @@ function Root() { navigation: { state: "loading" }, errors: [], params: {}, - activeMatch: null, }) router.subscribe(({ initialized, navigation, matches, errors }) => { - const match = Array.isArray(matches) && matches.length > 0 - ? matches[matches.length - 1] - : null + const [match] = matches || [] Object.assign(routerState, { initialized, activePath: match?.route?.path || "", @@ -112,7 +81,6 @@ function Root() { navigation, params: match?.params || {}, errors, - activeMatch: match, }) }) @@ -128,12 +96,12 @@ function Root() { return html`

Not Found

` } - const routeElement = routerState.activeMatch?.route?.element - if (typeof routeElement !== "function") { - console.warn("[router] no route element for path:", routerState.activePathFull) + const match = routes.find(r => r.path === routerState.activePath) + if (!match) { + console.warn("[router] no route match for path:", routerState.activePathFull) return Home({ params: routerState.params }) } - return routeElement({ params: routerState.params }) + return match.element({ params: routerState.params }) }} ` @@ -145,7 +113,7 @@ export function Link(href, children, onClick, classes = "") { href="${() => href}" @click="${(e) => { e.preventDefault() - router.navigate(toRouterHref(e.currentTarget.href)) + router.navigate(e.currentTarget.href) hideSidebar() onClick?.() }}" @@ -153,28 +121,13 @@ export function Link(href, children, onClick, classes = "") { } export function Navigate(href) { - router.navigate(toRouterHref(href)) + router.navigate(href) window.scrollTo(0, 0) } -function mountRouterWhenReady() { - const mountNode = document.getElementById("app") - if (!mountNode) { - // Some embedded browsers execute module scripts before is fully available. - setTimeout(mountRouterWhenReady, 0) - return - } - - if (!window.__thePondRouterMounted) { - window.__thePondRouterMounted = true - Root()(mountNode) - } else { - console.warn("[router] duplicate mount prevented") - } -} - -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", mountRouterWhenReady, { once: true }) +if (!window.__thePondRouterMounted) { + window.__thePondRouterMounted = true + Root()(document.getElementById("app")) } else { - mountRouterWhenReady() + console.warn("[router] duplicate mount prevented") } diff --git a/frogpilot/system/the_pond/assets/components/settings.js b/frogpilot/system/the_pond/assets/components/settings.js index c8e85f592..ba666ecce 100644 --- a/frogpilot/system/the_pond/assets/components/settings.js +++ b/frogpilot/system/the_pond/assets/components/settings.js @@ -1,4 +1,4 @@ -import { html } from "/assets/vendor/arrow-core.js" +import { html } from "https://esm.sh/@arrow-js/core" import { upperFirst } from "/assets/js/utils.js" import { Navigate } from "/assets/components/router.js" diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js index d838fa175..13bf1cc70 100644 --- a/frogpilot/system/the_pond/assets/components/sidebar.js +++ b/frogpilot/system/the_pond/assets/components/sidebar.js @@ -1,7 +1,8 @@ -import { html } from "/assets/vendor/arrow-core.js"; -import { hideSidebar, upperFirst } from "/assets/js/utils.js"; +import { html, reactive } from "https://esm.sh/@arrow-js/core"; +import { Link } from "/assets/components/router.js"; +import { upperFirst, hideSidebar } from "/assets/js/utils.js"; -const MENU_ITEMS = { +const MenuItems = { home: [ { name: "Home", link: "/", icon: "bi-house-fill" }, ], @@ -31,89 +32,36 @@ const MENU_ITEMS = { ], }; -function matchesPath(currentPath, link) { - if (link === "/") return currentPath === "/"; - return currentPath === link || currentPath.startsWith(`${link}/`); -} +const state = reactive({ + activeRoute: "" +}); -function buildSectionMarkup(section, links, currentPath) { - const linksMarkup = links.map((link) => { - const active = matchesPath(currentPath, link.link) ? "active" : ""; - return ` -
  • - - - ${upperFirst(link.name)} - -
  • - `; - }).join(""); +export function Sidebar() { + const currentPath = window.location.pathname; + const matchesPath = (link) => { + if (link === "/") return currentPath === "/"; + return currentPath === link || currentPath.startsWith(`${link}/`); + }; + const activeItem = Object.values(MenuItems).flat().find(item => matchesPath(item.link)); + state.activeRoute = activeItem?.name ?? ""; - return ` - - `; -} -function bindSidebarHandlers() { - const menuButton = document.getElementById("menu_button"); - const underlay = document.getElementById("sidebarUnderlay"); + function navigate(link) { + state.activeRoute = link.name; + window.scrollTo(0, 0); + hideSidebar(); - if (!menuButton || !underlay) return; - - if (!window.__thePondSidebarMenuBound) { - window.__thePondSidebarMenuBound = true; - menuButton.addEventListener("click", () => { - const sidebar = document.getElementById("sidebar"); - const currentUnderlay = document.getElementById("sidebarUnderlay"); - if (!sidebar || !currentUnderlay) return; - sidebar.classList.toggle("visible"); - currentUnderlay.classList.toggle("hidden"); + document.querySelectorAll('.sidebar li').forEach(el => { + el.classList.remove('active'); }); + + const linkElement = document.querySelector(`.sidebar li a[href="${link.link}"]`); + if (linkElement) { + linkElement.parentElement.classList.add('active'); + } } - underlay.onclick = hideSidebar; - - document.querySelectorAll("#sidebar a.menu-item-link").forEach((anchor) => { - if (anchor.dataset.boundClick === "1") return; - anchor.dataset.boundClick = "1"; - anchor.addEventListener("click", (event) => { - if (event.defaultPrevented) return; - if (event.button !== 0) return; - if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; - - event.preventDefault(); - const href = anchor.getAttribute("href") || "/"; - const navigate = window.__thePondNavigate; - if (typeof navigate === "function") { - navigate(href); - } else { - window.location.assign(href); - } - hideSidebar(); - window.scrollTo(0, 0); - }); - }); -} - -function renderSidebarIntoShell(currentPath) { - const shell = document.getElementById("sidebar_shell"); - if (!shell) return; - - const activePath = currentPath || window.location.pathname; - const sectionsMarkup = Object.entries(MENU_ITEMS) - .map(([section, links]) => buildSectionMarkup(section, links, activePath)) - .join(""); - - shell.innerHTML = ` + return html`
    - ${sectionsMarkup} + ${() => Object.entries(MenuItems).map(([section, links]) => html` + + `)} - - `; - - bindSidebarHandlers(); + `; } -export function Sidebar(currentPath) { - setTimeout(() => renderSidebarIntoShell(currentPath), 0); - return html``; +function setupMenuButton() { + const button = document.getElementById("menu_button"); + const sidebar = document.getElementById("sidebar"); + const underlay = document.getElementById("sidebarUnderlay"); + + button.addEventListener("click", () => { + sidebar.classList.toggle("visible"); + underlay.classList.toggle("hidden"); + }); + + underlay.addEventListener("click", hideSidebar); } + +document.addEventListener("DOMContentLoaded", setupMenuButton, false); diff --git a/frogpilot/system/the_pond/assets/components/tailscale/tailscale.js b/frogpilot/system/the_pond/assets/components/tailscale/tailscale.js index 1fa55b693..a5b5b97e8 100644 --- a/frogpilot/system/the_pond/assets/components/tailscale/tailscale.js +++ b/frogpilot/system/the_pond/assets/components/tailscale/tailscale.js @@ -1,4 +1,4 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js" +import { html, reactive } from "https://esm.sh/@arrow-js/core" import { Modal } from "/assets/components/modal.js"; export function TailscaleControl() { diff --git a/frogpilot/system/the_pond/assets/components/tools/device_settings.css b/frogpilot/system/the_pond/assets/components/tools/device_settings.css index df689f370..10660b7ad 100644 --- a/frogpilot/system/the_pond/assets/components/tools/device_settings.css +++ b/frogpilot/system/the_pond/assets/components/tools/device_settings.css @@ -367,58 +367,6 @@ font-weight: var(--font-weight-semibold); } -.ds-step-value { - color: var(--text-muted); -} - -.ds-manual-row { - align-items: center; - display: flex; - gap: 0.35rem; - margin-top: 0.15rem; - width: 100%; -} - -.ds-manual-input { - background: var(--sidebar-bg); - border: var(--border-style-main); - border-radius: var(--border-radius-sm); - color: var(--text-color); - flex: 1; - font-family: var(--font-body); - font-size: var(--font-size-xs); - min-width: 0; - padding: 0.22rem 0.35rem; -} - -.ds-manual-input:focus { - border-color: var(--main-fg); - outline: none; -} - -.ds-apply-btn { - background: transparent; - border: var(--border-style-main); - border-radius: var(--border-radius-sm); - color: var(--text-color); - cursor: pointer; - font-size: var(--font-size-xs); - padding: 0.2rem 0.45rem; - transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); - white-space: nowrap; -} - -.ds-apply-btn:hover:not(:disabled) { - background: var(--main-fg); - border-color: var(--main-fg); - color: var(--color-black); -} - -.ds-apply-btn:disabled { - cursor: not-allowed; - opacity: var(--disabled-opacity); -} - .ds-reset-btn { background: transparent; border: var(--border-style-main); diff --git a/frogpilot/system/the_pond/assets/components/tools/device_settings.js b/frogpilot/system/the_pond/assets/components/tools/device_settings.js index a326802fa..24162c8a0 100644 --- a/frogpilot/system/the_pond/assets/components/tools/device_settings.js +++ b/frogpilot/system/the_pond/assets/components/tools/device_settings.js @@ -1,4 +1,4 @@ -import { html, reactive } from "/assets/vendor/arrow-core.js" +import { html, reactive } from "https://esm.sh/@arrow-js/core" const endpointOptionsCache = {} const endpointOptionsInflight = {} @@ -220,18 +220,6 @@ function formatSliderValue(val, stepStr, precisionInt, key) { return Number(v.toFixed(dec)).toString() } -function formatNumericForInput(value, precision) { - const n = Number(value) - if (!Number.isFinite(n)) return "" - return Number(n.toFixed(precision)).toString() -} - -function formatStepValue(step, precision) { - const n = Number(step) - if (!Number.isFinite(n)) return "1" - return Number(n.toFixed(Math.max(0, precision))).toString() -} - function numericBounds(param) { const defaultBounds = { min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0), @@ -424,41 +412,6 @@ function stepNumericParam(param, direction) { updateNumericParam(param, next) } -function applyManualNumericParam(param) { - if (isNumericUpdating(param.key)) return - - const inputEl = document.getElementById(`ds-manual-${param.key}`) - if (!inputEl) return - - const raw = String(inputEl.value ?? "").trim() - if (!raw) { - showParamSnackbar("Enter a value first.", "error") - return - } - - const parsed = Number.parseFloat(raw) - if (!Number.isFinite(parsed)) { - showParamSnackbar("Enter a valid number.", "error") - return - } - - const bounds = numericBounds(param) - const precision = stepPrecision(bounds.step, param.precision) - const snapped = snapNumericToBoundsAndStep(parsed, bounds, precision) - if (snapped === null) { - showParamSnackbar("Value is out of range.", "error") - return - } - - inputEl.value = formatNumericForInput(snapped, precision) - - const current = resolveCurrentNumericValue(param, bounds) - const epsilon = Math.pow(10, -(precision + 2)) - if (Math.abs(snapped - current) <= epsilon) return - - updateNumericParam(param, snapped) -} - async function resetNumericParam(param) { const bounds = numericBounds(param) let defaultValue = resolveDefaultNumericValue(param, bounds) @@ -622,7 +575,6 @@ function renderSettingRow(p) { ? formatSliderValue(defaultNumeric, String(bounds.step), p.precision, p.key) : "N/A" const canReset = !updating && defaultNumeric !== null && Math.abs(defaultNumeric - currentNumeric) > epsilon - const stepLabel = formatStepValue(bounds.step, precision) return html`
    ${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)} - Step: ${stepLabel} per click Default: ${defaultLabel} -
    - - -