Revert "Fix Galaxy"

This reverts commit 4dac64e666.
This commit is contained in:
firestarsdog
2026-03-20 22:06:14 -04:00
parent 4dac64e666
commit 41424d267b
31 changed files with 241 additions and 563 deletions
@@ -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;
@@ -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`
<div class="disk">
<p>${disk.used} used of ${disk.size}</p>
<div class="progress">
<div
class="bar"
style="
border-bottom-right-radius: ${rightRadius};
border-top-right-radius: ${rightRadius};
width: ${100 - used}%;
"
></div>
</div>
</div>
`;
}
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 = `<p class="error">Failed to load data: ${state.error}</p>`;
return;
}
if (state.status !== "ready" || !data) {
shell.innerHTML = "<p>Loading...</p>";
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`
<div class="drivingStat">
<h2>${title}</h2>
<div><p>${formatInt(stats.drives)}</p><p>drives</p></div>
<div><p>${formatInt(stats.distance)}</p><p>${stats.unit || state.unit}</p></div>
<div><p>${formatInt(stats.hours)}</p><p>hours</p></div>
</div>
`;
const diskBlock = (disk = {}) => {
const usedPct = Number.parseFloat(disk.usedPercentage) || 0;
const rightRadius = usedPct >= 100 ? "0" : "var(--border-radius-md)";
return `
<div class="disk">
<p>${disk.used || "0 GB"} used of ${disk.size || "0 GB"}</p>
<div class="progress">
<div
class="bar"
style="
border-bottom-right-radius: ${rightRadius};
border-top-right-radius: ${rightRadius};
width: ${Math.max(0, 100 - usedPct)}%;
"
></div>
</div>
</div>
`;
};
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]) => `<p><strong>${label}:</strong> ${value ?? "Unknown"}</p>`)
.join("");
const diskMarkup = diskError.length
? `<p>${diskError.join("<br>")}</p>`
: (diskUsage.length ? diskUsage.map(diskBlock).join("") : diskBlock({}));
shell.innerHTML = `
<div>
<h1>Galaxy</h1>
<div class="drivingStats">
${statBlock("All Time", driveStats.all)}
${statBlock("Past Week", driveStats.week)}
${statBlock("FrogPilot", driveStats.frogpilot)}
</div>
<h2>Disk Usage</h2>
<div class="diskUsage">
${diskMarkup}
</div>
<h2>Software Info</h2>
<div class="softwareInfo">
<div class="softwareGrid">${softwareMarkup}</div>
</div>
<div><p>${format(stats.drives)}</p><p>drives</p></div>
<div><p>${format(stats.distance)}</p><p>${stats.unit ?? defaultUnit}</p></div>
<div><p>${format(stats.hours)}</p><p>hours</p></div>
</div>
`;
}
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`<p><strong>${label}:</strong> ${value ?? "Unknown"}</p>`
);
}
function renderDiskUsageSection({ diskError, diskUsage }) {
if (diskError) {
return html`<p>${diskError.join("<br>")}</p>`;
}
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`<div id="home_shell"><p>Loading...</p></div>`;
return html`
<div>
${() => {
if (state.isLoading) {
return html`<p>Loading...</p>`;
}
if (state.error) {
return html`<p class="error">Failed to load data: ${state.error}</p>`;
}
if (state.data) {
const { driveStats, softwareInfo } = state.data;
return html`
<h1>Galaxy</h1>
<div class="drivingStats">
${DriveStat("All Time", driveStats?.all, state.unit)}
${DriveStat("Past Week", driveStats?.week, state.unit)}
${DriveStat("FrogPilot", driveStats?.frogpilot, state.unit)}
</div>
<h2>Disk Usage</h2>
<div class="diskUsage">
${renderDiskUsageSection(state.data)}
</div>
<h2>Software Info</h2>
<div class="softwareInfo">
<div class="softwareGrid">${renderSoftwareInfo(softwareInfo)}</div>
</div>
`;
}
return html`<p>No data available.</p>`;
}}
</div>
`;
}
@@ -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,
@@ -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,
@@ -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() {
@@ -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() {
</div>
`;
}}
${() => state.truncated ? html`
<p class="screen-recordings-message">Showing first ${MAX_RENDERED_ROUTES} routes to keep the UI responsive.</p>
` : ""}
${() => {
if (state.routes.length > 0) {
return html`
@@ -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";
@@ -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`
<div>
<h1>Galaxy</h1>
<p>Safe mode is active while UI rendering is being repaired.</p>
<p>
<a href="/galaxy">Galaxy Pairing</a> |
<a href="/device_settings">Toggles</a> |
<a href="/manage_updates">Software</a>
</p>
</div>
`
}
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`<h1>Not Found</h1>`
}
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 })
}}
</div>
`
@@ -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 <body> 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")
}
@@ -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"
@@ -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 `
<li class="${active}">
<a class="menu-item-link" href="${link.link}">
<i class="bi ${link.icon}"></i>
<span>${upperFirst(link.name)}</span>
</a>
</li>
`;
}).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 `
<div class="sidebar_widget">
<ul class="menu_section">
<li>
<span class="section-title">${upperFirst(section)}</span>
<ul id="${section}">
${linksMarkup}
</ul>
</li>
</ul>
</div>
`;
}
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`
<div id="sidebarUnderlay" class="hidden"></div>
<div id="sidebar" class="sidebar">
<div>
@@ -124,15 +72,49 @@ function renderSidebarIntoShell(currentPath) {
</div>
</div>
<hr />
${sectionsMarkup}
${() => Object.entries(MenuItems).map(([section, links]) => html`
<div class="sidebar_widget">
<ul class="menu_section">
<li>
<span class="section-title">${upperFirst(section)}</span>
<ul id="${section}">
${links.map(link => {
const isActive = state.activeRoute === link.name;
const classList = [isActive && "active"].filter(Boolean).join(" ");
const content = html`
<div class="menu-item-link">
<i class="bi ${link.icon}"></i>
<span>${upperFirst(link.name)}</span>
</div>
`;
return html`
<li class="${classList}">
${Link(link.link, content, () => navigate(link))}
</li>
`;
})}
</ul>
</li>
</ul>
</div>
`)}
</div>
</div>
`;
bindSidebarHandlers();
</div>`;
}
export function Sidebar(currentPath) {
setTimeout(() => renderSidebarIntoShell(currentPath), 0);
return html`<div id="sidebar_shell"></div>`;
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);
@@ -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() {
@@ -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);
@@ -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`
<div class="ds-stepper">
<button
@@ -631,28 +583,7 @@ function renderSettingRow(p) {
@click="${() => stepNumericParam(p, -1)}">-</button>
<div class="ds-stepper-meta">
<span>${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)}</span>
<span class="ds-step-value">Step: ${stepLabel} per click</span>
<span class="ds-default-value">Default: ${defaultLabel}</span>
<div class="ds-manual-row">
<input
type="number"
class="ds-manual-input"
id="ds-manual-${p.key}"
min="${bounds.min}"
max="${bounds.max}"
step="${bounds.step}"
?disabled="${updating}"
value="${() => formatNumericForInput(resolveCurrentNumericValue(p, numericBounds(p)), precision)}"
@keydown="${(e) => {
if (e.key !== "Enter") return
e.preventDefault()
applyManualNumericParam(p)
}}" />
<button
class="ds-apply-btn"
?disabled="${updating}"
@click="${() => applyManualNumericParam(p)}">Apply</button>
</div>
<button
class="ds-reset-btn"
disabled="${() => !canReset || false}"
@@ -1,4 +1,4 @@
import { html } from "/assets/vendor/arrow-core.js"
import { html } from "https://esm.sh/@arrow-js/core"
export function DoorControl () {
async function lockDoors () {
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js";
import { html, reactive } from "https://esm.sh/@arrow-js/core";
import { formatSecondsToHuman, parseErrorLogToDate } from "/assets/js/utils.js";
import { Modal } from "/assets/components/modal.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"
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
loading: true,
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js";
import { html, reactive } from "https://esm.sh/@arrow-js/core";
const state = reactive({
loading: true,
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
loading: true,
@@ -1,4 +1,4 @@
import { html } from "/assets/vendor/arrow-core.js"
import { html } from "https://esm.sh/@arrow-js/core"
export function SpeedLimits() {
function handleDownload() {
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
loading: true,
@@ -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";
const defaultColors = {
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { formatSecondsToHuman, isGalaxyTunnel } from "/assets/js/utils.js"
import { Modal } from "/assets/components/modal.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"
import { TailscaleControl } from "/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"
const state = reactive({
loading: true,
@@ -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"
const state = reactive({
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
loading: true,
@@ -1,4 +1,4 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { html, reactive } from "https://esm.sh/@arrow-js/core"
import { DoorControl } from "/assets/components/tools/doors.js"
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -14
View File
@@ -41,20 +41,7 @@
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<script type="module">
import("/assets/components/router.js").catch((err) => {
console.error("[the_pond] bootstrap failed", err);
const target = document.getElementById("app") || document.body;
const pre = document.createElement("pre");
pre.style.whiteSpace = "pre-wrap";
pre.style.color = "#ff8080";
pre.style.background = "#200";
pre.style.padding = "12px";
pre.style.margin = "12px";
pre.textContent = `[the_pond] failed to load router:\n${err?.stack || err}`;
target.appendChild(pre);
});
</script>
<script type="module" src="/assets/components/router.js"></script>
<script src="/assets/js/snackbar.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">