From a0a17a2e175e3d4d38d9ee2183b73e4bed69dcaf Mon Sep 17 00:00:00 2001
From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com>
Date: Mon, 2 Mar 2026 13:42:42 +0100
Subject: [PATCH] refactor: consolidate exit codes into single source of truth,
add exit code column to dashboard
- Replace duplicate exitCodeCategories + exitCodeDescriptions maps in service.go
with unified exitCodeInfo map (single struct per code: Desc + Category)
- Add helper functions getExitCodeDescription() and getExitCodeCategory()
- Add all missing exit codes: 103-123 (validation/setup), 150-154 (systemd),
160-162 (Python), 170-193 (databases), 200-231 (Proxmox), 232-238 (tools),
239-249 (Node.js), 250-254 (app install/update), BSD sysexits (64-78)
- Replace ~300-line switch statement in dashboard.go with 3-line lookup
- Add 'Exit Code' column to Installation Log table (badge for failed/aborted)
- Add new error category 'build' to allowedErrorCategory
- Add missing category colors in error-analysis.js (service, database, proxmox, shell, build)
- Net reduction: ~148 lines of duplicated code removed
---
dashboard.go | 335 +----------------------------
public/static/css/dashboard.css | 23 ++
public/static/js/dashboard.js | 11 +-
public/static/js/error-analysis.js | 3 +-
public/templates/dashboard.html | 3 +-
service.go | 327 ++++++++++++++++++++--------
6 files changed, 277 insertions(+), 425 deletions(-)
diff --git a/dashboard.go b/dashboard.go
index ea2f05c..04c9527 100644
--- a/dashboard.go
+++ b/dashboard.go
@@ -1145,343 +1145,12 @@ func (p *PBClient) FetchErrorAnalysisData(ctx context.Context, days int, repoSou
// Build exit code stats
for code, count := range exitCodeCounts {
- desc := "Unknown"
- cat := "unknown"
- // Use the exit code descriptions and categories from service.go
if code == 0 {
// exit_code=0 is Success — skip from error stats
continue
}
- switch code {
- case 1:
- desc = "General error"
- cat = "unknown"
- case 2:
- desc = "Misuse of shell builtins"
- cat = "unknown"
- case 4:
- desc = "curl: Network/protocol error"
- cat = "network"
- case 5:
- desc = "curl: Could not resolve proxy"
- cat = "network"
- case 6:
- desc = "curl: Could not resolve host"
- cat = "network"
- case 7:
- desc = "curl: Connection refused"
- cat = "network"
- case 8:
- desc = "curl: FTP server reply error"
- cat = "network"
- case 10:
- desc = "Docker / privileged mode required"
- cat = "config"
- case 22:
- desc = "curl: HTTP error (404/500 etc.)"
- cat = "network"
- case 23:
- desc = "curl: Write error (disk full?)"
- cat = "storage"
- case 25:
- desc = "curl: Upload failed"
- cat = "network"
- case 28:
- desc = "curl: Connection timed out"
- cat = "timeout"
- case 30:
- desc = "curl: FTP port command failed"
- cat = "network"
- case 35:
- desc = "SSL connect error"
- cat = "network"
- case 56:
- desc = "curl: Receive error (connection reset)"
- cat = "network"
- case 75:
- desc = "Temporary failure (retry later)"
- cat = "unknown"
- case 78:
- desc = "curl: Remote file not found (404)"
- cat = "network"
- case 100:
- desc = "APT: Package manager error (broken packages / dependency problems)"
- cat = "apt"
- case 101:
- desc = "APT: Unmet dependencies"
- cat = "apt"
- case 102:
- desc = "APT: Lock held by another process"
- cat = "apt"
- case 124:
- desc = "Command timed out (timeout command)"
- cat = "timeout"
- case 125:
- desc = "Docker daemon error (container failed to run)"
- cat = "config"
- case 126:
- desc = "Command cannot execute (permission problem)"
- cat = "permission"
- case 127:
- desc = "Command not found"
- cat = "command_not_found"
- case 128:
- desc = "Invalid argument to exit"
- cat = "signal"
- case 129:
- desc = "Killed by SIGHUP (terminal closed / hangup)"
- cat = "user_aborted"
- case 130:
- desc = "Script terminated by Ctrl+C (SIGINT)"
- cat = "user_aborted"
- case 131:
- desc = "Killed by SIGQUIT (core dump)"
- cat = "signal"
- case 134:
- desc = "Process aborted (SIGABRT)"
- cat = "signal"
- case 137:
- desc = "Process killed (SIGKILL) - likely OOM"
- cat = "resource"
- case 139:
- desc = "Segmentation fault (SIGSEGV)"
- cat = "unknown"
- case 141:
- desc = "Broken pipe (SIGPIPE)"
- cat = "signal"
- case 143:
- desc = "Process terminated (SIGTERM)"
- cat = "signal"
- // Systemd / Service errors
- case 150:
- desc = "Systemd: Service failed to start"
- cat = "service"
- case 151:
- desc = "Systemd: Service unit not found"
- cat = "service"
- case 152:
- desc = "Permission denied (EACCES)"
- cat = "permission"
- case 153:
- desc = "Build/compile failed (make/gcc/cmake)"
- cat = "service"
- case 154:
- desc = "Node.js: Native addon build failed (node-gyp)"
- cat = "service"
- // Python / pip / uv
- case 160:
- desc = "Python: Virtualenv / uv environment missing or broken"
- cat = "dependency"
- case 161:
- desc = "Python: Dependency resolution failed"
- cat = "dependency"
- case 162:
- desc = "Python: Installation aborted (EXTERNALLY-MANAGED)"
- cat = "dependency"
- // PostgreSQL
- case 170:
- desc = "PostgreSQL: Connection failed"
- cat = "database"
- case 171:
- desc = "PostgreSQL: Authentication failed"
- cat = "database"
- case 172:
- desc = "PostgreSQL: Database does not exist"
- cat = "database"
- case 173:
- desc = "PostgreSQL: Fatal error in query"
- cat = "database"
- // MySQL / MariaDB
- case 180:
- desc = "MySQL/MariaDB: Connection failed"
- cat = "database"
- case 181:
- desc = "MySQL/MariaDB: Authentication failed"
- cat = "database"
- case 182:
- desc = "MySQL/MariaDB: Database does not exist"
- cat = "database"
- case 183:
- desc = "MySQL/MariaDB: Fatal error in query"
- cat = "database"
- // MongoDB
- case 190:
- desc = "MongoDB: Connection failed"
- cat = "database"
- case 191:
- desc = "MongoDB: Authentication failed"
- cat = "database"
- case 192:
- desc = "MongoDB: Database not found"
- cat = "database"
- case 193:
- desc = "MongoDB: Fatal query error"
- cat = "database"
- // Proxmox Custom Codes
- case 200:
- desc = "Proxmox: Failed to create lock file"
- cat = "proxmox"
- case 203:
- desc = "Proxmox: Missing CTID variable"
- cat = "config"
- case 204:
- desc = "Proxmox: Missing PCT_OSTYPE variable"
- cat = "config"
- case 205:
- desc = "Proxmox: Invalid CTID (<100)"
- cat = "config"
- case 206:
- desc = "Proxmox: CTID already in use"
- cat = "config"
- case 207:
- desc = "Proxmox: Password contains unescaped special chars"
- cat = "config"
- case 208:
- desc = "Proxmox: Invalid configuration (DNS/MAC/Network)"
- cat = "config"
- case 209:
- desc = "Proxmox: Container creation failed"
- cat = "proxmox"
- case 210:
- desc = "Proxmox: Cluster not quorate"
- cat = "proxmox"
- case 211:
- desc = "Proxmox: Timeout waiting for template lock"
- cat = "timeout"
- case 212:
- desc = "Proxmox: Storage 'iscsidirect' does not support containers"
- cat = "proxmox"
- case 213:
- desc = "Proxmox: Storage does not support 'rootdir' content"
- cat = "proxmox"
- case 214:
- desc = "Proxmox: Not enough storage space"
- cat = "storage"
- case 215:
- desc = "Proxmox: Container created but not listed (ghost state)"
- cat = "proxmox"
- case 216:
- desc = "Proxmox: RootFS entry missing in config"
- cat = "proxmox"
- case 217:
- desc = "Proxmox: Storage not accessible"
- cat = "storage"
- case 218:
- desc = "Proxmox: Template file corrupted or incomplete"
- cat = "proxmox"
- case 219:
- desc = "Proxmox: CephFS does not support containers"
- cat = "storage"
- case 220:
- desc = "Proxmox: Unable to resolve template path"
- cat = "proxmox"
- case 221:
- desc = "Proxmox: Template file not readable"
- cat = "proxmox"
- case 222:
- desc = "Proxmox: Template download failed"
- cat = "proxmox"
- case 223:
- desc = "Proxmox: Template not available after download"
- cat = "proxmox"
- case 224:
- desc = "Proxmox: PBS storage is for backups only"
- cat = "storage"
- case 225:
- desc = "Proxmox: No template available for OS/Version"
- cat = "proxmox"
- case 231:
- desc = "Proxmox: LXC stack upgrade failed"
- cat = "proxmox"
- // Node.js / npm
- case 243:
- desc = "Node.js: Out of memory (heap overflow)"
- cat = "resource"
- case 245:
- desc = "Node.js: Invalid command-line option"
- cat = "config"
- case 246:
- desc = "Node.js: Internal JavaScript Parse Error"
- cat = "unknown"
- case 247:
- desc = "Node.js: Fatal internal error"
- cat = "unknown"
- case 248:
- desc = "Node.js: Invalid C++ addon / N-API failure"
- cat = "unknown"
- case 249:
- desc = "npm/pnpm/yarn: Unknown fatal error"
- cat = "unknown"
- case 255:
- desc = "DPKG: Fatal internal error / set -e triggered"
- cat = "apt"
- default:
- if code > 128 && code < 192 {
- sigNum := code - 128
- sigName := ""
- switch sigNum {
- case 1:
- sigName = "SIGHUP (terminal closed)"
- case 2:
- sigName = "SIGINT (user interrupt)"
- case 3:
- sigName = "SIGQUIT (core dump)"
- case 6:
- sigName = "SIGABRT (abort)"
- case 9:
- sigName = "SIGKILL (force killed, likely OOM)"
- case 11:
- sigName = "SIGSEGV (segmentation fault)"
- case 13:
- sigName = "SIGPIPE (broken pipe)"
- case 15:
- sigName = "SIGTERM (terminated)"
- case 24:
- sigName = "SIGXCPU (CPU time limit exceeded)"
- case 25:
- sigName = "SIGXFSZ (file size limit exceeded)"
- default:
- sigName = fmt.Sprintf("signal %d", sigNum)
- }
- desc = "Killed by " + sigName
- cat = "signal"
- } else if code >= 64 && code <= 78 {
- // BSD sysexits.h codes
- switch code {
- case 64:
- desc = "Usage error (wrong arguments)"
- case 65:
- desc = "Data error (bad input data)"
- case 66:
- desc = "Input file not found"
- case 67:
- desc = "User not found"
- case 68:
- desc = "Host not found"
- case 69:
- desc = "Service unavailable"
- case 70:
- desc = "Internal software error"
- case 71:
- desc = "System error (OS error)"
- case 72:
- desc = "Critical OS file missing"
- case 73:
- desc = "Cannot create output file"
- case 74:
- desc = "I/O error"
- case 75:
- desc = "Temporary failure (retry later)"
- case 76:
- desc = "Remote protocol error"
- case 77:
- desc = "Permission denied"
- case 78:
- desc = "Configuration error"
- }
- cat = "unknown"
- }
- }
+ desc := getExitCodeDescription(code)
+ cat := getExitCodeCategory(code)
pct := float64(count) / float64(data.TotalErrors) * 100
data.ExitCodeStats = append(data.ExitCodeStats, ExitCodeStat{
ExitCode: code,
diff --git a/public/static/css/dashboard.css b/public/static/css/dashboard.css
index 4e3a213..a04a551 100644
--- a/public/static/css/dashboard.css
+++ b/public/static/css/dashboard.css
@@ -595,6 +595,29 @@ tr.clickable-row {
border-color: rgba(100, 116, 139, 0.3);
}
+/* Exit Code Badge */
+.exit-code-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 36px;
+ padding: 3px 8px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 700;
+ font-family: 'JetBrains Mono', monospace;
+ background: rgba(239, 68, 68, 0.12);
+ color: var(--accent-red, #ef4444);
+ border: 1px solid rgba(239, 68, 68, 0.25);
+ letter-spacing: 0.5px;
+}
+
+.exit-code-badge.aborted {
+ background: rgba(168, 85, 247, 0.12);
+ color: var(--accent-purple, #a855f7);
+ border-color: rgba(168, 85, 247, 0.25);
+}
+
/* Type Badge */
.type-badge {
display: inline-flex;
diff --git a/public/static/js/dashboard.js b/public/static/js/dashboard.js
index 8847034..31edc52 100644
--- a/public/static/js/dashboard.js
+++ b/public/static/js/dashboard.js
@@ -520,7 +520,7 @@ function renderTableRows(records) {
currentRecords = records;
if (records.length === 0) {
- tbody.innerHTML = '
No records found |
';
+ tbody.innerHTML = 'No records found |
';
return;
}
@@ -533,8 +533,17 @@ function renderTableRows(records) {
const created = r.created ? formatTimestamp(r.created) : '-';
const osDisplay = r.os_type ? (r.os_type + (r.os_version ? ' ' + r.os_version : '')) : '-';
+ // Exit code column: show badge for failed, dash for success/running
+ let exitCodeCell = '-';
+ if (r.status === 'failed' && r.exit_code !== undefined && r.exit_code !== null) {
+ exitCodeCell = '' + r.exit_code + '';
+ } else if (r.status === 'aborted' && r.exit_code !== undefined && r.exit_code !== null) {
+ exitCodeCell = '' + r.exit_code + '';
+ }
+
return '' +
'| ' + escapeHtml(r.status || 'unknown') + ' | ' +
+ '' + exitCodeCell + ' | ' +
'' + escapeHtml((r.type || '-').toUpperCase()) + ' | ' +
'' + escapeHtml(r.nsapp || '-') + ' | ' +
'' + escapeHtml(osDisplay) + ' | ' +
diff --git a/public/static/js/error-analysis.js b/public/static/js/error-analysis.js
index 154a7d1..adb5395 100644
--- a/public/static/js/error-analysis.js
+++ b/public/static/js/error-analysis.js
@@ -8,7 +8,8 @@ const catColors = {
'command_not_found': '#a855f7', 'user_aborted': '#64748b', 'timeout': '#eab308',
'storage': '#ec4899', 'resource': '#f97316', 'dependency': '#22d3ee',
'signal': '#eab308', 'config': '#84cc16', 'unknown': '#64748b',
- 'uncategorized': '#94a3b8'
+ 'uncategorized': '#94a3b8', 'service': '#06b6d4', 'database': '#8b5cf6',
+ 'proxmox': '#f59e0b', 'shell': '#fb923c', 'build': '#d946ef'
};
function escapeHtml(str) {
diff --git a/public/templates/dashboard.html b/public/templates/dashboard.html
index 3497a85..c02a842 100644
--- a/public/templates/dashboard.html
+++ b/public/templates/dashboard.html
@@ -301,6 +301,7 @@
| Status |
+ Exit Code |
Type |
Application |
OS |
@@ -311,7 +312,7 @@
- |
+ |
diff --git a/service.go b/service.go
index 4b76904..e24b4a4 100644
--- a/service.go
+++ b/service.go
@@ -709,84 +709,239 @@ var (
"timeout": true, "config": true, "resource": true, "unknown": true, "": true,
"user_aborted": true, "apt": true, "command_not_found": true,
"service": true, "database": true, "signal": true, "proxmox": true,
- "shell": true,
+ "shell": true, "build": true,
}
- // exitCodeCategories maps well-known exit codes to error categories
- exitCodeCategories = map[int]string{
- 1: "unknown", // General error
- 2: "unknown", // Misuse of shell builtins
- 4: "network", // curl: Network/protocol error
- 5: "network", // curl: Could not resolve proxy
- 6: "network", // curl: Could not resolve host
- 7: "network", // curl: Connection refused
- 8: "network", // curl: FTP server reply error
- 10: "config", // Docker / privileged mode required
- 22: "network", // curl: HTTP error (404/500 etc.)
- 23: "storage", // curl: Write error (disk full?)
- 25: "network", // curl: Upload failed
- 28: "timeout", // curl: Connection timed out
- 35: "network", // SSL connect error
- 56: "network", // curl: Receive error (connection reset)
- 100: "apt", // APT: package manager error
- 101: "apt", // APT: Unmet dependencies
- 102: "apt", // APT: Lock held by another process
- 124: "timeout", // Command timed out
- 125: "config", // Docker daemon error / container failed to run
- 126: "permission", // Command invoked cannot execute
- 127: "command_not_found", // Command not found
- 128: "signal", // Invalid argument to exit
- 129: "user_aborted", // Killed by SIGHUP (terminal closed) — reclassified as aborted
- 130: "user_aborted", // Script terminated by Ctrl+C (SIGINT)
- 131: "signal", // Killed by SIGQUIT (core dump)
- 134: "signal", // Process aborted (SIGABRT)
- 137: "resource", // SIGKILL - often OOM killer
- 139: "unknown", // SIGSEGV - segfault
- 141: "signal", // SIGPIPE
- 143: "signal", // SIGTERM
- 255: "apt", // DPKG: Fatal internal error
- }
+ // exitCodeInfo consolidates description and category for all known exit codes.
+ // This is the single source of truth — dashboard.go and all other code should
+ // use getExitCodeDescription() / getExitCodeCategory() instead of duplicating.
+ exitCodeInfo = map[int]struct {
+ Desc string
+ Category string
+ }{
+ // --- Generic / Shell ---
+ 0: {"Success", ""},
+ 1: {"General error", "unknown"},
+ 2: {"Misuse of shell builtins", "unknown"},
+ 3: {"General syntax or argument error", "unknown"},
- // exitCodeDescriptions provides human-readable exit code descriptions
- exitCodeDescriptions = map[int]string{
- 0: "Success",
- 1: "General error",
- 2: "Misuse of shell builtins",
- 4: "curl: Network/protocol error",
- 5: "curl: Could not resolve proxy",
- 6: "curl: DNS resolution failed",
- 7: "curl: Connection refused",
- 8: "curl: FTP server reply error",
- 10: "Docker / privileged mode required (unsupported environment)",
- 22: "curl: HTTP error (404/500 etc.)",
- 23: "curl: Write error (disk full?)",
- 25: "curl: Upload failed",
- 28: "curl: Connection timed out",
- 30: "curl: FTP port command failed",
- 35: "SSL connect error",
- 56: "curl: Receive error (connection reset)",
- 75: "Temporary failure (retry later)",
- 78: "curl: Remote file not found (404)",
- 100: "APT: Package manager error (broken packages / dependency problems)",
- 101: "APT: Unmet dependencies",
- 102: "APT: Lock held by another process",
- 124: "Command timed out",
- 125: "Docker daemon error (container failed to run)",
- 126: "Command cannot execute (permission problem)",
- 127: "Command not found",
- 128: "Invalid argument to exit",
- 129: "Killed by SIGHUP (terminal closed / hangup)",
- 130: "Script terminated by Ctrl+C (SIGINT)",
- 131: "Killed by SIGQUIT (core dump)",
- 134: "Process aborted (SIGABRT)",
- 137: "Process killed (SIGKILL) - likely OOM",
- 139: "Segmentation fault (SIGSEGV)",
- 141: "Broken pipe (SIGPIPE)",
- 143: "Process terminated (SIGTERM)",
- 255: "DPKG: Fatal internal error",
+ // --- curl / wget ---
+ 4: {"curl: Feature not supported or protocol error", "network"},
+ 5: {"curl: Could not resolve proxy", "network"},
+ 6: {"curl: DNS resolution failed", "network"},
+ 7: {"curl: Connection refused / host down", "network"},
+ 8: {"curl: Server reply error", "network"},
+ 16: {"curl: HTTP/2 framing layer error", "network"},
+ 18: {"curl: Partial file (transfer incomplete)", "network"},
+ 22: {"curl: HTTP error (404/500 etc.)", "network"},
+ 23: {"curl: Write error (disk full?)", "storage"},
+ 24: {"curl: Write to local file failed", "storage"},
+ 25: {"curl: Upload failed", "network"},
+ 26: {"curl: Read error on local file (I/O)", "storage"},
+ 27: {"curl: Out of memory", "resource"},
+ 28: {"curl: Connection timed out", "timeout"},
+ 30: {"curl: FTP port command failed", "network"},
+ 32: {"curl: FTP SIZE command failed", "network"},
+ 33: {"curl: HTTP range error", "network"},
+ 34: {"curl: HTTP post error", "network"},
+ 35: {"curl: SSL/TLS handshake failed", "network"},
+ 36: {"curl: FTP bad download resume", "network"},
+ 47: {"curl: Too many redirects", "network"},
+ 51: {"curl: SSL peer certificate verification failed", "network"},
+ 52: {"curl: Empty reply from server", "network"},
+ 55: {"curl: Failed sending network data", "network"},
+ 56: {"curl: Receive error (connection reset)", "network"},
+ 59: {"curl: Couldn't use specified SSL cipher", "network"},
+ 75: {"Temporary failure (retry later)", "network"},
+ 78: {"curl: Remote file not found (404)", "network"},
+ 92: {"curl: HTTP/2 stream error", "network"},
+ 95: {"curl: HTTP/3 layer error", "network"},
+
+ // --- Docker / Privileged ---
+ 10: {"Docker / privileged mode required", "config"},
+
+ // --- BSD sysexits.h (64-78) ---
+ 64: {"Usage error (wrong arguments)", "config"},
+ 65: {"Data format error (bad input data)", "unknown"},
+ 66: {"Input file not found", "unknown"},
+ 67: {"User not found", "unknown"},
+ 68: {"Host not found", "network"},
+ 69: {"Service unavailable", "service"},
+ 70: {"Internal software error", "unknown"},
+ 71: {"System error (OS-level failure)", "unknown"},
+ 72: {"Critical OS file missing", "unknown"},
+ 73: {"Cannot create output file", "storage"},
+ 74: {"I/O error", "storage"},
+ 76: {"Remote protocol error", "network"},
+ 77: {"Permission denied", "permission"},
+
+ // --- APT / DPKG ---
+ 100: {"APT: Package manager error (broken packages)", "apt"},
+ 101: {"APT: Configuration error (bad sources)", "apt"},
+ 102: {"APT: Lock held by another process", "apt"},
+
+ // --- Script Validation & Setup (103-123) ---
+ 103: {"Validation: Shell is not Bash", "config"},
+ 104: {"Validation: Not running as root", "permission"},
+ 105: {"Validation: PVE version not supported", "config"},
+ 106: {"Validation: Architecture not supported (ARM/PiMox)", "config"},
+ 107: {"Validation: Kernel key parameters unreadable", "config"},
+ 108: {"Validation: Kernel key limits exceeded", "config"},
+ 109: {"Proxmox: No available container ID", "proxmox"},
+ 110: {"Proxmox: Failed to apply default.vars", "proxmox"},
+ 111: {"Proxmox: App defaults file not available", "proxmox"},
+ 112: {"Proxmox: Invalid install menu option", "config"},
+ 113: {"LXC: Under-provisioned — user aborted", "user_aborted"},
+ 114: {"LXC: Storage too low — user aborted", "user_aborted"},
+ 115: {"Download: install.func failed or incomplete", "network"},
+ 116: {"Proxmox: Default bridge vmbr0 not found", "config"},
+ 117: {"LXC: Container did not reach running state", "proxmox"},
+ 118: {"LXC: No IP assigned after timeout", "timeout"},
+ 119: {"Proxmox: No valid storage for rootdir", "storage"},
+ 120: {"Proxmox: No valid storage for vztmpl", "storage"},
+ 121: {"LXC: Container network not ready", "network"},
+ 122: {"LXC: No internet — user declined", "user_aborted"},
+ 123: {"LXC: Local IP detection failed", "network"},
+
+ // --- Common shell/system errors ---
+ 124: {"Command timed out", "timeout"},
+ 125: {"Docker daemon error / command failed to start", "config"},
+ 126: {"Command cannot execute (permission problem)", "permission"},
+ 127: {"Command not found", "command_not_found"},
+ 128: {"Invalid argument to exit", "signal"},
+ 129: {"Killed by SIGHUP (terminal closed)", "user_aborted"},
+ 130: {"Script terminated by Ctrl+C (SIGINT)", "user_aborted"},
+ 131: {"Killed by SIGQUIT (core dump)", "signal"},
+ 132: {"Killed by SIGILL (illegal instruction)", "signal"},
+ 134: {"Process aborted (SIGABRT)", "signal"},
+ 137: {"Process killed (SIGKILL) — likely OOM", "resource"},
+ 139: {"Segmentation fault (SIGSEGV)", "unknown"},
+ 141: {"Broken pipe (SIGPIPE)", "signal"},
+ 143: {"Process terminated (SIGTERM)", "signal"},
+ 144: {"Killed by signal 16 (SIGUSR1/SIGSTKFLT)", "signal"},
+ 146: {"Killed by signal 18 (SIGTSTP)", "signal"},
+
+ // --- Systemd / Service errors (150-154) ---
+ 150: {"Systemd: Service failed to start", "service"},
+ 151: {"Systemd: Service unit not found", "service"},
+ 152: {"Permission denied (EACCES)", "permission"},
+ 153: {"Build/compile failed (make/gcc/cmake)", "build"},
+ 154: {"Node.js: Native addon build failed (node-gyp)", "build"},
+
+ // --- Python / pip / uv (160-162) ---
+ 160: {"Python: Virtualenv/uv environment missing or broken", "dependency"},
+ 161: {"Python: Dependency resolution failed", "dependency"},
+ 162: {"Python: Installation aborted (EXTERNALLY-MANAGED)", "dependency"},
+
+ // --- PostgreSQL (170-173) ---
+ 170: {"PostgreSQL: Connection failed", "database"},
+ 171: {"PostgreSQL: Authentication failed", "database"},
+ 172: {"PostgreSQL: Database does not exist", "database"},
+ 173: {"PostgreSQL: Fatal error in query", "database"},
+
+ // --- MySQL / MariaDB (180-183) ---
+ 180: {"MySQL/MariaDB: Connection failed", "database"},
+ 181: {"MySQL/MariaDB: Authentication failed", "database"},
+ 182: {"MySQL/MariaDB: Database does not exist", "database"},
+ 183: {"MySQL/MariaDB: Fatal error in query", "database"},
+
+ // --- MongoDB (190-193) ---
+ 190: {"MongoDB: Connection failed", "database"},
+ 191: {"MongoDB: Authentication failed", "database"},
+ 192: {"MongoDB: Database not found", "database"},
+ 193: {"MongoDB: Fatal query error", "database"},
+
+ // --- Proxmox Custom Codes (200-231) ---
+ 200: {"Proxmox: Failed to create lock file", "proxmox"},
+ 203: {"Proxmox: Missing CTID variable", "config"},
+ 204: {"Proxmox: Missing PCT_OSTYPE variable", "config"},
+ 205: {"Proxmox: Invalid CTID (<100)", "config"},
+ 206: {"Proxmox: CTID already in use", "config"},
+ 207: {"Proxmox: Password contains unescaped special chars", "config"},
+ 208: {"Proxmox: Invalid configuration (DNS/MAC/Network)", "config"},
+ 209: {"Proxmox: Container creation failed", "proxmox"},
+ 210: {"Proxmox: Cluster not quorate", "proxmox"},
+ 211: {"Proxmox: Timeout waiting for template lock", "timeout"},
+ 212: {"Proxmox: Storage 'iscsidirect' does not support containers", "proxmox"},
+ 213: {"Proxmox: Storage does not support 'rootdir' content", "proxmox"},
+ 214: {"Proxmox: Not enough storage space", "storage"},
+ 215: {"Proxmox: Container created but not listed (ghost state)", "proxmox"},
+ 216: {"Proxmox: RootFS entry missing in config", "proxmox"},
+ 217: {"Proxmox: Storage not accessible", "storage"},
+ 218: {"Proxmox: Template file corrupted or incomplete", "proxmox"},
+ 219: {"Proxmox: CephFS does not support containers", "storage"},
+ 220: {"Proxmox: Unable to resolve template path", "proxmox"},
+ 221: {"Proxmox: Template file not readable", "proxmox"},
+ 222: {"Proxmox: Template download failed", "proxmox"},
+ 223: {"Proxmox: Template not available after download", "proxmox"},
+ 224: {"Proxmox: PBS storage is for backups only", "storage"},
+ 225: {"Proxmox: No template available for OS/Version", "proxmox"},
+ 226: {"Proxmox: VM disk import or post-creation setup failed", "proxmox"},
+ 231: {"Proxmox: LXC stack upgrade failed", "proxmox"},
+
+ // --- Tools & Addon Scripts (232-238) ---
+ 232: {"Tools: Wrong execution environment", "config"},
+ 233: {"Tools: Application not installed (update prerequisite missing)", "config"},
+ 234: {"Tools: No LXC containers found", "proxmox"},
+ 235: {"Tools: Backup or restore operation failed", "storage"},
+ 236: {"Tools: Required hardware not detected", "config"},
+ 237: {"Tools: Dependency package installation failed", "dependency"},
+ 238: {"Tools: OS or distribution not supported", "config"},
+
+ // --- Node.js / npm (239-249) ---
+ 239: {"npm/Node.js: Unexpected runtime error", "dependency"},
+ 243: {"Node.js: Out of memory (heap overflow)", "resource"},
+ 245: {"Node.js: Invalid command-line option", "config"},
+ 246: {"Node.js: Internal JavaScript Parse Error", "unknown"},
+ 247: {"Node.js: Fatal internal error", "unknown"},
+ 248: {"Node.js: Invalid C++ addon / N-API failure", "unknown"},
+ 249: {"npm/pnpm/yarn: Unknown fatal error", "unknown"},
+
+ // --- Application Install/Update Errors (250-254) ---
+ 250: {"App: Download failed or version not determined", "network"},
+ 251: {"App: File extraction failed (corrupt/incomplete)", "storage"},
+ 252: {"App: Required file or resource not found", "unknown"},
+ 253: {"App: Data migration required — update aborted", "config"},
+ 254: {"App: User declined prompt or input timed out", "user_aborted"},
+
+ // --- DPKG ---
+ 255: {"DPKG: Fatal internal error / set -e triggered", "apt"},
}
)
+// getExitCodeDescription returns the human-readable description for an exit code.
+// Falls back to signal-based description for codes 128-191, or "Unknown" otherwise.
+func getExitCodeDescription(code int) string {
+ if info, ok := exitCodeInfo[code]; ok {
+ return info.Desc
+ }
+ if code > 128 && code < 192 {
+ sigNum := code - 128
+ sigNames := map[int]string{
+ 1: "SIGHUP", 2: "SIGINT", 3: "SIGQUIT", 6: "SIGABRT",
+ 9: "SIGKILL", 11: "SIGSEGV", 13: "SIGPIPE", 15: "SIGTERM",
+ 24: "SIGXCPU", 25: "SIGXFSZ",
+ }
+ if name, ok := sigNames[sigNum]; ok {
+ return fmt.Sprintf("Killed by %s (signal %d)", name, sigNum)
+ }
+ return fmt.Sprintf("Killed by signal %d", sigNum)
+ }
+ return fmt.Sprintf("Unknown (exit code %d)", code)
+}
+
+// getExitCodeCategory returns the error category for an exit code.
+// Falls back to "signal" for codes 128-191, or "unknown" otherwise.
+func getExitCodeCategory(code int) string {
+ if info, ok := exitCodeInfo[code]; ok {
+ return info.Category
+ }
+ if code > 128 && code < 192 {
+ return "signal"
+ }
+ return "unknown"
+}
+
func sanitizeShort(s string, max int) string {
s = strings.TrimSpace(s)
if s == "" {
@@ -1624,7 +1779,12 @@ func main() {
// API: Get exit code descriptions (static reference data)
mux.HandleFunc("/api/exit-codes", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(exitCodeDescriptions)
+ // Build a simple map[int]string from the unified exitCodeInfo
+ descs := make(map[int]string, len(exitCodeInfo))
+ for code, info := range exitCodeInfo {
+ descs[code] = info.Desc
+ }
+ json.NewEncoder(w).Encode(descs)
})
// Serve static files from the /public/static directory
@@ -1866,7 +2026,7 @@ func main() {
// Auto-categorize errors based on exit code when no category provided
if in.Status == "failed" && (in.ErrorCategory == "" || in.ErrorCategory == "unknown") {
- if cat, ok := exitCodeCategories[in.ExitCode]; ok {
+ if cat := getExitCodeCategory(in.ExitCode); cat != "unknown" {
in.ErrorCategory = cat
}
}
@@ -1878,13 +2038,7 @@ func main() {
// Enrich error text with exit code description if error text is empty
if in.Status == "failed" && in.Error == "" && in.ExitCode != 0 {
- if desc, ok := exitCodeDescriptions[in.ExitCode]; ok {
- in.Error = fmt.Sprintf("Exit code %d: %s", in.ExitCode, desc)
- } else if in.ExitCode > 128 && in.ExitCode < 192 {
- in.Error = fmt.Sprintf("Exit code %d: Killed by signal %d", in.ExitCode, in.ExitCode-128)
- } else {
- in.Error = fmt.Sprintf("Exit code %d: Unknown error", in.ExitCode)
- }
+ in.Error = fmt.Sprintf("Exit code %d: %s", in.ExitCode, getExitCodeDescription(in.ExitCode))
}
// Map input to PocketBase schema
@@ -2029,15 +2183,10 @@ func securityHeaders(next http.Handler) http.Handler {
})
}
-// categorizeExitCode returns a short human-readable category for an exit code
+// categorizeExitCode returns a short human-readable description for an exit code.
+// Delegates to getExitCodeDescription (single source of truth).
func categorizeExitCode(code int) string {
- if desc, ok := exitCodeDescriptions[code]; ok {
- return desc
- }
- if code > 128 && code < 192 {
- return fmt.Sprintf("Killed by signal %d", code-128)
- }
- return fmt.Sprintf("Unknown (exit code %d)", code)
+ return getExitCodeDescription(code)
}
// createGitHubIssue creates a new issue in the specified GitHub repository