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: `

StarPilot Auto Learn

StarPilot Auto Learn

Learned steering corrections, collection progress and current application.

Auto Learn results require authenticated local Galaxy access.
`, }