Skip to content

Commit e93a144

Browse files
author
Paul C
committed
v22.6.14: WolfRouter traceroute one-click install + distro-aware fallback
- /api/traceroute distinguishes ENOENT from other failures and now also returns install_command (sudo apt-get/dnf/pacman/zypper install -y traceroute) so the UI's copy-paste fallback is correct on RHEL / Arch / SUSE, not just Debian/Ubuntu. - WolfRouter Trace tab uses install_command for the manual fallback; defaults to apt only if the backend didn't send one (older nodes). - System Check page surfaces the missing traceroute binary proactively with a one-click Install button, alongside tcpdump/conntrack/etc. - installer::packages gains a "traceroute" mapping for all four distro families plus unit tests pinning the mapping (so a future PACKAGES refactor can't silently drop it). - CLAUDE.md gains a "Quality Bar — The Anthropic Test" section that ties the existing global no-TODO / no-guess / Codex-review rules together as a single quality gate. Reported by Adam Cogswell 2026-04-30: Visual TraceRoute tab failing on Ubuntu minimal images that ship without traceroute, with no in-app way to fix it.
1 parent 5249c85 commit e93a144

6 files changed

Lines changed: 157 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ All frontend code lives in `web/`:
6767

6868
**Serde conventions**: All structs use `#[serde(rename_all = "snake_case")]` for enums. Fields that may be absent from older JSON configs need `#[serde(default)]`. The frontend sends/receives snake_case JSON matching Rust field names.
6969

70+
## Quality Bar — The Anthropic Test
71+
72+
**Every change must meet the bar Anthropic PBC themselves would hold this code to if they were shipping it.** Before declaring any work done, ask: *"If Anthropic engineers had to ship this exact diff under their name, would they?"* If the honest answer is "no" or "not quite", the work is not done.
73+
74+
Concretely, this means:
75+
- **No half-measures.** All branches implemented, all error paths handled, all edge cases addressed. If three platforms exist (Proxmox/libvirt/native), all three work — not two-and-a-stub.
76+
- **No guessed values.** Every constant, ID, format, and protocol detail comes from reading the source — never from memory or "this looks right".
77+
- **No dead code, no TODOs, no "follow-up later".** Finish the work or raise the scope question explicitly *before* writing the partial version.
78+
- **Re-read the diff before declaring done.** Closure bugs, async-without-await, dead variables, unused branches — catch them yourself, don't ship them for Codex/review to find.
79+
- **Be honest about what was tested.** Compile-passes ≠ feature-works. Don't claim "production-ready" without exercising the actual code path. Surface known untested paths in the closing summary.
80+
- **The user-facing surface matters as much as the code.** Visible feedback for user actions, accessible interactions, no silent failures. A correct backend with a broken UX is not shipped quality.
81+
- **Defaults must be safe.** Public surfaces (status pages, logs, error messages) never leak internals, credentials, AI output, or host data unless explicitly intended.
82+
83+
This rule overrides "ship it fast" instincts. If the bar isn't met, say so plainly and either finish the work or stop and ask.
84+
7085
## Important Conventions
7186

7287
- All Rust source files start with the copyright header (`// Written by Paul Clevett` / `// (C)Copyright Wolf Software Systems Ltd`)

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "wolfstack"
3-
version = "22.6.13"
3+
version = "22.6.14"
44
edition = "2024"
55
authors = ["Wolf Software Systems Ltd"]
66
description = "Server management platform for the Wolf software suite"

src/api/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4137,14 +4137,23 @@ pub async fn traceroute_handler(
41374137
// no in-app remedy.
41384138
let is_missing = e.kind() == std::io::ErrorKind::NotFound
41394139
|| e.raw_os_error() == Some(2 /* ENOENT */);
4140+
// Build the manual-install command for THIS host's distro
4141+
// so the UI's copy-paste fallback isn't apt-only on RHEL /
4142+
// Arch / SUSE machines.
4143+
let install_command = if is_missing {
4144+
let distro = crate::installer::detect_distro();
4145+
let (mgr, args) = crate::installer::pkg_install_cmd(distro);
4146+
Some(format!("sudo {} {} traceroute", mgr, args))
4147+
} else { None };
41404148
return HttpResponse::InternalServerError().json(serde_json::json!({
41414149
"error": if is_missing {
4142-
"traceroute isn't installed on this host. Click the button below to install it (apt/dnf/pacman), or run the install manually.".to_string()
4150+
"traceroute isn't installed on this host. Click the button below to install it, or run the install manually.".to_string()
41434151
} else {
41444152
format!("Failed to run traceroute: {}", e)
41454153
},
41464154
"missing_tool": if is_missing { Some("traceroute") } else { None },
41474155
"install_package": if is_missing { Some("traceroute") } else { None },
4156+
"install_command": install_command,
41484157
}));
41494158
}
41504159
Err(e) => {

src/installer/packages.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ const PACKAGES: &[PackageMapping] = &[
6565
suse: Some("tcpdump"),
6666
service_unit: None,
6767
},
68+
PackageMapping {
69+
// Used by the Visual TraceRoute tab in WolfRouter and the
70+
// /api/traceroute endpoint. Ubuntu minimal / cloud images ship
71+
// without it by default — Adam Cogswell 2026-04-30 reported
72+
// the tab failing with no in-app install path.
73+
logical: "traceroute",
74+
binary: "traceroute",
75+
debian: Some("traceroute"),
76+
rhel: Some("traceroute"),
77+
arch: Some("traceroute"),
78+
suse: Some("traceroute"),
79+
service_unit: None,
80+
},
6881
PackageMapping {
6982
logical: "conntrack",
7083
binary: "conntrack",
@@ -303,3 +316,46 @@ fn svc_active(unit: &str) -> bool {
303316
.args(["is-active", "--quiet", unit])
304317
.status().map(|s| s.success()).unwrap_or(false)
305318
}
319+
320+
#[cfg(test)]
321+
mod tests {
322+
use super::*;
323+
324+
/// The Visual TraceRoute one-click installer in WolfRouter and the
325+
/// System Check page both look up the logical name "traceroute"
326+
/// and assume every supported distro can resolve it. If somebody
327+
/// later refactors PACKAGES and drops a row, that breakage would
328+
/// be invisible until a real user hit it. Pin it down here.
329+
#[test]
330+
fn traceroute_mapping_resolves_for_all_distros() {
331+
let pkg = PACKAGES.iter().find(|p| p.logical == "traceroute")
332+
.expect("traceroute logical name must be in PACKAGES");
333+
for d in [
334+
DistroFamily::Debian,
335+
DistroFamily::RedHat,
336+
DistroFamily::Arch,
337+
DistroFamily::Suse,
338+
] {
339+
assert_eq!(
340+
resolve(pkg, d), Some("traceroute"),
341+
"traceroute mapping missing for distro {:?}", d
342+
);
343+
}
344+
assert_eq!(pkg.binary, "traceroute");
345+
}
346+
347+
/// Every logical name advertised through the System Check UI's
348+
/// "Install" button must be in this table — otherwise the button
349+
/// 400s with "unknown package". Keep the list of expected logical
350+
/// names checked here; failing this test means a callsite was
351+
/// added without a corresponding row.
352+
#[test]
353+
fn known_logical_names_are_present() {
354+
for logical in &["traceroute", "tcpdump", "conntrack"] {
355+
assert!(
356+
PACKAGES.iter().any(|p| p.logical == *logical),
357+
"expected logical name {:?} in PACKAGES", logical
358+
);
359+
}
360+
}
361+
}

src/systemcheck.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ pub fn run_checks() -> Vec<DependencyCheck> {
165165
out.push(simple_auto("tcpdump", "Networking", &["--version"],
166166
"packet capture (Packets tab in WolfRouter)",
167167
hint("tcpdump", "tcpdump", "tcpdump", "tcpdump")));
168+
out.push(check_traceroute());
168169
out.push(simple_auto("pppd", "Networking", &["--version"],
169170
"PPPoE dial-up (WolfRouter WAN)",
170171
hint("ppp", "ppp", "ppp", "ppp")));
@@ -498,6 +499,37 @@ fn check_kernel_module(modname: &str, category: &str, why: &str) -> DependencyCh
498499
}
499500
}
500501

502+
/// Visual TraceRoute (and the /api/traceroute endpoint) shells out to
503+
/// `traceroute`. It's not in the core diag-tool bundle and Ubuntu's
504+
/// minimal / cloud images don't ship it. Surface it on the System
505+
/// Check page with a one-click install button so an operator who
506+
/// opens the WolfRouter Trace tab knows up front whether they need
507+
/// to install something. Adam Cogswell 2026-04-30: "if traceroute is
508+
/// missing offer the user the chance to install it from a button".
509+
fn check_traceroute() -> DependencyCheck {
510+
let (found, ver) = bin_check("traceroute", &["--version"]);
511+
let status = if found { DepStatus::Ok } else { DepStatus::Missing };
512+
let detail = if found {
513+
"Installed — Visual TraceRoute tab in WolfRouter renders the path map.".into()
514+
} else {
515+
"Not installed — the WolfRouter Visual TraceRoute tab will fail. Common on Ubuntu minimal / cloud images that omit it from the default package set.".into()
516+
};
517+
DependencyCheck {
518+
name: "traceroute".into(),
519+
category: "Networking".into(),
520+
status,
521+
version: ver,
522+
detail,
523+
install_hint: if found { None } else {
524+
Some(hint("traceroute", "traceroute", "traceroute", "traceroute"))
525+
},
526+
ai_helpful: false,
527+
// One-click install hooked to /api/system/install-package via
528+
// the "traceroute" mapping added in installer/packages.rs.
529+
install_package: if found { None } else { Some("traceroute".into()) },
530+
}
531+
}
532+
501533
fn check_cron() -> DependencyCheck {
502534
let (found, ver) = bin_check("crontab", &["-V"]);
503535
// The binary alone isn't enough — without a running daemon, jobs

web/js/wolfrouter.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4336,6 +4336,49 @@
43364336
return;
43374337
}
43384338
if (!resp.ok) {
4339+
// When traceroute itself isn't installed the backend tags
4340+
// the response with `missing_tool`. Surface an inline
4341+
// install button instead of a dead-end error so the user
4342+
// doesn't have to leave the tab to fix it.
4343+
// Adam Cogswell 2026-04-30: Ubuntu minimal ships without
4344+
// traceroute; previous flow had no in-app remedy.
4345+
if (data && data.missing_tool === 'traceroute') {
4346+
const pkg = data.install_package || 'traceroute';
4347+
// Distro-aware manual-install command supplied by the
4348+
// backend (apt-get / dnf / pacman / zypper). Falls back
4349+
// to apt only if the server didn't send one — the new
4350+
// backend always does, so this is belt-and-braces for
4351+
// cluster nodes still running an older build.
4352+
const manualCmd = data.install_command || ('sudo apt install ' + pkg);
4353+
if (status) {
4354+
status.innerHTML = `❌ ${wrEsc(data.error || 'traceroute is not installed')} ` +
4355+
`<button id="wr-tr-install" class="btn btn-sm btn-primary" style="margin-left:8px;">📦 Install ${wrEsc(pkg)}</button>`;
4356+
const btn = document.getElementById('wr-tr-install');
4357+
if (btn) btn.onclick = async () => {
4358+
btn.disabled = true;
4359+
btn.textContent = '⏳ Installing…';
4360+
try {
4361+
const ir = await fetch('/api/system/install-package', {
4362+
method: 'POST', headers: { 'Content-Type': 'application/json' },
4363+
body: JSON.stringify({ package: pkg }),
4364+
});
4365+
const idata = await ir.json().catch(() => ({}));
4366+
if (!ir.ok || idata.success === false) {
4367+
status.innerHTML = `❌ Install failed: ${wrEsc(idata.error || ir.statusText || 'unknown')}. Try the package manager directly: <code>${wrEsc(manualCmd)}</code>.`;
4368+
return;
4369+
}
4370+
status.textContent = '✅ Installed — re-running trace…';
4371+
// Re-fire the original Trace flow so the user
4372+
// doesn't have to click again.
4373+
wrTracerouteGo();
4374+
} catch (e) {
4375+
status.innerHTML = `❌ Install errored: ${wrEsc(e.message || String(e))}.`;
4376+
}
4377+
};
4378+
}
4379+
if (goBtn) { goBtn.disabled = false; goBtn.textContent = '🛰 Trace'; }
4380+
return;
4381+
}
43394382
if (status) status.textContent = '❌ ' + (data.error || resp.statusText);
43404383
if (goBtn) { goBtn.disabled = false; goBtn.textContent = '🛰 Trace'; }
43414384
return;

0 commit comments

Comments
 (0)