release(v1.7.27-alpha): mirror transparency — served-by line + one-click test button
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 15m40s
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 15m40s
- New "Served by {mirror}" line on the System Update page so operators can see
which mirror actually served the available manifest (vs. which is configured
primary). Backend threads the served URL through UpdateState.manifest_mirror.
- New update.test-mirror RPC + per-row lightning-bolt button that pings a
mirror and renders reachable/latency or error inline under the URL.
- UI polish on the mirrors section: Set Primary, Remove, and the new Test
action are compact icon buttons; add-mirror form moved into a dialog.
- "What's New" block prepended for v1.7.27-alpha.
21/21 update module tests pass. vue-tsc + vite build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,11 @@ pub struct UpdateState {
|
||||
pub rollback_available: bool,
|
||||
#[serde(default)]
|
||||
pub schedule: UpdateSchedule,
|
||||
/// URL of the mirror whose manifest populated `available_update`.
|
||||
/// Surfaces in the UI so operators can tell at a glance which mirror
|
||||
/// their node actually hit (vs. just which is configured primary).
|
||||
#[serde(default)]
|
||||
pub manifest_mirror: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for UpdateState {
|
||||
@@ -229,6 +234,7 @@ impl Default for UpdateState {
|
||||
update_in_progress: false,
|
||||
rollback_available: false,
|
||||
schedule: UpdateSchedule::DailyCheck,
|
||||
manifest_mirror: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +266,7 @@ pub async fn load_state(data_dir: &Path) -> Result<UpdateState> {
|
||||
// it unconditionally; the next check_for_updates will repopulate
|
||||
// if there's genuinely something newer.
|
||||
state.available_update = None;
|
||||
state.manifest_mirror = None;
|
||||
save_state(data_dir, &state).await?;
|
||||
}
|
||||
Ok(state)
|
||||
@@ -328,6 +335,7 @@ pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
||||
"Update available"
|
||||
);
|
||||
state.available_update = Some(manifest);
|
||||
state.manifest_mirror = Some(manifest_url.clone());
|
||||
} else {
|
||||
// Manifest version matches us or is behind
|
||||
// us — either we're current, or this mirror
|
||||
@@ -347,6 +355,7 @@ pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
||||
// break: another mirror could be ahead.
|
||||
continue 'mirrors;
|
||||
}
|
||||
state.manifest_mirror = None;
|
||||
state.available_update = None;
|
||||
}
|
||||
handled = true;
|
||||
@@ -375,6 +384,66 @@ pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MirrorTestResult {
|
||||
pub reachable: bool,
|
||||
pub latency_ms: u64,
|
||||
pub http_status: Option<u16>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Ping a mirror's manifest URL and return reachability + wall-clock
|
||||
/// latency. Used by the "Test mirror" button so operators can sanity-
|
||||
/// check a newly added mirror without running a full update check.
|
||||
pub async fn test_mirror(url: &str) -> MirrorTestResult {
|
||||
let client = match reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return MirrorTestResult {
|
||||
reachable: false,
|
||||
latency_ms: 0,
|
||||
http_status: None,
|
||||
error: Some(format!("client build failed: {}", e)),
|
||||
}
|
||||
}
|
||||
};
|
||||
let start = std::time::Instant::now();
|
||||
match client.get(url).send().await {
|
||||
Ok(resp) => {
|
||||
let latency_ms = start.elapsed().as_millis() as u64;
|
||||
let status = resp.status();
|
||||
if status.is_success() {
|
||||
MirrorTestResult {
|
||||
reachable: true,
|
||||
latency_ms,
|
||||
http_status: Some(status.as_u16()),
|
||||
error: None,
|
||||
}
|
||||
} else {
|
||||
MirrorTestResult {
|
||||
reachable: false,
|
||||
latency_ms,
|
||||
http_status: Some(status.as_u16()),
|
||||
error: Some(format!("HTTP {}", status.as_u16())),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let latency_ms = start.elapsed().as_millis() as u64;
|
||||
MirrorTestResult {
|
||||
reachable: false,
|
||||
latency_ms,
|
||||
http_status: None,
|
||||
error: Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current update status without checking remote.
|
||||
pub async fn get_status(data_dir: &Path) -> Result<UpdateState> {
|
||||
load_state(data_dir).await
|
||||
@@ -1155,6 +1224,7 @@ mod tests {
|
||||
update_in_progress: false,
|
||||
rollback_available: true,
|
||||
schedule: UpdateSchedule::AutoApply,
|
||||
manifest_mirror: None,
|
||||
};
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
let deserialized: UpdateState = serde_json::from_str(&json).unwrap();
|
||||
@@ -1265,6 +1335,10 @@ mod tests {
|
||||
update_in_progress: true,
|
||||
rollback_available: false,
|
||||
schedule: UpdateSchedule::Manual,
|
||||
manifest_mirror: Some(
|
||||
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
save_state(dir.path(), &state).await.unwrap();
|
||||
let loaded = load_state(dir.path()).await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user