Files
StarPilot/openpilot/starpilot/galaxy/web/js/toggle-backup.js
T
Prabhaav Pillai d921cf6559 Refactor Galaxy tools into a searchable nested catalog
Render tool menus from the catalog and update navigation, guides, and tests
2026-10-22 22:03:31 -04:00

87 lines
5.6 KiB
JavaScript

import { GxDialog } from "./dialog.js"
import { GxNotice } from "./notice.js"
import { requestJson } from "./startup.js"
import { decodeLayoutBackup, MAX_LAYOUT_BACKUP_BYTES } from "./layout-backup.js"
export const MAX_TOGGLE_BACKUP_BYTES = 256 * 1024
export const ToggleBackup = {
name: "ToggleBackup",
components: { GxDialog, GxNotice },
props: { unauthorized: { type: Function, required: true } },
data: () => ({ busy: false, draft: null, legacy: false, error: "", notice: "" }),
methods: {
async request(path, payload) {
try {
return await requestJson(path, { timeout: 30000, request: payload === undefined ? {} : {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
} })
} catch (error) { if (error.status === 401) this.unauthorized(); throw error }
},
async exportBackup() {
if (this.busy) return
this.busy = true; this.error = this.notice = ""
try {
const backup = await this.request("./api/settings/backup")
const url = URL.createObjectURL(new Blob([JSON.stringify(backup, null, 2)], { type: "application/json" }))
const link = document.createElement("a")
try {
link.href = url; link.download = "toggle-backup.json"
document.body.append(link); link.click(); link.remove()
} finally { link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000) }
this.notice = `${backup.settings.length} ${backup.settings.length === 1 ? "toggle" : "toggles"} backed up${backup.layout ? ", including layout and colors" : ""}.`
} catch (error) { this.error = error.message || "Could not create toggle backup." }
finally { this.busy = false }
},
async chooseFile(event) {
const file = event.target.files?.[0]
event.target.value = ""
if (!file || this.busy) return
this.error = this.notice = ""
try {
if (file.size > MAX_TOGGLE_BACKUP_BYTES) throw new Error("Toggle backup is too large.")
const text = await file.text(), value = JSON.parse(text)
if (!value || typeof value !== "object") throw new Error("Choose a Galaxy toggle backup.")
this.legacy = value.format === "starpilot-visual-layout"
if (this.legacy) {
if (file.size > MAX_LAYOUT_BACKUP_BYTES) throw new Error("Layout backup is too large.")
this.draft = await decodeLayoutBackup(text)
} else {
if (value.format !== "galaxy-toggles" || value.version !== 1 || !Array.isArray(value.settings)) throw new Error("Choose a Galaxy toggle backup.")
this.draft = value
}
} catch (error) { this.draft = null; this.error = error.message || "Could not read toggle backup." }
},
async restoreBackup() {
if (this.busy || !this.draft) return
const draft = this.draft, legacy = this.legacy
this.draft = null; this.busy = true; this.error = this.notice = ""
try {
if (legacy) {
const current = await this.request("./api/ui/layout")
if (current.editable !== true) throw new Error("Park the vehicle before restoring the layout.")
const result = await this.request("./api/ui/layout", { revision: current.revision, document: draft })
if (!result.valid) throw new Error("Layout restore could not be verified.")
this.notice = "Layout and colors restored."
} else {
const result = await this.request("./api/settings/restore", draft)
if (!result.complete) throw new Error(result.error || "Restore could not be completed. Review Toggles.")
this.notice = `${result.restored} ${result.restored === 1 ? "toggle" : "toggles"} restored${result.unchanged ? ` · ${result.unchanged} already matched` : ""}${result.skipped.length ? ` · ${result.skipped.length} unavailable settings skipped` : ""}${draft.layout ? ". Layout and colors restored" : ""}.`
}
} catch (error) { this.error = error.message || "Could not restore toggle backup." }
finally { this.busy = false }
},
},
template: `<section class="gx-card gx-panel gx-stack" :inert="busy" :aria-busy="busy || undefined">
<div class="gx-section__header"><i class="bi bi-cloud-arrow-down" aria-hidden="true"></i><span class="gx-section__title">Toggle backup</span></div><p class="gx-note">Save your editable toggles, onroad layout and colors. Restore compatible settings while parked.</p>
<div class="gx-actions"><button type="button" class="gx-btn gx-btn--tonal" @click="exportBackup">Back up toggles</button>
<button type="button" class="gx-btn gx-btn--tonal" @click="$refs.file.click()">Restore toggles</button>
<input ref="file" type="file" accept=".json,application/json" class="gx-sr-only" tabindex="-1" aria-label="Choose toggle backup" @change="chooseFile"></div>
<p v-if="busy" class="gx-note" role="status"><span class="gx-spinner" aria-hidden="true"></span> Working…</p>
<GxNotice v-if="notice" tone="success">{{ notice }}</GxNotice><GxNotice v-if="error" tone="danger">{{ error }}</GxNotice>
<GxDialog v-if="draft" :inert="busy" labelledby="gx-toggle-restore-title" @close="draft=null"><h3 id="gx-toggle-restore-title">Restore {{ legacy ? 'layout' : 'toggles' }}?</h3>
<p>{{ legacy ? 'Replace your saved layout and colors?' : 'Apply the compatible saved toggles, layout and colors from this backup? Unavailable settings will be skipped.' }} Keep the vehicle parked.</p>
<div class="gx-actions"><button type="button" class="gx-btn gx-btn--tonal" @click="draft=null">Cancel</button><button type="button" class="gx-btn" @click="restoreBackup">Restore</button></div>
</GxDialog>
</section>`,
}