dev mode: prefer the DHP's data, fall back to the local install

A dev run is for working on the client, and the data worth working against lives
on the DHP. Deciding this by "is anything installed" guessed wrong both ways: a PC
with an old install silently ran on stale local data, with nothing on screen
saying so, while a PC with none was refused outright.

Now: dev mode + the DHP answering -> the client runs from the checkout against the
DHP's API and tiles, installed or not. No DHP -> the local stack, as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kelorastus
2026-10-02 16:10:07 +02:00
parent 7a97afe046
commit 53bba10b3e
2 changed files with 37 additions and 8 deletions
+31 -5
View File
@@ -696,22 +696,27 @@ impl Launcher {
repo.join("tools").join("build").join("build.bat").is_file().then_some(repo)
}
/// Can this PC run a dev client with no install at all, straight against its DHP?
/// The DHP-backed dev run: the client comes from the checkout and its data comes from the
/// DHP - live tracks, live tiles, nothing installed needed.
///
/// Only with nothing installed: where there IS an install the local stack is the better
/// run - it holds this PC's own data and it is what an operator actually uses. This is
/// purely the case that used to be refused outright.
/// PREFERRED whenever dev mode is on and the DHP answers, installed or not. A dev run is
/// for working on the client, and the DHP holds the data worth working against; the local
/// stack is the fallback for when the DHP cannot be reached. Deciding it by "is anything
/// installed" guessed wrong in both directions: a PC with an old install silently ran on
/// stale local data, and nothing on screen said so.
///
/// Every reason it might not work is checked here rather than at launch, because the hub
/// needs to know whether to offer Launch at all.
fn dhp_direct(inner: &Inner) -> Option<DhpDirect> {
if Self::dev_engine(inner).is_none() || !inner.online || inner.state.installed_version.is_some() {
if Self::dev_engine(inner).is_none() || !inner.online {
return None;
}
let mcp = inner.settings.mcp_url.trim();
if mcp.is_empty() {
return None;
}
// The DHP's own Engine API, as its health answer named it. None means the DHP did not
// say where it is, so there is nothing to point a client at and the run falls back.
Some(DhpDirect {
engine_url: inner.engine_url.clone()?,
mcp_url: mcp.to_string(),
@@ -2975,6 +2980,27 @@ fn swapped_user<'a>(asked: &'a str, operator: Option<&str>) -> Option<&'a str> {
mod tests {
use super::*;
/// The rule the hub now follows: the DHP is PREFERRED, the install is the fallback.
/// Written as a table because the old version decided this by "is anything installed",
/// which was wrong in both directions.
#[test]
fn the_dhp_is_preferred_and_the_install_is_the_fallback() {
// (dev mode, online, installed) -> what a dev run should use
let cases = [
(true, true, true, "dhp"), // installed, but online: the live data wins
(true, true, false, "dhp"), // nothing installed: the case that was refused outright
(true, false, true, "local"), // no DHP: fall back to what is on this PC
(true, false, false, "none"), // no DHP and nothing installed: nothing to run
(false, true, true, "local"), // dev mode off: an ordinary operator launch
];
for (dev, online, installed, want) in cases {
// dhp_direct() needs an Inner, so this asserts the DECISION the gate encodes rather
// than calling it: dev && online picks the DHP, and only then does install matter.
let got = if dev && online { "dhp" } else if installed { "local" } else { "none" };
assert_eq!(got, want, "dev={dev} online={online} installed={installed}");
}
}
#[test]
fn nothing_installed_still_launches_when_the_dhp_can_stand_in() {
// The whole point of the DHP-direct run: a checkout and a reachable DHP are enough.
+6 -3
View File
@@ -243,9 +243,12 @@
<p class="secondary">
{#if checking}
Checking {site}…
{:else if !s?.installed}
Nothing is installed here: the client comes from your checkout and runs on
<strong>{site}</strong>'s data. You sign in inside ODIAN, not here.
{:else if s?.dhp_direct}
<!-- The DHP is preferred whenever it answers, installed or not: a dev run is for
working on the client, and the live data is on the DHP. -->
Live from <strong>{site}</strong>: the client comes from your checkout and its data,
tracks and map come from the DHP. You sign in inside ODIAN, not here.
{#if s?.installed}<span class="dim">Nothing local starts; {s.installed} is used only when the DHP is unreachable.</span>{/if}
{:else if s?.update_required}
<!-- Only reachable on a dev run: any other PC would be on the "update" action. -->
ODIAN <strong>{s.installed}</strong> is installed, <strong>{s.available}</strong> is on {site}. A dev run starts