mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
Merge pull request #190 from Corfirean/fix/update-release-history-target-20261030
Show notes for the available update despite a fresh older history cache
This commit is contained in:
10 files changed
+115
-44
No files matched your search
Generated
+3
-3
@@ -586,7 +586,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-core"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -634,7 +634,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-release"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"coa-core",
|
||||
@@ -643,7 +643,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-server-manager"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"coa-control-proto",
|
||||
"coa-core",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/coa-core", "crates/coa-release", "crates/coa-registry-proto",
|
||||
exclude = ["services/coa-registry", "services/coa-registry-e2e", "services/coa-coordinator", "services/coa-relay"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
area: manager
|
||||
type: fixed
|
||||
audience: admins
|
||||
title: Show available update notes even when release history is cached
|
||||
---
|
||||
Fetch the release notes for the offered update when they are missing from the cached history, including when an update becomes available while the history is open.
|
||||
@@ -63,7 +63,46 @@ fn validate(bytes: &[u8], component: &str) -> Result<Value, String> {
|
||||
}
|
||||
|
||||
pub fn load(component: &str, cache_dir: &Path) -> Result<Value, String> {
|
||||
load_for_target(component, cache_dir, None)
|
||||
}
|
||||
|
||||
pub fn load_for_target(
|
||||
component: &str,
|
||||
cache_dir: &Path,
|
||||
target: Option<&str>,
|
||||
) -> Result<Value, String> {
|
||||
let (repo, branch) = repository(component)?;
|
||||
load_with_remote(component, cache_dir, target, || {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(8))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let response = client
|
||||
.get(format!(
|
||||
"https://raw.githubusercontent.com/{repo}/{branch}/changelog/history.json"
|
||||
))
|
||||
.query(&[("target", target.unwrap_or(""))])
|
||||
.header(reqwest::header::CACHE_CONTROL, "no-cache")
|
||||
.send()
|
||||
.and_then(|r| r.error_for_status())
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut bytes = Vec::new();
|
||||
response
|
||||
.take(LIMIT + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_with_remote(
|
||||
component: &str,
|
||||
cache_dir: &Path,
|
||||
target: Option<&str>,
|
||||
remote: impl FnOnce() -> Result<Vec<u8>, String>,
|
||||
) -> Result<Value, String> {
|
||||
repository(component)?;
|
||||
let cache = cache_dir.join(format!("{component}.json"));
|
||||
let cached = || {
|
||||
let mut bytes = Vec::new();
|
||||
@@ -81,41 +120,33 @@ pub fn load(component: &str, cache_dir: &Path) -> Result<Value, String> {
|
||||
.and_then(|t| t.elapsed().ok())
|
||||
.is_some_and(|age| age < Duration::from_secs(3600))
|
||||
{
|
||||
if let Some(mut v) = cached() {
|
||||
v["source"] = "cache".into();
|
||||
return Ok(v);
|
||||
if let Some(mut value) = cached() {
|
||||
if target.is_none_or(|target| {
|
||||
value["releases"].as_array().is_some_and(|releases| {
|
||||
releases
|
||||
.iter()
|
||||
.any(|release| release["version"].as_str() == Some(target))
|
||||
})
|
||||
}) {
|
||||
value["source"] = "cache".into();
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
let remote = || -> Result<Value, String> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(8))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let response = client
|
||||
.get(format!(
|
||||
"https://raw.githubusercontent.com/{repo}/{branch}/changelog/history.json"
|
||||
))
|
||||
.send()
|
||||
.and_then(|r| r.error_for_status())
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut bytes = Vec::new();
|
||||
response
|
||||
.take(LIMIT + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let fetched = (|| {
|
||||
let bytes = remote()?;
|
||||
let mut value = validate(&bytes, component)?;
|
||||
if std::fs::create_dir_all(cache_dir).is_ok() {
|
||||
let _ = std::fs::write(&cache, &bytes);
|
||||
let _ = crate::fsx::atomic_write(&cache, &bytes);
|
||||
}
|
||||
value["source"] = "remote".into();
|
||||
Ok(value)
|
||||
};
|
||||
remote().or_else(|error| {
|
||||
})();
|
||||
fetched.or_else(|error| {
|
||||
cached()
|
||||
.map(|mut v| {
|
||||
v["source"] = "cache".into();
|
||||
v
|
||||
.map(|mut value| {
|
||||
value["source"] = "cache".into();
|
||||
value
|
||||
})
|
||||
.ok_or(error)
|
||||
})
|
||||
@@ -157,6 +188,34 @@ mod tests {
|
||||
assert!(validate(&serde_json::to_vec(&value).unwrap(), "server").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_update_bypasses_fresh_cache_missing_the_target_and_keeps_offline_fallback() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let feed = |version: &str| {
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"schema":1,"component":"manager","repository":"Corfirean/coa-server-manager",
|
||||
"releases":[{"version":version,"tag":format!("v{version}"),"date":"2026-10-30",
|
||||
"notes":["Visible update notes"]}]
|
||||
}))
|
||||
.unwrap()
|
||||
};
|
||||
std::fs::write(dir.path().join("manager.json"), feed("0.9.4")).unwrap();
|
||||
let value =
|
||||
load_with_remote("manager", dir.path(), Some("0.9.5"), || Ok(feed("0.9.5"))).unwrap();
|
||||
assert_eq!(value["source"], "remote");
|
||||
assert_eq!(value["releases"][0]["version"], "0.9.5");
|
||||
let cached = load_with_remote("manager", dir.path(), Some("0.9.5"), || {
|
||||
panic!("fresh target is cached")
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(cached["source"], "cache");
|
||||
let offline = load_with_remote("manager", dir.path(), Some("0.9.6"), || {
|
||||
Err("offline".into())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(offline["releases"][0]["version"], "0.9.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_valid_cache_is_available_without_network() {
|
||||
let dir = std::env::temp_dir().join(format!("coa-history-{}", uuid::Uuid::new_v4()));
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "coa-server-manager",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "coa-server-manager",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.8.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coa-server-manager",
|
||||
"private": true,
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -32,9 +32,9 @@ mod control_cmds;
|
||||
|
||||
/// Informational release notes only; never used to select or authorize an update.
|
||||
#[tauri::command]
|
||||
async fn release_history(component: String) -> std::result::Result<Value, String> {
|
||||
async fn release_history(component: String, target: Option<String>) -> std::result::Result<Value, String> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
coa_core::release_history::load(&component, &data_dir().join("release-history"))
|
||||
coa_core::release_history::load_for_target(&component, &data_dir().join("release-history"), target.as_deref())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CoA Server Manager",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"identifier": "com.coa.servermanager",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"Disable public server browsing in Settings": "Отключайте просмотр публичных серверов в настройках",
|
||||
"Keep deliberate stops and update shutdowns from triggering crash restart": "Ручная остановка и остановка для обновления не вызывают перезапуск после сбоя",
|
||||
"Server startup waits for a responsive console before reporting success": "Запуск сервера ожидает ответа консоли перед сообщением об успехе",
|
||||
"Restore the existing Manager window when launching it again": "Повторный запуск открывает уже существующее окно менеджера"
|
||||
"Restore the existing Manager window when launching it again": "Повторный запуск открывает уже существующее окно менеджера",
|
||||
"Show available update notes even when release history is cached": "Показывать изменения доступного обновления даже при сохранённой истории"
|
||||
},
|
||||
"de": {
|
||||
"Choose Parallel or Global level scaling in module settings": "Parallele oder globale Levelskalierung in den Moduleinstellungen wählen",
|
||||
@@ -62,7 +63,8 @@
|
||||
"Disable public server browsing in Settings": "Öffentliche Serversuche in den Einstellungen deaktivieren",
|
||||
"Keep deliberate stops and update shutdowns from triggering crash restart": "Bewusstes Stoppen und Herunterfahren für Updates lösen keinen Absturzneustart aus",
|
||||
"Server startup waits for a responsive console before reporting success": "Der Serverstart wartet vor der Erfolgsmeldung auf eine reagierende Konsole",
|
||||
"Restore the existing Manager window when launching it again": "Erneutes Starten stellt das vorhandene Manager-Fenster wieder her"
|
||||
"Restore the existing Manager window when launching it again": "Erneutes Starten stellt das vorhandene Manager-Fenster wieder her",
|
||||
"Show available update notes even when release history is cached": "Änderungen verfügbarer Updates auch bei zwischengespeichertem Verlauf anzeigen"
|
||||
},
|
||||
"fr": {
|
||||
"Choose Parallel or Global level scaling in module settings": "Choisissez une progression parallèle ou globale des niveaux dans les paramètres du module",
|
||||
@@ -94,7 +96,8 @@
|
||||
"Disable public server browsing in Settings": "Désactivez la consultation des serveurs publics dans les paramètres",
|
||||
"Keep deliberate stops and update shutdowns from triggering crash restart": "Les arrêts volontaires et ceux des mises à jour ne déclenchent plus de redémarrage après plantage",
|
||||
"Server startup waits for a responsive console before reporting success": "Le démarrage du serveur attend une console réactive avant d’annoncer sa réussite",
|
||||
"Restore the existing Manager window when launching it again": "Un nouveau lancement restaure la fenêtre existante du gestionnaire"
|
||||
"Restore the existing Manager window when launching it again": "Un nouveau lancement restaure la fenêtre existante du gestionnaire",
|
||||
"Show available update notes even when release history is cached": "Afficher les changements des mises à jour disponibles même si l’historique est en cache"
|
||||
},
|
||||
"es": {
|
||||
"Choose Parallel or Global level scaling in module settings": "Elige escalado de nivel paralelo o global en los ajustes del módulo",
|
||||
@@ -126,7 +129,8 @@
|
||||
"Disable public server browsing in Settings": "Desactiva la consulta de servidores públicos en los ajustes",
|
||||
"Keep deliberate stops and update shutdowns from triggering crash restart": "Las paradas voluntarias y las de actualización no activan el reinicio por fallo",
|
||||
"Server startup waits for a responsive console before reporting success": "El arranque del servidor espera a que responda la consola antes de indicar que ha terminado",
|
||||
"Restore the existing Manager window when launching it again": "Al volver a iniciar el gestor se restaura su ventana existente"
|
||||
"Restore the existing Manager window when launching it again": "Al volver a iniciar el gestor se restaura su ventana existente",
|
||||
"Show available update notes even when release history is cached": "Mostrar los cambios de las actualizaciones disponibles aunque el historial esté en caché"
|
||||
},
|
||||
"zh": {
|
||||
"Choose Parallel or Global level scaling in module settings": "在模块设置中选择并行或全局等级缩放",
|
||||
@@ -158,7 +162,8 @@
|
||||
"Disable public server browsing in Settings": "可在设置中禁用公共服务器浏览",
|
||||
"Keep deliberate stops and update shutdowns from triggering crash restart": "主动停止及更新时关闭不会触发崩溃自动重启",
|
||||
"Server startup waits for a responsive console before reporting success": "服务器启动会等待控制台响应后再报告成功",
|
||||
"Restore the existing Manager window when launching it again": "再次启动时恢复已有的管理器窗口"
|
||||
"Restore the existing Manager window when launching it again": "再次启动时恢复已有的管理器窗口",
|
||||
"Show available update notes even when release history is cached": "即使历史记录已缓存,也显示可用更新的说明"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
|
||||
@@ -31,14 +31,14 @@ export function ReleaseHistory({ selection, onSelect }: { selection: HistorySele
|
||||
}, [selection, component, updater.version]);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
void invoke<History>("release_history", { component }).then(feed => {
|
||||
void invoke<History>("release_history", { component, target }).then(feed => {
|
||||
if (live && feed.component === component && Array.isArray(feed.releases)) {
|
||||
setFeeds(previous => ({ ...previous, [component]: feed }));
|
||||
setFresh(previous => ({ ...previous, [component]: feed.source === "remote" }));
|
||||
}
|
||||
}).catch(() => {}); // Bundled reviewed notes remain visible offline.
|
||||
return () => { live = false; };
|
||||
}, [component]);
|
||||
}, [component, target]);
|
||||
const releases = [...(mode === "range" ? releasesForUpdate(feeds[component].releases, from, target) : feeds[component].releases)]
|
||||
.sort((a, b) => compareVersions(b.version.split("-")[0], a.version.split("-")[0]) || b.date.localeCompare(a.date));
|
||||
const record = releases.find(r => r.version === selected) ?? releases[0];
|
||||
|
||||
Reference in new issue
Block a user