release(v1.7.29-alpha): VPS as default app registry + settings UI
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 13m54s

- New Settings → App registries page (/dashboard/settings/registries)
  that mirrors the update-mirrors experience: list of configured
  registries, test reachability, set primary, add/remove. New
  registry.set-primary RPC; existing registry.{list,add,remove,test}
  reused.
- Default RegistryConfig flipped: VPS (23.182.128.160:3000/lfg2025) is
  now Server 1 (primary), tx1138 is Server 2 (fallback).
- Install pipeline now rewrites the first pull to the primary registry
  URL before attempting it. Before this, installs always hit whichever
  registry the image was hardcoded to, so changing the primary didn't
  actually affect where images came from. On failure, the existing
  fallback walk skips the primary (already tried) and walks the rest.
- App catalog proxy UPSTREAMS order flipped so the catalog follows the
  same VPS-first rule.
- Reboot overlay: animated "a" logo now sits in the center of the ring
  (matches the screensaver composition). Extracted the logo-wrapper
  pattern inline.

7/7 registry tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-21 15:54:07 -04:00
parent 79ae14a127
commit 7432d84545
15 changed files with 541 additions and 36 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.28-alpha"
version = "1.7.29-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]

View File

@@ -120,8 +120,8 @@ impl ApiHandler {
/// first 2xx response. 15s total timeout.
async fn handle_app_catalog_proxy() -> Result<Response<hyper::Body>> {
const UPSTREAMS: &[&str] = &[
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json",
"http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json",
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json",
];
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))

View File

@@ -220,6 +220,7 @@ impl RpcHandler {
"registry.list" => self.handle_registry_list().await,
"registry.add" => self.handle_registry_add(params).await,
"registry.remove" => self.handle_registry_remove(params).await,
"registry.set-primary" => self.handle_registry_set_primary(params).await,
"registry.test" => self.handle_registry_test(params).await,
// Streaming ecash payments

View File

@@ -649,8 +649,24 @@ impl RpcHandler {
);
let _ = std::fs::create_dir_all(&user_tmp);
// Rewrite to the primary registry's URL so the first attempt
// honors the operator's mirror choice (default: VPS) instead of
// blindly using whatever registry the image was hardcoded to.
// If the rewritten URL fails, pull_from_registries_with_skip
// falls through to the other configured registries.
let (primary_url, primary_tls) =
crate::container::registry::primary_image_url(&self.config.data_dir, docker_image)
.await;
if primary_url != docker_image {
debug!("Rewrote {} → {} for primary registry", docker_image, primary_url);
}
let mut pull_args = vec!["pull".to_string(), primary_url.clone()];
if !primary_tls {
pull_args.push("--tls-verify=false".to_string());
}
let mut child = tokio::process::Command::new("podman")
.args(["pull", docker_image])
.args(&pull_args)
.env("TMPDIR", &user_tmp)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
@@ -684,23 +700,35 @@ impl RpcHandler {
true
}
Err(_) => {
tracing::warn!("Image pull timed out after 60s: {}", docker_image);
tracing::warn!("Image pull timed out after 60s: {}", primary_url);
let _ = child.kill().await;
let _ = child.wait().await; // reap zombie
true
}
};
if !primary_failed && primary_url != docker_image {
// Primary pull succeeded but used a rewritten URL. Tag under
// the original image reference so downstream code (images -q,
// run -d docker_image, etc.) finds it.
let _ = tokio::process::Command::new("podman")
.args(["tag", &primary_url, docker_image])
.output()
.await;
tracing::info!("Pulled {} from primary registry ({})", docker_image, primary_url);
}
if primary_failed {
// Try all configured fallback registries dynamically
match crate::container::registry::pull_from_registries(
// Primary failed — walk the remaining configured registries.
// Skip primary_url so we don't retry what just failed.
match crate::container::registry::pull_from_registries_with_skip(
&self.config.data_dir,
docker_image,
&user_tmp,
Some(&primary_url),
)
.await
{
Ok(_) => {
tracing::info!("Pulled {} via dynamic registry fallback", docker_image);
tracing::info!("Pulled {} via fallback registry", docker_image);
}
Err(e) => {
return Err(anyhow::anyhow!("Image pull failed: {}", e));
@@ -1467,6 +1495,36 @@ server {
Ok(serde_json::json!({ "registries": config.registries, "removed": url }))
}
/// Promote a registry to primary by resetting priorities — the named
/// URL becomes priority 0, every other enabled registry is bumped up
/// by 10. Order is stable (ties broken by original priority).
pub(in crate::api::rpc) async fn handle_registry_set_primary(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let url = params
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing url"))?;
let mut config = crate::container::registry::load_registries(&self.config.data_dir).await?;
if !config.registries.iter().any(|r| r.url == url) {
return Err(anyhow::anyhow!("Registry '{}' not found", url));
}
// Reassign priorities: target = 0, everyone else = 10, 20, 30…
// in their existing priority order.
let target_url = url.to_string();
config.registries.sort_by_key(|r| (r.url != target_url, r.priority));
for (i, r) in config.registries.iter_mut().enumerate() {
r.priority = if r.url == target_url { 0 } else { (i as u32) * 10 };
}
crate::container::registry::save_registries(&self.config.data_dir, &config).await?;
Ok(serde_json::json!({ "registries": config.registries, "primary": url }))
}
pub(in crate::api::rpc) async fn handle_registry_test(
&self,
params: Option<serde_json::Value>,

View File

@@ -44,16 +44,16 @@ impl Default for RegistryConfig {
Self {
registries: vec![
Registry {
url: "git.tx1138.com/lfg2025".to_string(),
name: "Archipelago Primary".to_string(),
tls_verify: true,
url: "23.182.128.160:3000/lfg2025".to_string(),
name: "Server 1 (VPS)".to_string(),
tls_verify: false,
enabled: true,
priority: 0,
},
Registry {
url: "23.182.128.160:3000/lfg2025".to_string(),
name: "Archipelago Fallback".to_string(),
tls_verify: false,
url: "git.tx1138.com/lfg2025".to_string(),
name: "Server 2 (tx1138)".to_string(),
tls_verify: true,
enabled: true,
priority: 10,
},
@@ -94,6 +94,28 @@ impl RegistryConfig {
candidates
}
/// Rewrite an image to use the highest-priority enabled registry, so
/// the FIRST pull attempt honors the operator's primary choice instead
/// of blindly using whatever registry the image URL was hardcoded to.
/// Returns (rewritten_url, tls_verify) — or the original URL + default
/// tls_verify=true if there's no primary (no enabled registries).
pub fn rewrite_for_primary(&self, image: &str) -> (String, bool) {
match self.active_registries().first() {
Some(primary) => (self.rewrite_image(image, primary), primary.tls_verify),
None => (image.to_string(), true),
}
}
}
/// Load the registry config and rewrite an image to use the primary
/// registry's URL. Convenience wrapper for callers that don't already
/// have a `RegistryConfig` in hand.
pub async fn primary_image_url(data_dir: &Path, image: &str) -> (String, bool) {
match load_registries(data_dir).await {
Ok(config) => config.rewrite_for_primary(image),
Err(_) => (image.to_string(), true),
}
}
/// Extract the image name from a full image reference.
@@ -134,10 +156,20 @@ pub async fn save_registries(data_dir: &Path, config: &RegistryConfig) -> Result
}
/// Try pulling an image from configured registries in priority order.
/// If `already_tried` is Some, that URL is skipped (avoids retrying the
/// primary when the caller already attempted it with progress streaming).
/// Returns the image reference that succeeded.
pub async fn pull_from_registries(data_dir: &Path, image: &str, tmpdir: &str) -> Result<String> {
pub async fn pull_from_registries_with_skip(
data_dir: &Path,
image: &str,
tmpdir: &str,
already_tried: Option<&str>,
) -> Result<String> {
let config = load_registries(data_dir).await?;
let candidates = config.image_candidates(image);
let mut candidates = config.image_candidates(image);
if let Some(skip) = already_tried {
candidates.retain(|(url, _)| url != skip);
}
for (candidate, tls_verify) in &candidates {
debug!("Trying registry: {}", candidate);
@@ -196,6 +228,7 @@ pub async fn pull_from_registries(data_dir: &Path, image: &str, tmpdir: &str) ->
))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -217,9 +250,11 @@ mod tests {
#[test]
fn test_rewrite_image() {
let config = RegistryConfig::default();
let fallback = &config.registries[1];
// Default primary is now VPS (index 0). A tx1138-hardcoded image
// rewrites to VPS when asked for the primary mirror.
let primary = &config.registries[0];
assert_eq!(
config.rewrite_image("git.tx1138.com/lfg2025/bitcoin-knots:latest", fallback),
config.rewrite_image("git.tx1138.com/lfg2025/bitcoin-knots:latest", primary),
"23.182.128.160:3000/lfg2025/bitcoin-knots:latest"
);
}
@@ -228,8 +263,20 @@ mod tests {
fn test_image_candidates() {
let config = RegistryConfig::default();
let candidates = config.image_candidates("git.tx1138.com/lfg2025/lnd:v0.18.4-beta");
assert!(candidates.len() >= 2);
assert_eq!(candidates[0].0, "git.tx1138.com/lfg2025/lnd:v0.18.4-beta");
// Defaults: VPS (primary) + tx1138. tx1138 is filtered out because
// it's identical to the original image URL, leaving one candidate.
assert_eq!(candidates.len(), 1);
// Primary-first — VPS rewrite leads the candidate list.
assert_eq!(candidates[0].0, "23.182.128.160:3000/lfg2025/lnd:v0.18.4-beta");
}
#[test]
fn test_rewrite_for_primary_uses_top_priority() {
let config = RegistryConfig::default();
let (url, tls) =
config.rewrite_for_primary("git.tx1138.com/lfg2025/lnd:v0.18.4-beta");
assert_eq!(url, "23.182.128.160:3000/lfg2025/lnd:v0.18.4-beta");
assert!(!tls, "VPS primary is HTTP — tls_verify should be false");
}
#[test]