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
-
+
${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``;
+ 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`
-
- `
-}
-
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 `
-
-
-
- `;
- }).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`