342 lines
13 KiB
Rust
Raw Normal View History

use super::RpcHandler;
use crate::update;
use anyhow::{Context, Result};
impl RpcHandler {
/// Check for available system updates.
/// Tries git-based check first (if repo exists), falls back to manifest-based.
pub(super) async fn handle_update_check(&self) -> Result<serde_json::Value> {
release(v1.7.6-alpha): robust apply_update + manifest-override env var apply_update frontend swap Transient EROFS on .198 (filesystem hiccup — root FS mounts with errors=remount-ro so a fleeting glitch can bounce /opt to RO for a moment) caught the pre-cleanup `rm -rf web-ui.new web-ui.bak` mid- stride and aborted the apply. Rewrote the swap to use a timestamped staging dir (web-ui.new.<ms>) and a timestamped old-copy path so nothing needs to be rm'd before the extract. After the new tree is mv'd into place, the previous rollback copy is rotated aside with a .<ms> suffix (best-effort) and this apply's old copy becomes the new web-ui.bak. If the final mv fails, the staged old is restored so nginx keeps serving. handle_update_check manifest override handle_update_check takes the git path whenever ~/archy/.git exists. On the dev box (.116) that meant the Pull & Rebuild button was always the only option even though the manifest-path OTA was already wired via ARCHIPELAGO_UPDATE_URL. Now: if that env var is set, we skip the git detection entirely and use the manifest path. The regular fleet (no env var, no repo) hits the manifest branch naturally; beta dev nodes (repo + no env var) still get Pull & Rebuild; dev nodes with the env var explicitly set can finally test the manifest OTA end-to-end. Artefacts: archipelago 356e78cc…91a6dd 40372288 archipelago-frontend-1.7.6-alpha.tar.gz 4fb79664…0172e9 76984615 (reused) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:33:10 -04:00
// Manifest override: when ARCHIPELAGO_UPDATE_URL is explicitly set,
// the operator wants OTA via manifest — typically a dev box where
// ~/archy/.git exists but isn't the intended update surface.
// Without this short-circuit the dev box always advertises "Pull
// & Rebuild" and can never exercise the manifest OTA path.
let manifest_override = std::env::var("ARCHIPELAGO_UPDATE_URL").is_ok();
let repo_dir = std::path::PathBuf::from(
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
)
.join("archy");
release(v1.7.6-alpha): robust apply_update + manifest-override env var apply_update frontend swap Transient EROFS on .198 (filesystem hiccup — root FS mounts with errors=remount-ro so a fleeting glitch can bounce /opt to RO for a moment) caught the pre-cleanup `rm -rf web-ui.new web-ui.bak` mid- stride and aborted the apply. Rewrote the swap to use a timestamped staging dir (web-ui.new.<ms>) and a timestamped old-copy path so nothing needs to be rm'd before the extract. After the new tree is mv'd into place, the previous rollback copy is rotated aside with a .<ms> suffix (best-effort) and this apply's old copy becomes the new web-ui.bak. If the final mv fails, the staged old is restored so nginx keeps serving. handle_update_check manifest override handle_update_check takes the git path whenever ~/archy/.git exists. On the dev box (.116) that meant the Pull & Rebuild button was always the only option even though the manifest-path OTA was already wired via ARCHIPELAGO_UPDATE_URL. Now: if that env var is set, we skip the git detection entirely and use the manifest path. The regular fleet (no env var, no repo) hits the manifest branch naturally; beta dev nodes (repo + no env var) still get Pull & Rebuild; dev nodes with the env var explicitly set can finally test the manifest OTA end-to-end. Artefacts: archipelago 356e78cc…91a6dd 40372288 archipelago-frontend-1.7.6-alpha.tar.gz 4fb79664…0172e9 76984615 (reused) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:33:10 -04:00
if !manifest_override && repo_dir.join(".git").exists() {
if let Ok(git_status) = self.git_check_update(&repo_dir).await {
return Ok(git_status);
}
}
// Fall back to manifest-based check
let state = update::check_for_updates(&self.config.data_dir).await?;
let update_info = state.available_update.as_ref().map(|u| {
serde_json::json!({
"version": u.version,
"release_date": u.release_date,
"changelog": u.changelog,
"components": u.components.len(),
})
});
Ok(serde_json::json!({
"current_version": state.current_version,
"last_check": state.last_check,
"update_available": update_info.is_some(),
"update": update_info,
}))
}
/// Git-based update check: runs `git fetch` and compares HEAD to origin/main.
async fn git_check_update(&self, repo_dir: &std::path::Path) -> Result<serde_json::Value> {
let repo_str = repo_dir.to_string_lossy().to_string();
// git fetch origin main
let fetch = tokio::process::Command::new("git")
.args(["fetch", "origin", "main", "--quiet"])
.current_dir(&repo_str)
.output()
.await
.context("git fetch failed")?;
if !fetch.status.success() {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
anyhow::bail!(
"git fetch failed: {}",
String::from_utf8_lossy(&fetch.stderr)
);
}
// Get local and remote HEADs
let local = tokio::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.current_dir(&repo_str)
.output()
.await?;
let local_hash = String::from_utf8_lossy(&local.stdout).trim().to_string();
let remote = tokio::process::Command::new("git")
.args(["rev-parse", "--short", "origin/main"])
.current_dir(&repo_str)
.output()
.await?;
let remote_hash = String::from_utf8_lossy(&remote.stdout).trim().to_string();
let update_available = local_hash != remote_hash;
// Get commit count and changelog if update available
let mut changelog = Vec::new();
let mut commits_behind = 0u64;
if update_available {
let count = tokio::process::Command::new("git")
.args(["rev-list", "HEAD..origin/main", "--count"])
.current_dir(&repo_str)
.output()
.await?;
commits_behind = String::from_utf8_lossy(&count.stdout)
.trim()
.parse()
.unwrap_or(0);
let log = tokio::process::Command::new("git")
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.args([
"log",
"HEAD..origin/main",
"--oneline",
"--no-merges",
"-20",
])
.current_dir(&repo_str)
.output()
.await?;
changelog = String::from_utf8_lossy(&log.stdout)
.lines()
.map(|l| l.to_string())
.collect();
}
let now = chrono::Utc::now().to_rfc3339();
Ok(serde_json::json!({
"current_version": local_hash,
"last_check": now,
"update_available": update_available,
"update_method": "git",
"update": if update_available {
Some(serde_json::json!({
"version": remote_hash,
"commits_behind": commits_behind,
"changelog": changelog,
}))
} else { None },
}))
}
/// Apply git-based update: runs self-update.sh which pulls, builds, and restarts.
pub(super) async fn handle_update_git_apply(&self) -> Result<serde_json::Value> {
let script = std::path::PathBuf::from(
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()),
)
.join("archy/scripts/self-update.sh");
if !script.exists() {
anyhow::bail!("self-update.sh not found at {}", script.display());
}
// Spawn the update script in the background (it will restart the service)
let child = tokio::process::Command::new("bash")
.arg(&script)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.context("Failed to spawn self-update.sh")?;
tracing::info!(pid = child.id(), "Self-update script spawned");
Ok(serde_json::json!({
"started": true,
"message": "Update started. The service will restart when complete.",
}))
}
/// Get update status without checking remote.
pub(super) async fn handle_update_status(&self) -> Result<serde_json::Value> {
let state = update::get_status(&self.config.data_dir).await?;
release(v1.7.15-alpha): bulletproof downloads — resume, retry, real progress download_update Each component download is now resumable via HTTP Range requests (Range: bytes=N-) and retried up to 6 times with exponential backoff (5/15/30/60/120/180s). On a dropped connection the next attempt picks up at the last written byte offset instead of restarting at zero. Streams via reqwest::Response::chunk() to the staging file so a 160 MB frontend tarball doesn't sit in RAM. SHA is verified over the complete file at the end of each component; mismatch nukes the staged file and restarts from scratch. Real download progress counters New AtomicU64 globals DOWNLOAD_BYTES/DOWNLOAD_TOTAL are updated from the chunk loop. update.status exposes them as download_progress.{bytes_downloaded, total_bytes, active}. The SystemUpdate.vue progress bar now polls update.status every second instead of incrementing a fake random counter — and crucially, if the user navigates away and back, the component picks up the in-progress download from the backend atomics immediately. Update-check retries handle_update_check now retries the manifest fetch up to 3 times with a 5s gap if the first try hits a transport error, so a momentary gitea hiccup doesn't make a node report "up to date" when there actually is a new release. Tight 10s connect timeout per attempt keeps the total bounded. Artefacts: archipelago 1070c87f…c081c162b 40584792 archipelago-frontend-1.7.15-alpha.tar.gz 8e630eba…63fd43f 162078068 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:17:58 -04:00
// Expose live download progress so the UI can resume the
// progress bar after navigation instead of showing the fake
// creep again. An RPC poll every ~1s during download drives a
// real progress indicator that survives route changes.
let downloaded = update::DOWNLOAD_BYTES
.load(std::sync::atomic::Ordering::Relaxed);
let total = update::DOWNLOAD_TOTAL
.load(std::sync::atomic::Ordering::Relaxed);
let active = total > 0 && downloaded < total;
let completed = total > 0 && downloaded >= total;
// Stall detection: if the progress-at timestamp hasn't advanced
// for 30+ seconds while active, the download is wedged (usually
// HTTP stream silently dropped and reqwest is waiting out its
// read timeout). The UI uses this to surface a Cancel button
// with explanatory copy.
let stalled = if active {
let last_at = update::DOWNLOAD_PROGRESS_AT
.load(std::sync::atomic::Ordering::Relaxed);
if last_at > 0 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
now.saturating_sub(last_at) > 30_000
} else {
false
}
} else {
false
};
Ok(serde_json::json!({
"current_version": state.current_version,
"last_check": state.last_check,
"update_available": state.available_update.is_some(),
"update_in_progress": state.update_in_progress,
"rollback_available": state.rollback_available,
release(v1.7.15-alpha): bulletproof downloads — resume, retry, real progress download_update Each component download is now resumable via HTTP Range requests (Range: bytes=N-) and retried up to 6 times with exponential backoff (5/15/30/60/120/180s). On a dropped connection the next attempt picks up at the last written byte offset instead of restarting at zero. Streams via reqwest::Response::chunk() to the staging file so a 160 MB frontend tarball doesn't sit in RAM. SHA is verified over the complete file at the end of each component; mismatch nukes the staged file and restarts from scratch. Real download progress counters New AtomicU64 globals DOWNLOAD_BYTES/DOWNLOAD_TOTAL are updated from the chunk loop. update.status exposes them as download_progress.{bytes_downloaded, total_bytes, active}. The SystemUpdate.vue progress bar now polls update.status every second instead of incrementing a fake random counter — and crucially, if the user navigates away and back, the component picks up the in-progress download from the backend atomics immediately. Update-check retries handle_update_check now retries the manifest fetch up to 3 times with a 5s gap if the first try hits a transport error, so a momentary gitea hiccup doesn't make a node report "up to date" when there actually is a new release. Tight 10s connect timeout per attempt keeps the total bounded. Artefacts: archipelago 1070c87f…c081c162b 40584792 archipelago-frontend-1.7.15-alpha.tar.gz 8e630eba…63fd43f 162078068 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:17:58 -04:00
"download_progress": if active || completed {
Some(serde_json::json!({
"bytes_downloaded": downloaded,
"total_bytes": total,
"active": active,
"stalled": stalled,
release(v1.7.15-alpha): bulletproof downloads — resume, retry, real progress download_update Each component download is now resumable via HTTP Range requests (Range: bytes=N-) and retried up to 6 times with exponential backoff (5/15/30/60/120/180s). On a dropped connection the next attempt picks up at the last written byte offset instead of restarting at zero. Streams via reqwest::Response::chunk() to the staging file so a 160 MB frontend tarball doesn't sit in RAM. SHA is verified over the complete file at the end of each component; mismatch nukes the staged file and restarts from scratch. Real download progress counters New AtomicU64 globals DOWNLOAD_BYTES/DOWNLOAD_TOTAL are updated from the chunk loop. update.status exposes them as download_progress.{bytes_downloaded, total_bytes, active}. The SystemUpdate.vue progress bar now polls update.status every second instead of incrementing a fake random counter — and crucially, if the user navigates away and back, the component picks up the in-progress download from the backend atomics immediately. Update-check retries handle_update_check now retries the manifest fetch up to 3 times with a 5s gap if the first try hits a transport error, so a momentary gitea hiccup doesn't make a node report "up to date" when there actually is a new release. Tight 10s connect timeout per attempt keeps the total bounded. Artefacts: archipelago 1070c87f…c081c162b 40584792 archipelago-frontend-1.7.15-alpha.tar.gz 8e630eba…63fd43f 162078068 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:17:58 -04:00
}))
} else { None },
}))
}
/// Dismiss the update notification.
pub(super) async fn handle_update_dismiss(&self) -> Result<serde_json::Value> {
update::dismiss_update(&self.config.data_dir).await?;
Ok(serde_json::json!({ "ok": true }))
}
/// Download the available update to staging.
pub(super) async fn handle_update_download(&self) -> Result<serde_json::Value> {
let progress = update::download_update(&self.config.data_dir).await?;
Ok(serde_json::json!({
"total_bytes": progress.total_bytes,
"downloaded_bytes": progress.downloaded_bytes,
"components_downloaded": progress.components_downloaded,
}))
}
/// Cancel an in-flight or stuck download. Clears the live counters
/// and staging dir so the UI returns to the "Download Update" state.
pub(super) async fn handle_update_cancel_download(&self) -> Result<serde_json::Value> {
update::cancel_download(&self.config.data_dir).await?;
Ok(serde_json::json!({ "canceled": true }))
}
/// Apply the staged update.
pub(super) async fn handle_update_apply(&self) -> Result<serde_json::Value> {
update::apply_update(&self.config.data_dir).await?;
Ok(serde_json::json!({ "applied": true, "restart_required": true }))
}
/// Rollback to the previous version.
pub(super) async fn handle_update_rollback(&self) -> Result<serde_json::Value> {
update::rollback_update(&self.config.data_dir).await?;
Ok(serde_json::json!({ "rolled_back": true, "restart_required": true }))
}
/// List configured update mirrors in priority order.
pub(super) async fn handle_update_list_mirrors(&self) -> Result<serde_json::Value> {
let list = update::load_mirrors(&self.config.data_dir).await?;
Ok(serde_json::json!({ "mirrors": list }))
}
/// Add a mirror to the end of the list. Params: `{ url, label? }`.
/// Duplicates (same URL) are replaced rather than added twice.
pub(super) async fn handle_update_add_mirror(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let url = params
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing url"))?
.trim()
.to_string();
if !url.starts_with("http://") && !url.starts_with("https://") {
anyhow::bail!("url must start with http:// or https://");
}
let label = params
.get("label")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let mut list = update::load_mirrors(&self.config.data_dir).await?;
list.retain(|m| m.url != url);
list.push(update::UpdateMirror { url, label });
update::save_mirrors(&self.config.data_dir, &list).await?;
Ok(serde_json::json!({ "mirrors": list }))
}
/// Remove a mirror by URL. Params: `{ url }`.
pub(super) async fn handle_update_remove_mirror(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let url = params
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing url"))?;
let mut list = update::load_mirrors(&self.config.data_dir).await?;
list.retain(|m| m.url != url);
update::save_mirrors(&self.config.data_dir, &list).await?;
Ok(serde_json::json!({ "mirrors": list }))
}
/// Move a mirror to the top of the list so it's tried first.
/// Params: `{ url }`.
pub(super) async fn handle_update_set_primary_mirror(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let url = params
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing url"))?;
let mut list = update::load_mirrors(&self.config.data_dir).await?;
let Some(idx) = list.iter().position(|m| m.url == url) else {
anyhow::bail!("mirror not in list");
};
let entry = list.remove(idx);
list.insert(0, entry);
update::save_mirrors(&self.config.data_dir, &list).await?;
Ok(serde_json::json!({ "mirrors": list }))
}
/// Get the current update schedule.
pub(super) async fn handle_update_get_schedule(&self) -> Result<serde_json::Value> {
let schedule = update::get_schedule(&self.config.data_dir).await?;
Ok(serde_json::json!({ "schedule": schedule }))
}
/// Set the update schedule. Params: { schedule: "manual" | "daily_check" | "auto_apply" }
pub(super) async fn handle_update_set_schedule(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let schedule_str = params["schedule"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'schedule' parameter"))?;
let schedule = match schedule_str {
"manual" => update::UpdateSchedule::Manual,
"daily_check" => update::UpdateSchedule::DailyCheck,
"auto_apply" => update::UpdateSchedule::AutoApply,
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
_ => anyhow::bail!(
"Invalid schedule: '{}'. Use manual, daily_check, or auto_apply",
schedule_str
),
};
update::set_schedule(&self.config.data_dir, schedule).await?;
Ok(serde_json::json!({ "schedule": schedule }))
}
}