This commit is contained in:
firestar5683
2026-09-14 14:11:41 -05:00
parent 16ec6bc5c9
commit 2878d13c3d
6 changed files with 85 additions and 16 deletions
@@ -790,7 +790,7 @@ export function NavDestination() {
<section class="keys-required-wrapper">
<div class="keys-required-widget">
<div class="keys-required-title">Mapbox Keys Required</div>
<p class="keys-required-text">You must set both your public and secret Mapbox keys before using navigation features.</p>
<p class="keys-required-text">The public key powers destination search and the map. The secret key lets your comma calculate the on-device route and provide navigation turn desires. Add both keys before starting navigation.</p>
<a href="/manage_navigation_keys" class="keys-required-button">Go to "Manage Keys"</a>
</div>
</section>
@@ -926,7 +926,7 @@ function NavigationDestination({
isFavorited,
favoriteRoutes = [],
steps = []
}) {
}) {
async function cancelNavigation() {
showSnackbar("Navigation cancelled...");
removeRouteFromMap(map);
@@ -938,19 +938,32 @@ function NavigationDestination({
await fetch("/api/navigation", { method: "DELETE" });
}
async function confirmDestination() {
let response;
let result = {};
try {
response = await fetch("/api/navigation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
longitude: destinationCoordinates[0],
latitude: destinationCoordinates[1],
routeId,
})
});
result = await response.json().catch(() => ({}));
} catch {
showSnackbar("Could not reach the comma to start navigation.", "error");
return;
}
if (!response.ok) {
showSnackbar(result.message || "Failed to start navigation.", "error");
return;
}
onConfirm?.();
showSnackbar("Navigation set!");
localStorage.setItem("activeRouteId", routeId);
await fetch("/api/navigation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
longitude: destinationCoordinates[0],
latitude: destinationCoordinates[1],
routeId,
})
});
await loadFavorites();
const searchInputEl = document.getElementById("search-field");
if (searchInputEl) searchInputEl.value = "";
@@ -97,6 +97,7 @@ export const NavigationDestinationPanel = {
navigationStarted: false,
isMetric: false,
mapboxPublic: "",
mapboxSecret: "",
language: "",
lastPosition: null,
map: null,
@@ -110,6 +111,7 @@ export const NavigationDestinationPanel = {
},
computed: {
hasMapbox() { return !!this.mapboxPublic },
hasRoutingKey() { return !!this.mapboxSecret },
recentPlaces() {
const seen = new Set()
return [...this.favorites, ...this.recentDestinations].filter((place) => {
@@ -157,6 +159,7 @@ export const NavigationDestinationPanel = {
api.getNavigationFavorites().catch(() => ({ favorites: [] })),
])
this.mapboxPublic = String(nav?.mapboxPublic || "").trim()
this.mapboxSecret = String(nav?.mapboxSecret || "").trim()
this.language = String(nav?.language || "").trim()
this.isMetric = !!nav?.isMetric
this.lastPosition = coordinates(nav?.lastPosition)
@@ -284,6 +287,10 @@ export const NavigationDestinationPanel = {
showSnackbar("Add a Mapbox public key in App Keys first.", "error")
return
}
if (!this.hasRoutingKey) {
showSnackbar("Add a Mapbox secret key in App Keys first. It is required for the comma to calculate the on-device route and provide navigation turn desires.", "error")
return
}
this.loadingRoute = true
try {
this.destination = place || await this.resolveQuery()
@@ -419,11 +426,14 @@ export const NavigationDestinationPanel = {
<div v-else ref="map" class="gx-navigation-map"></div>
<div v-if="hasMapbox && !loading" class="gx-navigation-overlay">
<section v-if="!hasRoutingKey" class="gx-navigation-error gx-card">
The map and destination search only use your public Mapbox key. Add a <a href="#/navigation/keys">secret Mapbox key in App Keys</a> before starting navigation so the comma can calculate the on-device route and provide turn desires.
</section>
<section class="gx-navigation-search gx-card">
<div class="gx-navigation-search__row">
<i class="bi bi-search" aria-hidden="true"></i>
<input class="gx-field" v-model="query" @input="onInput" @keyup.enter="setDestination()" placeholder="Search here" aria-label="Search for a destination" autocomplete="off" />
<button type="button" class="gx-icon-btn gx-navigation-send" :disabled="loadingRoute || searching || !query.trim()" @click="setDestination()" aria-label="Send destination" title="Send destination"><i class="bi bi-send-fill"></i></button>
<button type="button" class="gx-icon-btn gx-navigation-send" :disabled="loadingRoute || searching || !query.trim() || !hasRoutingKey" @click="setDestination()" aria-label="Send destination" :title="hasRoutingKey ? 'Send destination' : 'A Mapbox secret key is required to start navigation'"><i class="bi bi-send-fill"></i></button>
</div>
<div v-if="searching" class="gx-navigation-status">Searching...</div>
<div v-if="suggestions.length" class="gx-navigation-suggestions">
@@ -453,7 +463,7 @@ export const NavigationDestinationPanel = {
</div>
<div class="gx-navigation-summary__actions">
<button v-if="navigationStarted" type="button" class="gx-btn gx-btn--danger" @click="cancelNavigation"><i class="bi bi-x-lg"></i> Cancel Navigation</button>
<button v-else type="button" class="gx-btn gx-btn--success" :disabled="loadingRoute" @click="setDestination(destination)"><i class="bi bi-sign-turn-right"></i> {{ loadingRoute ? 'Calculating...' : 'Start Navigation' }}</button>
<button v-else type="button" class="gx-btn gx-btn--success" :disabled="loadingRoute || !hasRoutingKey" :title="hasRoutingKey ? 'Start Navigation' : 'A Mapbox secret key is required to start navigation'" @click="setDestination(destination)"><i class="bi bi-sign-turn-right"></i> {{ loadingRoute ? 'Calculating...' : 'Start Navigation' }}</button>
<button type="button" class="gx-btn gx-btn--favorite" :class="{ active: isFavorite }" @click="toggleFavorite"><i class="bi" :class="isFavorite ? 'bi-heart-fill' : 'bi-heart'"></i> {{ isFavorite ? 'Unfavorite' : 'Favorite' }}</button>
</div>
</section>
@@ -393,6 +393,7 @@ def _install_server_import_stubs():
sys.modules["openpilot.starpilot.navigation.destination_store"] = _simple_module(
"openpilot.starpilot.navigation.destination_store",
normalize_destination_payload=lambda payload: payload,
routing_configured=lambda params: bool(str(params.get("MapboxSecretKey") or "").strip()),
update_recent_destinations=lambda *args, **kwargs: [],
)
sys.modules["openpilot.starpilot.system.the_galaxy.factory_reset"] = _simple_module(
@@ -462,6 +462,33 @@ def test_navigation_last_position_rejects_stale_persisted_fix(monkeypatch):
assert the_galaxy._get_navigation_last_position() is None
def test_navigation_api_rejects_destination_without_secret_key(monkeypatch):
client, fake_params = _params_client(monkeypatch, {"MapboxPublicKey": "public"}, "tici")
response = client.post("/api/navigation", json={
"name": "Work",
"latitude": 41.0,
"longitude": -87.0,
})
assert response.status_code == 400
assert "secret key" in response.get_json()["message"]
assert fake_params.get("NavDestination") is None
def test_navigation_api_accepts_destination_with_secret_key(monkeypatch):
client, fake_params = _params_client(monkeypatch, {"MapboxSecretKey": "secret"}, "tici")
response = client.post("/api/navigation", json={
"name": "Work",
"latitude": 41.0,
"longitude": -87.0,
})
assert response.status_code == 200
assert json.loads(fake_params.get("NavDestination"))["name"] == "Work"
def test_save_longitudinal_maneuver_status_writes_json_param_as_dict(monkeypatch):
fake_params = WritableFakeParams()
monkeypatch.setattr(the_galaxy, "params", fake_params)
@@ -504,7 +504,6 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
assert "ref=\"map\"" in destination and "setNavigation(this.destination)" in destination
assert destination.count("methods: {") == 1 and "secondaryLabel," in destination
assert _read("js/components/LateralTuningPanel.js")
# Shared API surface added for the second batch of ported pages.
for method in ["selectTestingGround",
"getSentryStatus", "getSentryEvents", "deleteSentryEvent", "sentryPushSubscribe",
@@ -536,6 +535,20 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
assert lateral.index('>Saved Tunes</span>') < lateral.index('>Local Routes</span>')
def test_navigation_requires_secret_key_before_starting_on_device_route():
destination = _read("js/components/NavigationDestinationPanel.js")
classic_destination = (REPO_ROOT / "starpilot/system/the_galaxy/assets/components/navigation/navigation_destination.js").read_text(encoding="utf-8")
assert 'mapboxSecret: ""' in destination
assert "hasRoutingKey()" in destination
assert "!query.trim() || !hasRoutingKey" in destination
assert "loadingRoute || !hasRoutingKey" in destination
assert "required for the comma to calculate the on-device route" in destination
assert "secret key lets your comma calculate the on-device route" in classic_destination
assert "if (!response.ok)" in classic_destination
assert 'result.message || "Failed to start navigation."' in classic_destination
def test_ui_cameras_hub_vasm_and_pip_native_no_embed():
app = _read("js/app.js")
store = _read("js/store.js")
+6 -1
View File
@@ -168,7 +168,7 @@ from openpilot.starpilot.common.testing_grounds import (
TESTING_GROUNDS_SLOT_DEFINITIONS as SHARED_TESTING_GROUNDS_SLOT_DEFINITIONS,
TESTING_GROUNDS_STATE_PATH as SHARED_TESTING_GROUNDS_STATE_PATH,
)
from openpilot.starpilot.navigation.destination_store import normalize_destination_payload, update_recent_destinations
from openpilot.starpilot.navigation.destination_store import normalize_destination_payload, routing_configured, update_recent_destinations
from openpilot.starpilot.system.the_galaxy.factory_reset import remove_path as _run_factory_reset_delete
from openpilot.starpilot.system.the_galaxy import flm_workspace, utilities
from openpilot.starpilot.system.the_galaxy.update_recovery import inspect_interrupted_update, public_recovery_status, recover_interrupted_update
@@ -5760,6 +5760,11 @@ def setup(app):
@app.route("/api/navigation", methods=["POST"])
def set_navigation():
if not routing_configured(params):
return {
"message": "A Mapbox secret key is required to calculate the on-device route and provide navigation turn desires. Add it in App Keys first."
}, 400
destination = normalize_destination_payload(request.json)
if destination is None:
return {"message": "Invalid destination payload"}, 400