App / Version

This commit is contained in:
firestar5683
2026-06-28 12:44:58 -05:00
parent afe15a9a80
commit bdc1532258
17 changed files with 268 additions and 34 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAl
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.selfdrive.ui.lib.starpilot_version import starpilot_display_description
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr, trn
@@ -228,5 +229,5 @@ class HomeLayout(Widget):
def _get_version_text(self) -> str:
brand = "openpilot"
description = self.params.get("UpdaterCurrentDescription")
description = starpilot_display_description(self.params.get("UpdaterCurrentDescription"))
return f"{brand} {description}" if description else brand
+4 -3
View File
@@ -3,6 +3,7 @@ import time
import datetime
from pathlib import Path
from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.lib.starpilot_version import starpilot_display_description
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, trn
@@ -54,7 +55,7 @@ class SoftwareLayout(Widget):
super().__init__()
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
self._version_item = text_item(lambda: tr("Current Version"), starpilot_display_description(ui_state.params.get("UpdaterCurrentDescription")))
self._auto_updates_toggle = toggle_item(
lambda: tr("Automatically Install Updates"),
lambda: tr("Automatically install updates when parked with an active internet connection."),
@@ -103,7 +104,7 @@ class SoftwareLayout(Widget):
self._onroad_label.set_visible(ui_state.is_onroad())
# Update current version and release notes
current_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
current_desc = starpilot_display_description(ui_state.params.get("UpdaterCurrentDescription"))
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
self._version_item.action_item.set_text(current_desc)
self._version_item.set_description(current_release_notes)
@@ -154,7 +155,7 @@ class SoftwareLayout(Widget):
# Update install button
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
if update_available:
new_desc = ui_state.params.get("UpdaterNewDescription") or ""
new_desc = starpilot_display_description(ui_state.params.get("UpdaterNewDescription"))
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
self._install_btn.action_item.set_text(tr("INSTALL"))
self._install_btn.action_item.set_value(new_desc)
+13
View File
@@ -0,0 +1,13 @@
STARPILOT_DISPLAY_VERSION = "7.26"
def starpilot_display_description(description: str | None) -> str:
if not description:
return ""
parts = [part.strip() for part in description.split(" / ")]
if not parts:
return STARPILOT_DISPLAY_VERSION
parts[0] = STARPILOT_DISPLAY_VERSION
return " / ".join(parts)
+3 -3
View File
@@ -10,6 +10,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.lib.starpilot_version import STARPILOT_DISPLAY_VERSION
from openpilot.selfdrive.ui.ui_state import ui_state
HEAD_BUTTON_FONT_SIZE = 40
@@ -166,11 +167,10 @@ class MiciHomeLayout(Widget):
self._did_long_press = False
def _get_version_text(self) -> tuple[str, str, str, str] | None:
version = ui_state.params.get("Version")
branch = ui_state.params.get("GitBranch")
commit = ui_state.params.get("GitCommit")
if not all((version, branch, commit)):
if not all((branch, commit)):
return None
commit_date_raw = ui_state.params.get("GitCommitDate")
@@ -181,7 +181,7 @@ class MiciHomeLayout(Widget):
except (ValueError, IndexError, TypeError, AttributeError):
date_str = ""
return version, branch, commit[:7], date_str
return STARPILOT_DISPLAY_VERSION, branch, commit[:7], date_str
def _render(self, _):
# TODO: why is there extra space here to get it to be flush?
+3 -1
View File
@@ -5,6 +5,7 @@ from dataclasses import dataclass
from enum import IntEnum
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
from openpilot.selfdrive.ui.lib.starpilot_version import STARPILOT_DISPLAY_VERSION
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -252,7 +253,8 @@ class MiciOffroadAlerts(Scroller):
# format: "version / branch / commit / date"
parts = new_desc.split(" / ")
if len(parts) > 3:
version, date = parts[0], parts[3]
date = parts[3]
version = STARPILOT_DISPLAY_VERSION
version_string = f"\nopenpilot {version}, {date}\n"
update_alert_data.text = f"Update available {version_string}. Click to update. Read the release notes at blog.comma.ai."
@@ -6,6 +6,7 @@ from enum import IntEnum
import pyray as rl
from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.lib.starpilot_version import STARPILOT_DISPLAY_VERSION
from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
@@ -71,11 +72,11 @@ class SoftwareInfoLayoutMici(Widget):
def _update_state(self):
desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "")
if desc is not None:
version, branch, commit, date = desc
self._version_text_label.set_text(f"{version} ({date})")
_, branch, commit, date = desc
self._version_text_label.set_text(f"{STARPILOT_DISPLAY_VERSION} ({date})")
self._branch_text_label.set_text(f"{branch} ({commit})")
else:
self._version_text_label.set_text(ui_state.params.get("Version") or "N/A")
self._version_text_label.set_text(STARPILOT_DISPLAY_VERSION if ui_state.params.get("Version") else "N/A")
self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A")
def _render(self, _):
@@ -201,7 +202,7 @@ class InstallUpdateButton(BigButton):
super()._update_state()
desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "")
value = f"{desc[0]} ({desc[1]})" if desc is not None else ""
value = f"{STARPILOT_DISPLAY_VERSION} ({desc[1]})" if desc is not None else ""
if self.get_value() != value:
self.set_value(value)
+2 -6
View File
@@ -337,12 +337,8 @@ void OffroadHome::refresh() {
date->setVisible(util::system_time_valid() && !simple_mode);
if (simple_mode) {
version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription")));
version->setText(getBrand() + " " + formatStarPilotDisplayVersionDescription(QString::fromStdString(params.get("UpdaterCurrentDescription"))));
} else {
QString versionText = getVersion().left(14).trimmed();
if (!versionText.startsWith("v", Qt::CaseInsensitive)) {
versionText.prepend("v");
}
version->setText(getBrand() + " - " + versionText + " - " + cleanModelName(starpilot_toggles.value("model_name").toString()));
version->setText(getBrand() + " - " + getStarPilotDisplayVersion() + " - " + cleanModelName(starpilot_toggles.value("model_name").toString()));
}
}
+2 -2
View File
@@ -191,11 +191,11 @@ void SoftwarePanel::updateLabels() {
targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch")));
// current + new versions
versionLbl->setText(QString::fromStdString(params.get("UpdaterCurrentDescription")));
versionLbl->setText(formatStarPilotDisplayVersionDescription(QString::fromStdString(params.get("UpdaterCurrentDescription"))));
versionLbl->setDescription(QString::fromStdString(params.get("UpdaterCurrentReleaseNotes")));
installBtn->setVisible((!is_onroad || parked) && params.getBool("UpdateAvailable"));
installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription")));
installBtn->setValue(formatStarPilotDisplayVersionDescription(QString::fromStdString(params.get("UpdaterNewDescription"))));
installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes")));
update();
+19
View File
@@ -14,6 +14,7 @@
#include <QLayoutItem>
#include <QStyleOption>
#include <QPainterPath>
#include <QStringList>
#include <QTextStream>
#include <QtXml/QDomDocument>
@@ -30,6 +31,24 @@ QString getBrand() {
return QObject::tr("StarPilot");
}
QString getStarPilotDisplayVersion() {
return "7.26";
}
QString formatStarPilotDisplayVersionDescription(const QString &description) {
if (description.isEmpty()) {
return "";
}
QStringList parts = description.split(" / ");
if (parts.isEmpty()) {
return getStarPilotDisplayVersion();
}
parts[0] = getStarPilotDisplayVersion();
return parts.join(" / ");
}
QString getUserAgent() {
return "openpilot-" + getVersion();
}
+2
View File
@@ -15,6 +15,8 @@
QString getVersion();
QString getBrand();
QString getStarPilotDisplayVersion();
QString formatStarPilotDisplayVersionDescription(const QString &description);
QString getUserAgent();
std::optional<QString> getDongleId();
QMap<QString, QString> getSupportedLanguages();
@@ -2,6 +2,7 @@
all: unset;
background-color: var(--main-fg);
border-radius: var(--border-radius-sm);
color: var(--text-on-primary);
cursor: pointer;
padding: var(--padding-xs) var(--padding-xs);
transition: background-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
@@ -41,6 +42,37 @@
transform: var(--hover-scale-sm);
}
.navkeys-app-actions {
display: flex;
justify-content: flex-end;
margin-bottom: var(--margin-base);
}
.navkeys-app-container {
flex-basis: min(100%, calc((var(--width-lg) * 2) + var(--gap-lg)));
max-width: calc((var(--width-lg) * 2) + var(--gap-lg));
}
.navkeys-copy-btn,
.navkeys-link-btn {
align-items: center;
display: inline-flex;
gap: var(--gap-xs);
justify-content: center;
white-space: nowrap;
}
.navkeys-copy-btn {
min-width: 5.5rem;
}
.navkeys-icon-btn {
align-items: center;
display: inline-flex;
justify-content: center;
min-width: 2.25rem;
}
.navkeys-error {
color: var(--danger-bg);
font-size: var(--font-size-sm);
@@ -72,6 +104,7 @@
cursor: text;
flex: 1;
font-size: var(--font-size-base);
min-width: 0;
padding: var(--padding-xs) var(--padding-sm);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
}
@@ -87,6 +120,11 @@
color: var(--text-muted);
}
.navkeys-token-input {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
}
.navkeys-label {
display: block;
font-size: var(--font-size-sm);
@@ -94,6 +132,10 @@
margin-bottom: var(--margin-xs);
}
.navkeys-link-btn {
text-decoration: none;
}
.navkeys-message {
color: var(--success-bg);
font-size: var(--font-size-sm);
@@ -1,5 +1,7 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { Modal } from "/assets/components/modal.js";
const DEFAULT_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.embaucha.galaxynav&hl=en-US&ah=9FldHJ99kxL8oNbSlO5F4sQqwC4"
export function NavKeys() {
const state = reactive({
@@ -20,6 +22,12 @@ export function NavKeys() {
publicKey: "", secretKey: "",
editPublic: false, editSecret: false,
savedPublic: false, savedSecret: false,
galaxyCookieName: "galaxy_session",
galaxySessionToken: "",
galaxyAppUrl: DEFAULT_PLAY_STORE_URL,
galaxyPaired: false,
galaxySessionVisible: false,
showDeleteModal: false,
keyToDelete: null,
@@ -60,6 +68,34 @@ export function NavKeys() {
req: async (url, opts) => {
const response = await fetch(url, opts)
return { ok: response.ok, data: await response.json().catch(() => ({})) }
},
copyText: async (text) => {
if (!text) {
throw new Error("Nothing to copy")
}
if (navigator.clipboard?.writeText && window.isSecureContext) {
await navigator.clipboard.writeText(text)
return
}
const textarea = document.createElement("textarea")
textarea.value = text
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.left = "-9999px"
textarea.style.opacity = "0"
document.body.appendChild(textarea)
textarea.select()
try {
if (!document.execCommand("copy")) {
throw new Error("Copy command failed")
}
} finally {
textarea.remove()
}
}
}
@@ -96,6 +132,7 @@ export function NavKeys() {
const api = {
path: {
galaxy: "/api/galaxy/session",
key: "/api/navigation_key",
nav: "/api/navigation"
},
@@ -103,7 +140,9 @@ export function NavKeys() {
load: async () => {
const { ok, data } = await util.req(api.path.nav)
if (!ok) {
return showMessage("error", "Failed to load keys...", "")
showMessage("error", "Failed to load keys...", "")
await api.loadGalaxySession()
return
}
state.amap1Key = data.amap1Key ?? ""
@@ -119,6 +158,29 @@ export function NavKeys() {
state.initialMapboxComplete = state.savedPublic && state.savedSecret
bumpImageVersion()
await api.loadGalaxySession()
},
loadGalaxySession: async () => {
const { ok, data } = await util.req(api.path.galaxy)
if (!ok) {
return showMessage("error", "Failed to load Galaxy session...", "app")
}
state.galaxyAppUrl = data.appUrl || DEFAULT_PLAY_STORE_URL
state.galaxyCookieName = data.cookieName || "galaxy_session"
state.galaxyPaired = !!data.paired
state.galaxySessionToken = data.sessionToken || ""
state.galaxySessionVisible = false
},
copyGalaxySession: async () => {
try {
await util.copyText(state.galaxySessionToken)
showMessage("message", "Session token copied!", "app")
} catch (e) {
showMessage("error", "Copy failed...", "app")
}
},
save: (kind) => async () => {
@@ -295,6 +357,60 @@ export function NavKeys() {
`
}
function renderAppKeys() {
return html`
<div class="navkeys-title">App Keys</div>
<div class="navkeys-app-actions">
<a
class="navkeys-btn navkeys-link-btn"
href="${() => state.galaxyAppUrl}"
rel="noopener noreferrer"
target="_blank">
<i class="bi bi-google-play"></i>
<span>Install The App</span>
</a>
</div>
<label class="navkeys-label" for="galaxy-cookie-name">Cookie Name</label>
<div class="navkeys-row">
<input
class="navkeys-input navkeys-token-input"
id="galaxy-cookie-name"
readonly
value="${() => state.galaxyCookieName}"
/>
</div>
<label class="navkeys-label" for="galaxy-session-token">Session Token</label>
<div class="navkeys-row">
<input
class="navkeys-input navkeys-token-input"
id="galaxy-session-token"
placeholder="${() => state.galaxyPaired ? "Session token unavailable..." : "Pair Galaxy to create a session token..."}"
readonly
type="${() => state.galaxySessionVisible ? "text" : "password"}"
value="${() => state.galaxySessionToken}"
/>
<button
aria-label="${() => state.galaxySessionVisible ? "Hide session token" : "Show session token"}"
class="navkeys-btn navkeys-icon-btn"
@click="${() => { state.galaxySessionVisible = !state.galaxySessionVisible }}"
disabled="${() => !state.galaxySessionToken}"
title="${() => state.galaxySessionVisible ? "Hide session token" : "Show session token"}">
<i class="${() => `bi ${state.galaxySessionVisible ? "bi-eye-slash" : "bi-eye"}`}"></i>
</button>
<button
class="navkeys-btn navkeys-copy-btn"
@click="${api.copyGalaxySession}"
disabled="${() => !state.galaxySessionToken}">
<i class="bi bi-copy"></i>
<span>Copy</span>
</button>
</div>
`
}
return html`
<div class="navkeys-wrapper navkeys-offset-top">
<div class="navkeys-container">
@@ -304,6 +420,10 @@ export function NavKeys() {
<div class="navkeys-container">
${renderGroup("Mapbox Keys", ["public", "secret"])}
${renderStatus("mapbox")}
</div>
<div class="navkeys-container navkeys-app-container">
${renderAppKeys()}
${renderStatus("app")}
</div>
</div>
${() => state.showDeleteModal ? Modal({
@@ -10,11 +10,11 @@ import { LateralManeuvers } from "/assets/components/tools/lateral_maneuvers.js"
import { LongitudinalManeuvers } from "/assets/components/tools/longitudinal_maneuvers.js"
import { MapsManager } from "/assets/components/tools/maps.js"
import { NavDestination } from "/assets/components/navigation/navigation_destination.js?v=nav-search-context-1"
import { NavKeys } from "/assets/components/navigation/navigation_keys.js?v=nav-search-context-1"
import { NavKeys } from "/assets/components/navigation/navigation_keys.js?v=app-keys-session-1"
import { RouteRecordings } from "/assets/components/recordings/dashcam_routes.js"
import { SettingsView } from "/assets/components/settings.js"
import { ScreenRecordings } from "/assets/components/recordings/screen_recordings.js"
import { Sidebar } from "/assets/components/sidebar.js"
import { Sidebar } from "/assets/components/sidebar.js?v=app-keys-session-1"
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260303t"
import { LivePlots } from "/assets/components/tools/plots.js"
@@ -18,7 +18,7 @@ const MENU_ITEMS = {
{ name: "Long Maneuvers", link: "/longitudinal_maneuvers", icon: "bi-signpost-split" },
{ name: "Maps", link: "/manage_maps", icon: "bi-map" },
{ name: "Navigation", link: "/set_navigation_destination", icon: "bi-geo-alt-fill" },
{ name: "Navigation Keys", link: "/manage_navigation_keys", icon: "bi-key-fill" },
{ name: "App Keys", link: "/manage_navigation_keys", icon: "bi-key-fill" },
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
{ name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" },
@@ -20,7 +20,7 @@
<link rel="stylesheet" href="/assets/components/main.css">
<link rel="stylesheet" href="/assets/components/modal.css">
<link rel="stylesheet" href="/assets/components/navigation/navigation_destination.css?v=nav-search-context-1">
<link rel="stylesheet" href="/assets/components/navigation/navigation_keys.css?v=nav-search-context-1">
<link rel="stylesheet" href="/assets/components/navigation/navigation_keys.css?v=app-keys-session-1">
<link rel="stylesheet" href="/assets/components/recordings/dashcam_routes.css">
<link rel="stylesheet" href="/assets/components/recordings/screen_recordings.css">
<link rel="stylesheet" href="/assets/components/settings.css">
@@ -88,3 +88,10 @@ def test_navigation_last_position_rejects_stale_persisted_fix(monkeypatch):
monkeypatch.setattr(the_galaxy, "system_time_valid", lambda: True)
assert the_galaxy._get_navigation_last_position() is None
def test_galaxy_session_value_matches_cookie_format():
assert the_galaxy._build_galaxy_session_value(
"testGalaxySlug01",
"a" * 64,
) == f"testGalaxySlug01%3A{'a' * 64}"
+39 -9
View File
@@ -484,6 +484,9 @@ KEYS = {
"secret": ("secret", "sk.", "MapboxSecretKey", "Secret key", 80),
}
GALAXY_COOKIE_NAME = "galaxy_session"
GALAXY_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.embaucha.galaxynav&hl=en-US&ah=9FldHJ99kxL8oNbSlO5F4sQqwC4"
NAVIGATION_MEMORY_LOCATION_STALE_SECONDS = 10.0
NAVIGATION_PERSISTED_LOCATION_FUTURE_SKEW_SECONDS = 60.0
NAVIGATION_PERSISTED_LOCATION_MAX_AGE_SECONDS = 24 * 60 * 60
@@ -500,6 +503,23 @@ MAPS_DOWNLOAD_PARAM = "DownloadMaps"
MAPS_CANCEL_DOWNLOAD_PARAM = "CancelDownloadMaps"
def _get_galaxy_dir():
return Path(Paths.comma_home()) / "starpilot" / "data" / "galaxy" if PC else Path("/data/galaxy")
def _read_galaxy_text(path):
try:
return path.read_text().strip() if path.is_file() else ""
except Exception:
return ""
def _build_galaxy_session_value(slug, token):
if not slug or not token:
return ""
return quote(f"{slug}:{token}", safe="")
def _parse_last_gps_position(raw_value):
if not raw_value:
return None
@@ -5571,22 +5591,32 @@ def setup(app):
}), 202
# ── Galaxy pairing (mirrors settings.cc L262-282) ──────────────────
GALAXY_DIR = Path("/data/galaxy")
GALAXY_DIR = _get_galaxy_dir()
GALAXY_AUTH_FILE = GALAXY_DIR / "glxyauth"
GALAXY_SESSION_FILE = GALAXY_DIR / "glxysession"
GALAXY_SLUG_FILE = GALAXY_DIR / "glxyslug"
@app.route("/api/galaxy/status", methods=["GET"])
def galaxy_status():
try:
paired = GALAXY_AUTH_FILE.is_file() and len(GALAXY_AUTH_FILE.read_text().strip()) == 64
except Exception:
paired = False
slug_file = GALAXY_DIR / "glxyslug"
slug = slug_file.read_text().strip() if slug_file.is_file() else ""
paired = len(_read_galaxy_text(GALAXY_AUTH_FILE)) == 64
slug = _read_galaxy_text(GALAXY_SLUG_FILE)
return jsonify({
"paired": paired,
"url": f"https://galaxy.firestar.link/{slug}" if slug else "",
})
@app.route("/api/galaxy/session", methods=["GET"])
def galaxy_session():
slug = _read_galaxy_text(GALAXY_SLUG_FILE)
token = _read_galaxy_text(GALAXY_SESSION_FILE)
paired = len(_read_galaxy_text(GALAXY_AUTH_FILE)) == 64 and bool(slug and token)
return jsonify({
"appUrl": GALAXY_PLAY_STORE_URL,
"cookieName": GALAXY_COOKIE_NAME,
"paired": paired,
"sessionToken": _build_galaxy_session_value(slug, token),
})
@app.route("/api/galaxy/pair", methods=["POST"])
def galaxy_pair():
data = request.get_json() or {}
@@ -5599,12 +5629,12 @@ def setup(app):
GALAXY_AUTH_FILE.write_text(pw_hash)
# Generate 256-bit secure session token
(GALAXY_DIR / "glxysession").write_text(secrets.token_hex(32))
GALAXY_SESSION_FILE.write_text(secrets.token_hex(32))
# Generate 16-character alphanumeric routing slug
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
slug = ''.join(secrets.choice(charset) for _ in range(16))
(GALAXY_DIR / "glxyslug").write_text(slug)
GALAXY_SLUG_FILE.write_text(slug)
return jsonify({
"message": "Pairing successful!",