Restore FLM Results Navigation

This commit is contained in:
firestar5683
2026-10-19 22:30:33 -05:00
parent 72de373447
commit 393e06209e
8 changed files with 44 additions and 25 deletions
+27 -3
View File
@@ -58,13 +58,27 @@ assert.doesNotMatch(FlmPage.template, /v-html|Analyze selected|GM Trial|Choose F
assert.match(FlmPage.template, /Currently applied/)
assert.match(FlmPage.template, /PID unsupported/)
const app = readFileSync(new URL("../web/js/app.js", import.meta.url), "utf8")
assert(app.includes("['/tuning', '/tuning/plots'].includes(route.path)"))
assert(app.includes("['/tuning', '/tuning/plots', '/tuning/flm'].includes(route.path)"))
const analysis = app.match(/<div v-else-if="\['\/tuning', '\/tuning\/plots', '\/tuning\/flm'\]\.includes\(route.path\)"[\s\S]*?<\/div>/)?.[0]
assert(analysis, 'Existing analysis route and old bookmarks share one page')
assert.match(analysis, /<PlotsPage /)
assert.match(analysis, /<FlmPage embedded /)
assert.doesNotMatch(app, /<FlmPage v-else-if/)
compile(analysis.replace("v-else-if", "v-if"), { onError: error => { throw error }, decodeEntities: value => value.replaceAll("&amp;", "&") })
assert.doesNotMatch(app, /Recorded-drive analysis/)
const settings = readFileSync(new URL("../web/js/settings.js", import.meta.url), "utf8")
assert.doesNotMatch(settings, /FLMTrainingResults|View FLM training results/)
const catalog = JSON.parse(readFileSync(new URL("../web/data/catalog.json", import.meta.url), "utf8"))
assert(catalog.tools.some(tool => tool.path === "/tuning/plots"))
assert(catalog.tools.some(tool => tool.path === "/tuning/flm"))
assert(!catalog.tools.some(tool => tool.path === "/tuning/flm"))
assert.equal(catalog.tools.filter(tool => tool.path.startsWith('/tuning')).length, 1)
const plots = readFileSync(new URL('../web/js/plots.js', import.meta.url), 'utf8')
assert.doesNotMatch(plots, /href="#\/tuning\/flm"/)
const driving = readFileSync(new URL('../web/js/driving.js', import.meta.url), 'utf8')
assert.match(driving, /go\('\/tuning\/plots'\)/)
assert.doesNotMatch(driving, /\/tuning\/flm/)
assert.match(FlmPage.template, /<section v-if="!embedded"[^>]*><div class="gx-actions"><h3>Live steering response/)
assert.doesNotMatch(FlmPage.template, /href="#\/tuning\/plots"/)
const route = readFileSync(new URL("../web/js/route-workspace.js", import.meta.url), "utf8")
assert.doesNotMatch(route, /Analyze driving logs/)
@@ -77,7 +91,17 @@ function instance() {
}
const originalFetch = globalThis.fetch, originalDocument = globalThis.document
try {
globalThis.document = { hidden: false, removeEventListener() {} }
globalThis.document = { hidden: false, addEventListener() {}, removeEventListener() {} }
for (const embedded of [true, false]) {
const page = { ...FlmPage.data(), embedded, mode: 'local', unauthorized() {}, load() {} }
let plotStarts = 0
FlmPage.created.call(page)
page.feed.start = () => { plotStarts++ }
page.feed.stop = () => {}
FlmPage.mounted.call(page)
assert.equal(plotStarts, embedded ? 0 : 1, 'Embedded results reuse the existing live plots')
FlmPage.beforeUnmount.call(page)
}
const calls = []
globalThis.fetch = async (url, options) => { calls.push({ url, options }); return { ok: true, json: async () => value } }
const vm = instance(); await vm.load()
@@ -19,7 +19,7 @@ class PreviewContractTest(unittest.TestCase):
self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "partial-preview"], ["/logs"])
self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "local-only"],
["/bluetooth", "/cameras", "/device-preferences", "/galaxy", "/manage_models", "/model_laboratory",
"/navigation", "/system", "/theme_maker", "/tuning/plots", "/tuning/flm", "/vehicle", "/android-auto"])
"/navigation", "/system", "/theme_maker", "/tuning/plots", "/vehicle", "/android-auto"])
self.assertTrue(all(tool["availability"] in {"unavailable", "partial-preview", "local-only"} for tool in catalog["tools"]))
self.assertTrue(all(tool["name"] and tool["description"] and tool["icon"] for tool in catalog["tools"]))
@@ -75,14 +75,7 @@
"name": "Plots & Analysis",
"path": "/tuning/plots",
"icon": "bi-sign-turn-right",
"description": "Live steering and acceleration plots",
"availability": "local-only"
},
{
"name": "Auto Learn Results",
"path": "/tuning/flm",
"icon": "bi-sliders",
"description": "StarPilot learned steering corrections and fitting progress",
"description": "Live control plots and StarPilot Auto Learn results",
"availability": "local-only"
},
{
@@ -96,7 +89,7 @@
"name": "Android Auto",
"path": "/android-auto",
"icon": "bi-phone",
"description": "Show StarPilot on your car’s screen",
"description": "Show StarPilot on your car\u2019s screen",
"availability": "local-only",
"visibility": "authenticated"
}
+4 -2
View File
@@ -250,8 +250,10 @@ createApp({
<SettingsPage @nested="ensureBackEntry" ref="activeSettings" v-else-if="drivingSettingsPage" :key="drivingSettingsPage" :mode="state.monitorMode" :unauthorized="sessionExpired" :initial-page="drivingSettingsPage" :title="'Driving settings · ' + drivingSettingsPage.replaceAll('_', ' ')" :return-to="returnToDriving" />
<SettingsPage @nested="ensureBackEntry" ref="activeSettings" v-else-if="route.path === '/settings'" :key="state.searchPage || 'hub'" :mode="state.monitorMode" :unauthorized="sessionExpired"
:initial-page="state.searchPage || 'hub'" :return-to="state.searchPage ? returnFromSearch : null" />
<PlotsPage v-else-if="['/tuning', '/tuning/plots'].includes(route.path)" :mode="state.monitorMode" :unauthorized="sessionExpired" />
<FlmPage v-else-if="route.path === '/tuning/flm'" :mode="state.monitorMode" :unauthorized="sessionExpired" :go="go" />
<div v-else-if="['/tuning', '/tuning/plots', '/tuning/flm'].includes(route.path)" class="gx-stack">
<PlotsPage :mode="state.monitorMode" :unauthorized="sessionExpired" />
<FlmPage embedded :mode="state.monitorMode" :unauthorized="sessionExpired" />
</div>
<GxState v-else :title="pageName">This capability is unavailable in this build. No operation was attempted.</GxState>
</div></Transition></div></main>
<BottomNav :path="route.path" :local-access="authState.localAccess" @navigate="go" />
+1 -1
View File
@@ -51,7 +51,7 @@ export const DrivingPage = {
</section>
<div class="gx-driving__extras gx-actions"><button type="button" class="gx-btn gx-btn--tonal" @click="go('/driving/longitudinal-curves')">Edit longitudinal curves</button>
<button type="button" class="gx-btn gx-btn--tonal" @click="go('/tuning/plots')">Live plots</button>
<button type="button" class="gx-btn gx-btn--tonal" @click="go('/tuning/flm')">Auto Learn results</button></div>
<button type="button" class="gx-btn gx-btn--tonal" @click="go('/tuning/plots')">Auto Learn results</button></div>
</template>
</section>`,
}
+7 -6
View File
@@ -8,10 +8,11 @@ import { validTrainingResults, trainingSummary, readable, detailText, surfaceRow
export const FlmPage = {
name: "FlmPage",
components: { GxState, GxNotice, PlotGraph },
props: { mode: { type: String, required: true }, unauthorized: { type: Function, required: true } },
props: { mode: { type: String, required: true }, unauthorized: { type: Function, required: true }, embedded: { type: Boolean, default: false } },
data: () => ({ report: null, error: "", loading: false, request: null, receivedAt: 0, now: 0,
status: "idle", data: null, history: [], plotError: "", showPlots: true }),
created() {
if (this.embedded) this.showPlots = false
this.feed = new PlotsFeed({ publish: update => {
const { error, ...values } = update
Object.assign(this.$data, values, { plotError: error })
@@ -26,7 +27,7 @@ export const FlmPage = {
else { this.load(); if (this.showPlots && this.mode === "local") this.feed.start() }
}
document.addEventListener("visibilitychange", this.visibility)
if (this.mode === "local") { this.poller.start(); this.load(); if (!document.hidden) this.feed.start() }
if (this.mode === "local") { this.poller.start(); this.load(); if (this.showPlots && !document.hidden) this.feed.start() }
},
beforeUnmount() {
this.activePage = false; clearTimeout(this.freshnessTimer); this.poller?.stop(); this.request?.abort(); this.request = null; this.feed.stop()
@@ -79,9 +80,9 @@ export const FlmPage = {
},
},
template: `<div class="gx-view gx-flm gx-stack">
<header class="gx-settings__header gx-page-header"><div><h2>StarPilot Auto Learn</h2>
<header class="gx-settings__header gx-page-header"><div><h3 v-if="embedded">StarPilot Auto Learn</h3><h2 v-else>StarPilot Auto Learn</h2>
<p>Learned steering corrections, collection progress and current application.</p></div>
<a class="gx-btn gx-btn--tonal" href="#/tuning/plots">Live plots</a></header>
</header>
<GxState v-if="mode !== 'local'">Auto Learn results require authenticated local Galaxy access.</GxState>
<template v-else>
<section class="gx-card gx-flm__panel"><h3 role="status">{{ summary }}</h3>
@@ -118,10 +119,10 @@ export const FlmPage = {
<section class="gx-card gx-flm__panel"><h3>Latest fit comparison</h3>
<dl v-if="losses"><dt>Baseline loss</dt><dd>{{ losses.baseline.toPrecision(5) }}</dd><dt>Learned proposal loss</dt><dd>{{ losses.learned.toPrecision(5) }}</dd></dl>
<p v-else>No fit comparison has been reported yet.</p>
<p class="gx-note">These are fitter-reported losses for the latest proposal. Live tracking below is a separate observation and does not measure a before/after learning benefit.</p>
<p class="gx-note">These are fitter-reported losses for the latest proposal. The live control plots are a separate observation and does not measure a before/after learning benefit.</p>
</section>
</div>
<section class="gx-card gx-flm__panel"><div class="gx-actions"><h3>Live steering response</h3>
<section v-if="!embedded" class="gx-card gx-flm__panel"><div class="gx-actions"><h3>Live steering response</h3>
<button type="button" class="gx-btn gx-btn--tonal" :aria-expanded="showPlots" @click="togglePlots">{{ showPlots ? 'Pause live plots' : 'Show live plots' }}</button></div>
<template v-if="showPlots"><p role="status">{{ status === 'current' ? 'Live' : status === 'stale' ? 'Last received window — reconnecting' : 'Waiting for steering telemetry' }}</p>
<p v-if="plotError" class="gx-note">{{ plotError }}</p>
+1 -2
View File
@@ -223,8 +223,7 @@ export const PlotsPage = {
},
template: `
<div class="gx-view gx-plots">
<header class="gx-settings__header gx-page-header"><div><h2>Plots</h2><p>Live local control observations. Graphs and match scores are diagnostic, not driving-control checks.</p></div>
<div class="gx-actions"><a class="gx-btn gx-btn--tonal" href="#/tuning/flm">Auto Learn results</a></div></header>
<header class="gx-settings__header gx-page-header"><div><h2>Plots &amp; Analysis</h2><p>Live local control observations. Graphs and match scores are diagnostic, not driving-control checks.</p></div></header>
<GxState v-if="mode !== 'local'">Live plots require the authenticated local Galaxy service.</GxState>
<template v-else>
<div class="gx-plots__toolbar"><GxIconButton :label="paused ? 'Resume live plots' : 'Pause live plots'" :icon="paused ? 'bi-play-fill' : 'bi-pause-fill'" @click="togglePause" />
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "7af93a9b9dfe7c8fbba91f94cd2567216d327acf626051bc61d23656c1a3dcb3", "schema_version": 1}
{"protected_inventory_sha256": "64d12e6db0a074393432aa9f80dc6c251227f216e1478f5075e077fd1ce2d6ee", "schema_version": 1}