mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 02:42:11 +00:00
321f563a52
Show workload-capable source failures on Workloads and keep matching Proxmox host agents attached to their API source when inventory collection is blocked.
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
/**
|
|
* connectionErrorPresentation
|
|
*
|
|
* Translates raw backend connection errors (often Go error chains exposing
|
|
* implementation detail like 'context deadline exceeded' or
|
|
* 'Client.Timeout exceeded while awaiting headers') into short user-facing
|
|
* messages.
|
|
*
|
|
* The connection-table row already shows the connection name and host, so
|
|
* the humanized message focuses on the failure mode and a short remediation
|
|
* hint without restating identity.
|
|
*
|
|
* For unknown errors we fall back to the raw message stripped of the most
|
|
* common implementation prefixes so it's at least readable.
|
|
*/
|
|
|
|
const POLL_PREFIX_PATTERN = /^poll_[a-z_]+\s+failed\s+on\s+[^:]+:\s*/i;
|
|
// Patterns from common Go error chains: `Get "https://...":`, `Post "..."`.
|
|
const HTTP_VERB_PATTERN = /\b(?:Get|Post|Put|Delete|Patch|Head)\s+"[^"]+":\s*/g;
|
|
|
|
interface HumanizedConnectionError {
|
|
/** Short headline ('Connection timed out', 'Host not found', etc.). */
|
|
headline: string;
|
|
/** One-line remediation hint, or null when none applies. */
|
|
hint: string | null;
|
|
}
|
|
|
|
const TIMEOUT_HINT =
|
|
'Check the host is reachable, the port is correct, and the network path is open.';
|
|
const TLS_HINT =
|
|
'Verify the certificate or pin a fingerprint in TLS settings, or disable TLS verification temporarily for testing.';
|
|
const AUTH_HINT = 'Re-check the API token or username/password.';
|
|
|
|
const HUMANIZED_PATTERNS: { match: RegExp; headline: string; hint: string | null }[] = [
|
|
{
|
|
match: /context deadline exceeded|Client\.Timeout|i\/o timeout|deadline exceeded/i,
|
|
headline: 'Connection timed out',
|
|
hint: TIMEOUT_HINT,
|
|
},
|
|
{
|
|
match: /no such host|dns:.*lookup|name does not resolve/i,
|
|
headline: 'Host not found',
|
|
hint: 'Check the hostname or IP address.',
|
|
},
|
|
{
|
|
match: /connection refused|connect:\s*connection refused/i,
|
|
headline: 'Connection refused',
|
|
hint: 'The host is reachable but rejected the connection on this port. Check the port is correct and the service is running.',
|
|
},
|
|
{
|
|
match: /no route to host|network is unreachable/i,
|
|
headline: 'No network route to host',
|
|
hint: 'The host cannot be reached from this Pulse instance. Check VPN, firewall, or VLAN configuration.',
|
|
},
|
|
{
|
|
match: /x509:.*signed by unknown authority|certificate signed by unknown/i,
|
|
headline: 'TLS certificate not trusted',
|
|
hint: TLS_HINT,
|
|
},
|
|
{
|
|
match: /x509:.*expired|certificate has expired/i,
|
|
headline: 'TLS certificate expired',
|
|
hint: TLS_HINT,
|
|
},
|
|
{
|
|
match: /tls:\s*handshake failure|tls handshake|bad certificate/i,
|
|
headline: 'TLS handshake failed',
|
|
hint: TLS_HINT,
|
|
},
|
|
{
|
|
match: /\b401\b|unauthorized|authentication failed|invalid (?:credentials|api token)/i,
|
|
headline: 'Authentication failed',
|
|
hint: AUTH_HINT,
|
|
},
|
|
{
|
|
match: /\b403\b|forbidden|permission denied/i,
|
|
headline: 'Permission denied',
|
|
hint: 'The credentials connected, but the user/token lacks the required role.',
|
|
},
|
|
{
|
|
match: /\b404\b|not found/i,
|
|
headline: 'Endpoint not found',
|
|
hint: 'Pulse reached the host but the API path is missing. Check the URL path or the platform version.',
|
|
},
|
|
{
|
|
match: /\b5\d{2}\b|server error|internal error/i,
|
|
headline: 'Server error',
|
|
hint: 'The platform returned a server error. Check the platform logs for the underlying cause.',
|
|
},
|
|
];
|
|
|
|
const stripImplementationPrefixes = (raw: string): string =>
|
|
raw
|
|
.replace(POLL_PREFIX_PATTERN, '')
|
|
.replace(HTTP_VERB_PATTERN, '')
|
|
.trim();
|
|
|
|
export const humanizeConnectionError = (raw?: string | null): HumanizedConnectionError | null => {
|
|
if (!raw) return null;
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) return null;
|
|
|
|
for (const pattern of HUMANIZED_PATTERNS) {
|
|
if (pattern.match.test(trimmed)) {
|
|
return { headline: pattern.headline, hint: pattern.hint };
|
|
}
|
|
}
|
|
|
|
// Unknown error: fall back to the raw text with the most common
|
|
// implementation-leaking prefixes peeled off so it's at least readable.
|
|
const cleaned = stripImplementationPrefixes(trimmed);
|
|
return { headline: cleaned || 'Connection error', hint: null };
|
|
};
|
|
|
|
/**
|
|
* Render a single-line message suitable for inline display in a table row.
|
|
* Combines headline and hint when both apply.
|
|
*/
|
|
export const formatConnectionErrorMessage = (raw?: string | null): string | null => {
|
|
const humanized = humanizeConnectionError(raw);
|
|
if (!humanized) return null;
|
|
return humanized.hint ? `${humanized.headline}. ${humanized.hint}` : humanized.headline;
|
|
};
|