Surface structured RCON failure reasons in the fleet UI
rconGetStatus previously collapsed at least five distinct failure modes (no password, no port, socket error, timeout, bad reply) into a single null return, so the UI could only say "rcon unreachable" — useless when diagnosing a real Alcatraz outage. Refactored to rconStatusResult() returning a discriminated union with a RconFailure reason and human-facing detail. The worker persists these as "<reason>:<detail>" in last_error (no schema change). /api/servers now returns the derived endpoint (host:port) per server plus a top-level integrations block (rcon/komodo configured, timeout, default host). Server detail page gains an RCON diagnostic panel when health is not online: shows the exact endpoint being dialed, integration status, timeout, a color-coded reason tag, the detail, and a targeted fix hint. Wrong-password replies are detected via "bad rcon" text in the print body. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { decodeRconError, rconFixHint } from '../../src/lib/ui/rconError.js';
|
||||
|
||||
describe('decodeRconError', () => {
|
||||
it('splits reason:detail from a well-formed error', () => {
|
||||
expect(decodeRconError('timeout:no reply within 3000ms')).toEqual({
|
||||
reason: 'timeout',
|
||||
detail: 'no reply within 3000ms'
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves colons inside the detail portion', () => {
|
||||
expect(decodeRconError('send-error:ENETUNREACH: no route to host 10.0.0.5:28960')).toEqual({
|
||||
reason: 'send-error',
|
||||
detail: 'ENETUNREACH: no route to host 10.0.0.5:28960'
|
||||
});
|
||||
});
|
||||
|
||||
it('recognizes every known reason code', () => {
|
||||
for (const r of ['no-config', 'no-port', 'send-error', 'timeout', 'bad-reply', 'no-players'] as const) {
|
||||
expect(decodeRconError(`${r}:whatever`)?.reason).toBe(r);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns null for legacy free-text errors', () => {
|
||||
expect(decodeRconError('rcon unreachable')).toBeNull();
|
||||
expect(decodeRconError('unknown-reason:hello')).toBeNull();
|
||||
expect(decodeRconError('')).toBeNull();
|
||||
expect(decodeRconError(null)).toBeNull();
|
||||
expect(decodeRconError(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('rconFixHint', () => {
|
||||
it('returns a non-empty hint for every failure reason', () => {
|
||||
for (const r of ['no-config', 'no-port', 'send-error', 'timeout', 'bad-reply', 'no-players'] as const) {
|
||||
expect(rconFixHint(r).length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user