Compare commits
2 Commits
4346007d37
...
36015a19fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36015a19fe | ||
|
|
e57514b690 |
@ -312,7 +312,16 @@ impl RpcHandler {
|
|||||||
|
|
||||||
let mut stopped = 0u32;
|
let mut stopped = 0u32;
|
||||||
let mut removed = 0u32;
|
let mut removed = 0u32;
|
||||||
let mut errors = Vec::new();
|
// Two distinct failure classes, kept separate so they don't get
|
||||||
|
// conflated (the old single `errors` vec did, which caused the "ghost in
|
||||||
|
// My Apps" bug): `container_errors` means a container could NOT be
|
||||||
|
// removed (force-rm failed too) — the app is genuinely still present, so
|
||||||
|
// we keep its state entry and surface a hard error. `cleanup_errors`
|
||||||
|
// means volume/network/data-dir teardown left residue — the containers
|
||||||
|
// are already gone, so the app IS uninstalled and MUST disappear from My
|
||||||
|
// Apps; the residue is logged but never ghosts the app.
|
||||||
|
let mut container_errors: Vec<String> = Vec::new();
|
||||||
|
let mut cleanup_errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
self.set_uninstall_stage(
|
self.set_uninstall_stage(
|
||||||
package_id,
|
package_id,
|
||||||
@ -370,7 +379,7 @@ impl RpcHandler {
|
|||||||
let msg =
|
let msg =
|
||||||
format!("Failed to remove {}: {}; {}", name, stderr.trim(), e);
|
format!("Failed to remove {}: {}; {}", name, stderr.trim(), e);
|
||||||
tracing::error!("Uninstall {}: {}", package_id, msg);
|
tracing::error!("Uninstall {}: {}", package_id, msg);
|
||||||
errors.push(msg);
|
container_errors.push(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -379,12 +388,35 @@ impl RpcHandler {
|
|||||||
Err(force_err) => {
|
Err(force_err) => {
|
||||||
let msg = format!("Failed to remove {}: {}; {}", name, e, force_err);
|
let msg = format!("Failed to remove {}: {}; {}", name, e, force_err);
|
||||||
tracing::error!("Uninstall {}: {}", package_id, msg);
|
tracing::error!("Uninstall {}: {}", package_id, msg);
|
||||||
errors.push(msg);
|
container_errors.push(msg);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A container that survived even force-remove means the app is NOT
|
||||||
|
// actually uninstalled — keep its state entry and fail so the spawned
|
||||||
|
// task reverts it to its prior state (and the user can retry), rather
|
||||||
|
// than orphaning a live container that's missing from My Apps.
|
||||||
|
if !container_errors.is_empty() {
|
||||||
|
tracing::error!(
|
||||||
|
"Uninstall {}: containers could not be removed: {:?}",
|
||||||
|
package_id,
|
||||||
|
container_errors
|
||||||
|
);
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Uninstall {} failed: {}",
|
||||||
|
package_id,
|
||||||
|
container_errors.join("; ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containers are gone → the app is uninstalled. Remove its state entry
|
||||||
|
// NOW, before the (possibly slow, possibly fallible) volume/data
|
||||||
|
// teardown below, so My Apps updates immediately and a residue failure
|
||||||
|
// can never leave a ghost. Reinstall/scan no longer see a stale entry.
|
||||||
|
self.remove_package_state_entry(package_id).await;
|
||||||
|
|
||||||
self.set_uninstall_stage(package_id, "Cleaning up volumes")
|
self.set_uninstall_stage(package_id, "Cleaning up volumes")
|
||||||
.await;
|
.await;
|
||||||
// Avoid global Podman volume prune on production nodes: store-wide
|
// Avoid global Podman volume prune on production nodes: store-wide
|
||||||
@ -432,70 +464,73 @@ impl RpcHandler {
|
|||||||
let stderr = String::from_utf8_lossy(&o.stderr);
|
let stderr = String::from_utf8_lossy(&o.stderr);
|
||||||
let msg = format!("Failed to remove data {}: {}", dir, stderr.trim());
|
let msg = format!("Failed to remove data {}: {}", dir, stderr.trim());
|
||||||
tracing::error!("Uninstall {}: {}", package_id, msg);
|
tracing::error!("Uninstall {}: {}", package_id, msg);
|
||||||
errors.push(msg);
|
cleanup_errors.push(msg);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let msg = format!("Failed to remove data {}: {}", dir, e);
|
let msg = format!("Failed to remove data {}: {}", dir, e);
|
||||||
tracing::error!("Uninstall {}: {}", package_id, msg);
|
tracing::error!("Uninstall {}: {}", package_id, msg);
|
||||||
errors.push(msg);
|
cleanup_errors.push(msg);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
// The app is already gone from My Apps (entry removed above). Residual
|
||||||
|
// volume/data cleanup failures are logged but NEVER ghost the app — a
|
||||||
|
// reinstall and the next uninstall both tolerate leftover dirs.
|
||||||
|
if !cleanup_errors.is_empty() {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Uninstall {} completed with errors: {:?}",
|
"Uninstall {} removed but left cleanup residue: {:?}",
|
||||||
package_id,
|
package_id,
|
||||||
errors
|
cleanup_errors
|
||||||
);
|
);
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Uninstall {} partially failed: {}",
|
|
||||||
package_id,
|
|
||||||
errors.join("; ")
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Uninstall {} complete: stopped={}, removed={}",
|
"Uninstall {} complete: stopped={}, removed={}, cleanup_errors={}",
|
||||||
package_id,
|
package_id,
|
||||||
stopped,
|
stopped,
|
||||||
removed
|
removed,
|
||||||
|
cleanup_errors.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Immediately remove from in-memory state so the UI updates without
|
|
||||||
// waiting for the scanner's absence threshold (3 scans × 60s each).
|
|
||||||
{
|
|
||||||
let (mut data, _rev) = self.state_manager.get_snapshot().await;
|
|
||||||
let before = data.package_data.len();
|
|
||||||
data.package_data.remove(package_id);
|
|
||||||
// Also remove any alias keys (e.g. "bitcoin-knots" vs "bitcoin")
|
|
||||||
let aliases: Vec<String> = data
|
|
||||||
.package_data
|
|
||||||
.keys()
|
|
||||||
.filter(|k| {
|
|
||||||
super::config::all_container_names(package_id)
|
|
||||||
.iter()
|
|
||||||
.any(|c| c.strip_prefix("archy-").unwrap_or(c) == k.as_str())
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
for alias in &aliases {
|
|
||||||
data.package_data.remove(alias);
|
|
||||||
}
|
|
||||||
if data.package_data.len() < before {
|
|
||||||
self.state_manager.update_data(data).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"status": "uninstalled",
|
"status": "uninstalled",
|
||||||
"stopped": stopped,
|
"stopped": stopped,
|
||||||
"removed": removed,
|
"removed": removed,
|
||||||
|
"cleanup_warnings": cleanup_errors,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove a package's entry (and any alias keys) from persisted state so it
|
||||||
|
/// disappears from My Apps immediately, without waiting for the scanner's
|
||||||
|
/// absence threshold (3 scans × 60s). Called as soon as an uninstall has
|
||||||
|
/// removed the app's containers — before the slower volume/data teardown —
|
||||||
|
/// so a residue failure can never leave a ghost entry behind.
|
||||||
|
async fn remove_package_state_entry(&self, package_id: &str) {
|
||||||
|
let (mut data, _rev) = self.state_manager.get_snapshot().await;
|
||||||
|
let before = data.package_data.len();
|
||||||
|
data.package_data.remove(package_id);
|
||||||
|
// Also remove any alias keys (e.g. "bitcoin-knots" vs "bitcoin").
|
||||||
|
let aliases: Vec<String> = data
|
||||||
|
.package_data
|
||||||
|
.keys()
|
||||||
|
.filter(|k| {
|
||||||
|
super::config::all_container_names(package_id)
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.strip_prefix("archy-").unwrap_or(c) == k.as_str())
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for alias in &aliases {
|
||||||
|
data.package_data.remove(alias);
|
||||||
|
}
|
||||||
|
if data.package_data.len() < before {
|
||||||
|
self.state_manager.update_data(data).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a bundled app (create container from pre-loaded image if needed).
|
/// Start a bundled app (create container from pre-loaded image if needed).
|
||||||
pub(in crate::api::rpc) async fn handle_bundled_app_start(
|
pub(in crate::api::rpc) async fn handle_bundled_app_start(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@ -245,7 +245,26 @@ phases 2–6 (`dual-ecash-design.md`).
|
|||||||
|
|
||||||
## 8b. SESSION STATE + RESUME (updated 2026-06-23) — READ §8b "CURRENT STATE + RESUME" FIRST
|
## 8b. SESSION STATE + RESUME (updated 2026-06-23) — READ §8b "CURRENT STATE + RESUME" FIRST
|
||||||
|
|
||||||
### ▶ CURRENT STATE + RESUME (2026-06-23) — RESUME FROM HERE (works from any device)
|
### ▶ SESSION b (2026-06-23 PM) — LATEST, RESUME FROM HERE
|
||||||
|
|
||||||
|
**Canonical resume detail: memory `project_session_resume_2026_06_23b` (▶️ top of MEMORY.md).**
|
||||||
|
`gitea-vps2/main = 4346007d` pushed; local HEAD `e57514b6` (uninstall fix, committed, **not pushed/deployed**).
|
||||||
|
|
||||||
|
Shipped + verified live on .228 (all in 4346007d):
|
||||||
|
- **Connection-lost FULLY fixed** — companion `image_exists` journal-flood (Stdio::null) + netbird UDP-port reconcile churn (`wait_for_manifest_host_ports` tcp-only). .228: flood→0, ws/db→0 disconnects, load 3.95→2.26.
|
||||||
|
- **netbird → manifest-driven** (#20 ph4) — 3 manifests + 4 orchestrator primitives (base64 secret, GeneratedCert+`ensure_manifest_certs`, templated-file render `{{HOST_IP}}/{{NETWORK_GATEWAY}}/{{secret:}}`, udp port protocol). Live: https 8087→200, OIDC→200, resolver=gateway. Legacy-Rust delete deferred to post-full-verify.
|
||||||
|
- **registry-manifest flip (code)** — `EMBED_MANIFESTS` default-on, `main.rs` bounded pre-load `refresh_catalog`. Catalog regenerated w/ 52 embedded manifests but **NOT published** (gitignored + never committed; publish = force-add to gitea-vps2 main). Do after fleet binary roll.
|
||||||
|
- **UX regression root-caused + fixed** — the mobile/desktop UX (loader/AppLoadingScreen, store-driven launch, app icons, android webview footer) was on `companion-mobile-ux` and **never merged to main**, so any main build silently dropped it. **Merged → main**, frontend redeployed to .228. Android 0.4.9/code13 pushed for user to build APK elsewhere.
|
||||||
|
|
||||||
|
In progress — **Workstream F lifecycle bugs** (this §, user-picked next):
|
||||||
|
- **uninstall ghost — FIXED (e57514b6), not deployed.** `handle_package_uninstall` returned Err on any cleanup-residue failure *before* removing the package state entry → ghost in My Apps + revert-to-Installed. Now: split container vs cleanup errors; remove state entry as soon as containers gone (before slow data rm). NEXT: deploy + verify a real uninstall clears My Apps (CAUTION: destroys app data — get user OK).
|
||||||
|
- grafana reinstall-stops (#14, likely same root cause, re-test after) + fedimint guardian wait-vs-stuck (#15, not started).
|
||||||
|
|
||||||
|
Next: deploy+verify uninstall fix on .228 → push e57514b6 → roll binary to fleet → publish catalog → finish F → Phase 3 → multinode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ▶ CURRENT STATE + RESUME (2026-06-23) — earlier session-a baseline (historical)
|
||||||
|
|
||||||
**✅ HEADLINE (2026-06-23): single-node gate GREEN (`run-gate.sh` 5/5 on .228, 0 not-ok) +
|
**✅ HEADLINE (2026-06-23): single-node gate GREEN (`run-gate.sh` 5/5 on .228, 0 not-ok) +
|
||||||
multinode test deploy DONE to 6 nodes.** The exit criterion (§5) is met. Green took fixing **two real
|
multinode test deploy DONE to 6 nodes.** The exit criterion (§5) is met. Green took fixing **two real
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user