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 @@ -
Loading...
+
Loading...
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