mirror of
https://github.com/Heretek-AI/telemetry-service.git
synced 2026-07-01 13:54:38 -04:00
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:
+2
-333
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>' +
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user