import { GxState } from "./state.js" import { GxNotice } from "./notice.js" import { PollTimer, connectionError } from "./polling.js" import { requestJson } from "./startup.js" import { PlotGraph, PlotsFeed, chartGeometry } from "./plots.js" import { validTrainingResults, trainingSummary, readable, detailText, surfaceRows, coordinateText, trackingView, measurementText, validTrackingValues, trackingPlotView, sourceSeconds } from "./flm-training-results.js" export const FlmPage = { name: "FlmPage", components: { GxState, GxNotice, PlotGraph }, 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 }) }, unauthorized: this.unauthorized }) }, mounted() { this.activePage = true this.poller = new PollTimer({ read: () => this.load(), interval: 1000 }) this.visibility = () => { this.now = Date.now() if (document.hidden) { this.request?.abort(); this.feed.stop() } 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 (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() document.removeEventListener("visibilitychange", this.visibility) }, computed: { current() { return this.report?.current }, live() { return !this.error && !!this.current?.fresh && this.current.ageSeconds + Math.max(0, this.now - this.receivedAt) / 1000 <= 2 }, summary() { return trainingSummary(this.report, this.error || (this.current && !this.live ? "stale" : "")) }, currentApplied() { return this.live && this.current?.application?.applied === true }, rows() { return surfaceRows(this.report, this.live) }, tracking() { return trackingView(this.report, this.live) }, trackingValues() { return validTrackingValues(this.current?.trackingValues) ? this.current.trackingValues : null }, trackingPlot() { return trackingPlotView(this.report, this.now - this.receivedAt) }, pairedTrackingChart() { return chartGeometry(this.trackingPlot?.points || [], ['desiredForceAccel', 'measuredForceAccel']) }, pairedErrorChart() { return chartGeometry(this.trackingPlot?.points || [], ['errorForceAccel']) }, readiness() { return this.current?.details?.["Snapshot readiness"] }, details() { return Object.entries(this.current?.details || {}) }, losses() { const d = this.current?.details || {}, baseline = d["Fit baseline loss"], learned = d["Fit candidate loss"] return typeof baseline === "number" && Number.isFinite(baseline) && baseline >= 0 && typeof learned === "number" && Number.isFinite(learned) && learned >= 0 ? { baseline, learned } : null }, lateralChart() { return chartGeometry(this.history, ["desiredLateralAccel", "actualLateralAccel"]) }, termsChart() { return chartGeometry(this.history, ["lateralP", "lateralI", "lateralD", "lateralF"]) }, }, methods: { readable, detailText, coordinateText, measurementText, sourceSeconds, amount(value) { return value === null || value === undefined ? "Not reported" : value.toLocaleString() }, togglePlots() { this.showPlots = !this.showPlots if (this.showPlots && this.mode === "local" && !document.hidden) this.feed.start() else this.feed.stop() }, async load() { this.now = Date.now() if (!this.activePage || this.mode !== "local" || this.request || document.hidden) return const controller = new AbortController() this.request = controller; this.loading = true try { const value = await requestJson("./api/flm/training-results", { request: { signal: controller.signal } }) if (this.request !== controller || !this.activePage) return if (!validTrainingResults(value)) throw new Error("The device returned an unsupported training report.") this.report = value; this.error = ""; this.receivedAt = this.now = Date.now() clearTimeout(this.freshnessTimer) if (value.current?.fresh) this.freshnessTimer = setTimeout(() => { this.now = Date.now() }, Math.max(0, 2000 - value.current.ageSeconds * 1000) + 1) } catch (error) { if (this.request !== controller || !this.activePage) return if (error.status === 401 || ["access_unavailable", "setup_required"].includes(error.code)) this.unauthorized() else this.error = connectionError(error, "Live update unavailable. Reconnecting automatically…") } finally { if (this.request === controller) { this.request = null; this.loading = false } } }, }, template: `
Learned steering corrections, collection progress and current application.
{{ readable(current.reason) }}
Last controller report: {{ current.ageSeconds.toFixed(1) }} seconds old when read.
{{ readable(current.application.reason) }}
{{ readable(tracking.reason) }}
Parameters under evaluation: {{ tracking.changedParameters }}
Comparable episodes: {{ tracking.beforeEpisodes }} before · {{ tracking.futureEpisodes }} after · {{ tracking.requiredEpisodes }} future episodes required.
Steering input checks: {{ readable(tracking.modelStatus) }} · Steering delay: {{ measurementText(tracking.lagMs) }} ms
| Observed measurement | Before trial | Future trial episodes |
|---|---|---|
| {{ row.label }} | {{ measurementText(row.before) }} | {{ measurementText(row.future) }} |
{{ trackingPlot.points.length }} original endpoints selected from {{ trackingPlot.totalEndpoints }} · {{ trackingPlot.age.toFixed(1) }} seconds since the last measured pose · Alignment delay {{ (trackingPlot.lagNs / 1e6).toFixed(1) }} ms.
Earlier episode only; the current steering context is not established by this plot.
Each requested value is paired by the learner with its genuine later measured pose. The horizontal axis uses those pose timestamps; selected endpoints are not resampled or upsampled. Blue is requested, orange is measured.
Latest control / applied output / measured pose: {{ sourceSeconds(trackingPlot.points.at(-1).controlMonoTime) }} / {{ sourceSeconds(trackingPlot.points.at(-1).applyMonoTime) }} / {{ sourceSeconds(trackingPlot.points.at(-1).poseMonoTime) }} monotonic seconds. Episode {{ trackingPlot.episodeId }} · Applied profile {{ trackingPlot.profileSha256 }}.
No delay-paired steering episode plot has been reported yet.
The live desired/observed plots show sampled controller telemetry; they are not actuator-delay aligned.
Before and after use separate completed, comparable driving episodes. Unmeasured values remain “Not measured”. Passing future checks does not itself confirm controller application or saving; currently applied values require fresh controller evidence.
Each value is an original-effect correction coordinate; baseline 0 means no automatic correction. Accepted baseline values were acknowledged by the controller. A pending trial target is an offer, not proof of application. Currently applied values require fresh controller evidence.
Each value is an original-effect correction coordinate. Baseline 0 means no automatic correction. Learned values are the latest evaluated proposal; current values require fresh controller application evidence.
Speed-window coordinates multiply their original baseline by (1 + correction). Deadband and angle-assist coordinates use the controller’s physical normalizer. Raw degrees, torque and manual tune values are not published by this report.
| Parameter | Baseline | {{ trackingValues ? 'Accepted baseline' : 'Learned proposal' }} | Pending trial target | Currently applied | Support |
|---|---|---|---|---|---|
| {{ readable(row.name) }} | {{ coordinateText(row.baseline) }} | {{ coordinateText(trackingValues ? row.accepted : row.learned) }} | {{ coordinateText(row.pending) }} | {{ coordinateText(row.applied) }} | {{ row.pidSupported ? 'Torque / torque-actuated PID' : 'Torque; PID unsupported (no friction stage)' }} |
All 30 parameters have torque stages; 26 have torque-actuated PID stages. The four friction thresholds have no PID friction measurement stage. Support describes controller capability; it does not establish a validated fit. Missing values remain “Not reported”.
Collection begins when the device reports eligible steering examples.
{{ readable(readiness.reason) }}
Selected bundles: {{ amount(readiness.selectedBundles) }} / {{ amount(readiness.requiredBundles) }} required · {{ amount(readiness.wholeBundles) }} whole bundles retained.
Evaluating the collected examples…
{{ readable(current.fit.status) }} · {{ readable(current.fit.reason) }}
Training windows: {{ amount(current.fit.trainWindows) }} · Validation windows: {{ amount(current.fit.heldoutWindows) }}
Collection counts describe retained data. The fitter separately checks physical calibration, complete event support and validation.
No fit comparison has been reported yet.
These are fitter-reported losses for the latest proposal. The live control plots are separate observations and do not measure a before/after learning benefit.
{{ status === 'current' ? 'Live' : status === 'stale' ? 'Last received window — reconnecting' : 'Waiting for steering telemetry' }}
P · I · D · F; absent terms leave gaps.
Blue is desired response; orange is actual. Existing live control telemetry; missing samples leave gaps.
No learning history has been reported yet.