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
This commit is contained in:
CanbiZ (MickLesk)
2026-03-02 13:42:42 +01:00
parent 56192f09bd
commit a0a17a2e17
6 changed files with 277 additions and 425 deletions
+2 -333
View File
@@ -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,
+23
View File
@@ -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;
+10 -1
View File
@@ -520,7 +520,7 @@ function renderTableRows(records) {
currentRecords = records;
if (records.length === 0) {
tbody.innerHTML = '<tr><td colspan="8"><div class="loading" style="padding: 40px;">No records found</div></td></tr>';
tbody.innerHTML = '<tr><td colspan="9"><div class="loading" style="padding: 40px;">No records found</div></td></tr>';
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 = '<span class="exit-code-badge">' + r.exit_code + '</span>';
} else if (r.status === 'aborted' && r.exit_code !== undefined && r.exit_code !== null) {
exitCodeCell = '<span class="exit-code-badge aborted">' + r.exit_code + '</span>';
}
return '<tr class="clickable-row" onclick="showRecordDetail(' + index + ')">' +
'<td><span class="status-badge ' + statusClass + '">' + escapeHtml(r.status || 'unknown') + '</span></td>' +
'<td>' + exitCodeCell + '</td>' +
'<td><span class="type-badge ' + typeClass + '">' + escapeHtml((r.type || '-').toUpperCase()) + '</span></td>' +
'<td><strong>' + escapeHtml(r.nsapp || '-') + '</strong></td>' +
'<td>' + escapeHtml(osDisplay) + '</td>' +
+2 -1
View File
@@ -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) {
+2 -1
View File
@@ -301,6 +301,7 @@
<thead>
<tr>
<th data-sort="status" class="sortable">Status</th>
<th data-sort="exit_code" class="sortable">Exit Code</th>
<th data-sort="type" class="sortable">Type</th>
<th data-sort="nsapp" class="sortable">Application</th>
<th data-sort="os_type" class="sortable">OS</th>
@@ -311,7 +312,7 @@
</tr>
</thead>
<tbody id="recordsTable">
<tr><td colspan="8"><div class="loading"><div class="loading-spinner"></div>Loading...</div></td></tr>
<tr><td colspan="9"><div class="loading"><div class="loading-spinner"></div>Loading...</div></td></tr>
</tbody>
</table>
</div>
+238 -89
View File
@@ -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