Remove Komodo backend in favor of native hosts
This commit is contained in:
@@ -165,8 +165,6 @@
|
||||
</div>{/if}
|
||||
{#if integrations?.native && !integrations.hostsReady}
|
||||
<div class="setup-note"><span><strong>Prepare a game host</strong> · {integrations.hostsTotal ? 'Hosts are connected or enrolled but need setup before new deployments.' : 'Add a host, prepare its runtime, and download game files.'}</span><a href="/hosts">Review hosts →</a></div>
|
||||
{:else if $page.data.user?.role === 'admin' && $fleetIntegrations && !integrations.native && (!integrations.komodo || !integrations.rcon)}
|
||||
<div class="setup-note"><span><strong>Finish system setup</strong> · {!integrations.komodo ? 'Komodo' : ''}{!integrations.komodo && !integrations.rcon ? ' and ' : ''}{!integrations.rcon ? 'RCON' : ''} not configured.</span><a href="/settings">Review setup →</a></div>
|
||||
{/if}
|
||||
|
||||
{#if problems.length || attentionOperations.length || starting || provisioning}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { hostApiError, installerCommand, objectBody, requireManagementOrigin } f
|
||||
import { presentHostPreparation } from '$lib/server/hosts/preparation.js';
|
||||
import { config } from '$lib/server/config.js';
|
||||
|
||||
export function GET({ request }: { request: Request }) { requireManagementOrigin(request); return json({ hosts: listHosts().map(presentHostPreparation), defaultRuntimeImage: config.komodo.defaultImage }); }
|
||||
export function GET({ request }: { request: Request }) { requireManagementOrigin(request); return json({ hosts: listHosts().map(presentHostPreparation), defaultRuntimeImage: config.defaultRuntimeImage }); }
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
requireManagementOrigin(request);
|
||||
try {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { kickProvisioning } from '$lib/server/pipeline/worker.js';
|
||||
type Event = { request: Request; params: { id: string } };
|
||||
export function GET({ request, params }: Event) {
|
||||
requireManagementOrigin(request); const host = getHost(params.id); if (!host) error(404, 'Host not found.');
|
||||
return json({ host: presentHostPreparation(host), instances: hostInstances(host.id), serverNow: Date.now(), calibration: capacityCalibration(host.id), defaultRuntimeImage: config.komodo.defaultImage, deletion: hostDeletionStatus(host.id) });
|
||||
return json({ host: presentHostPreparation(host), instances: hostInstances(host.id), serverNow: Date.now(), calibration: capacityCalibration(host.id), defaultRuntimeImage: config.defaultRuntimeImage, deletion: hostDeletionStatus(host.id) });
|
||||
}
|
||||
export async function PATCH({ request, params }: Event) {
|
||||
requireManagementOrigin(request);
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getServer } from '$lib/server/fleet/servers.js';
|
||||
import { config } from '$lib/server/config.js';
|
||||
import { tcpProbeAll } from '$lib/server/integrations/netProbe.js';
|
||||
|
||||
/**
|
||||
* Multi-target TCP reachability probe. Fires parallel connect attempts at a
|
||||
* fixed matrix of candidate hosts + the configured KOMODO_ADDRESS host, so
|
||||
* we can see at a glance which network paths reach anything and where
|
||||
* packets die (DNS, routing, or refused-at-port).
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ params }) => {
|
||||
const name = params.name;
|
||||
if (!name) return json({ ok: false, error: 'missing server name' }, { status: 400 });
|
||||
|
||||
const row = getServer(name);
|
||||
if (!row) return json({ ok: false, error: `unknown server: ${name}` }, { status: 404 });
|
||||
|
||||
// Parse the configured Komodo URL to extract host+port.
|
||||
let komodoHost: string | null = null;
|
||||
let komodoPort: number | null = null;
|
||||
if (config.komodo.address) {
|
||||
try {
|
||||
const u = new URL(config.komodo.address);
|
||||
komodoHost = u.hostname;
|
||||
komodoPort = u.port ? Number.parseInt(u.port, 10) : u.protocol === 'https:' ? 443 : 80;
|
||||
} catch {
|
||||
/* leave null */
|
||||
}
|
||||
}
|
||||
|
||||
const probePort = komodoPort ?? 9120;
|
||||
|
||||
// Build the trial matrix. Every trial hits the same target port (the
|
||||
// Komodo port) via a different network path, so we can see which paths
|
||||
// reach the same service.
|
||||
const trials: Array<{ label: string; host: string; port: number }> = [
|
||||
{ label: `127.0.0.1:${probePort} (container loopback)`, host: '127.0.0.1', port: probePort },
|
||||
{ label: `host.docker.internal:${probePort} (bridge gateway alias)`, host: 'host.docker.internal', port: probePort },
|
||||
{ label: `172.17.0.1:${probePort} (default bridge gateway)`, host: '172.17.0.1', port: probePort }
|
||||
];
|
||||
|
||||
if (komodoHost && !trials.some((t) => t.host === komodoHost)) {
|
||||
trials.push({
|
||||
label: `${komodoHost}:${probePort} (KOMODO_ADDRESS)`,
|
||||
host: komodoHost,
|
||||
port: probePort
|
||||
});
|
||||
}
|
||||
|
||||
// RCON's default host is worth testing too — even though RCON is UDP,
|
||||
// if TCP to the same host also fails, it's a routing/firewall issue,
|
||||
// not a UDP-specific one.
|
||||
const rconHost = row.rcon_host ?? config.rcon.host;
|
||||
if (rconHost && !trials.some((t) => t.host === rconHost)) {
|
||||
trials.push({
|
||||
label: `${rconHost}:${probePort} (RCON_HOST)`,
|
||||
host: rconHost,
|
||||
port: probePort
|
||||
});
|
||||
}
|
||||
|
||||
const results = await tcpProbeAll(trials, config.httpTimeoutMs);
|
||||
|
||||
return json({
|
||||
ok: true,
|
||||
config: {
|
||||
komodoAddress: config.komodo.address || null,
|
||||
komodoHost,
|
||||
komodoPort,
|
||||
rconHost,
|
||||
probeTimeoutMs: config.httpTimeoutMs
|
||||
},
|
||||
results
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { komodoResolvePortDetailed } from '$lib/server/integrations/komodo.js';
|
||||
import { getServer, setServerField } from '$lib/server/fleet/servers.js';
|
||||
|
||||
/**
|
||||
* Trigger a fresh Komodo GetDeployment lookup for a specific server and
|
||||
* return the structured result — including a snippet of the response so
|
||||
* the UI can render "here's where we found the port" (or "here's what
|
||||
* Komodo returned; extend extractUdpPort for this shape").
|
||||
*
|
||||
* On success, the resolved port is written to servers.game_port so the
|
||||
* next RCON attempt uses it without waiting for another sweep tick.
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ params }) => {
|
||||
const name = params.name;
|
||||
if (!name) return json({ ok: false, reason: 'no-name', detail: 'missing server name' }, { status: 400 });
|
||||
|
||||
const row = getServer(name);
|
||||
if (!row) return json({ ok: false, reason: 'no-name', detail: `unknown server: ${name}` }, { status: 404 });
|
||||
|
||||
const result = await komodoResolvePortDetailed(name);
|
||||
if (result.ok) {
|
||||
setServerField(name, 'game_port', result.port);
|
||||
}
|
||||
return json(result);
|
||||
};
|
||||
@@ -1,59 +1,30 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
|
||||
import { listServers } from '$lib/server/fleet/servers.js';
|
||||
import { listServerKeys } from '$lib/server/fleet/server-keys.js';
|
||||
import { serverObservation } from '$lib/server/fleet/status.js';
|
||||
import { freshStatusTimestamp } from '$lib/server-freshness.js';
|
||||
import { tcpProbe } from '$lib/server/integrations/netProbe.js';
|
||||
import type { SetupCheck, SystemStatus } from '$lib/system-status.js';
|
||||
import { listHosts } from '$lib/server/hosts/index.js';
|
||||
import { listLibraries } from '$lib/server/game-files/index.js';
|
||||
|
||||
export async function GET({ url }: { url: URL }) {
|
||||
export async function GET() {
|
||||
const servers = listServers();
|
||||
const readyServers = servers.filter(server => serverObservation(server).health === 'online').length;
|
||||
const rconServers = servers.filter(server => !server.last_error && freshStatusTimestamp(server.last_seen_at, Date.now()) && server.container_state === 'running').length;
|
||||
const savedKeys = listServerKeys().length;
|
||||
const hosts = listHosts();
|
||||
if (hosts.length || !komodoConfigured()) {
|
||||
const readyHosts = hosts.filter(host => host.readiness === 'ready');
|
||||
const connected = hosts.filter(host => host.connected);
|
||||
const versions = listLibraries().flatMap(game => game.versions.filter(version => version.id === game.latestVersionId));
|
||||
const cached = versions.filter(version => version.caches.some(cache => cache.status === 'ready' && readyHosts.some(host => host.id === cache.hostId)));
|
||||
const runtimes = connected.filter(host => host.inventory.runtimeImages?.length);
|
||||
const checks: SetupCheck[] = [
|
||||
{ id: 'hosts', title: 'Host agents', state: readyHosts.length ? 'ready' : hosts.length ? 'attention' : 'unconfigured', detail: `${connected.length} connected, ${readyHosts.length} deployment-ready, ${hosts.length} enrolled or awaiting enrollment.`, href: '/hosts', action: hosts.length ? 'Review hosts' : 'Add host' },
|
||||
{ id: 'runtime', title: 'Plutainer runtime', state: runtimes.length ? 'ready' : 'attention', detail: runtimes.length ? `${runtimes.length} connected hosts report cached runtime images.` : 'Hosts prepare the configured Plutainer runtime automatically.', href: '/hosts', action: 'View host setup' },
|
||||
{ id: 'game-files', title: 'T5 / T6 game files', state: cached.length ? 'ready' : 'attention', detail: `${versions.length} games have uploaded files; ${cached.length} have their latest files verified on a ready host.`, href: '/game-files', action: 'Manage game files' },
|
||||
{ id: 'rcon', title: 'Agent-local game connection', state: rconServers ? 'ready' : 'unverified', detail: rconServers ? `${rconServers} servers have fresh RCON responses.` : 'The host agent checks RCON locally after deployment. Controller access to game ports is not required.', href: servers.length ? '/' : '/hosts', action: servers.length ? 'Review fleet' : 'Review hosts' },
|
||||
{ id: 'keys', title: 'Saved server keys', state: savedKeys ? 'ready' : 'attention', detail: `${savedKeys} saved ${savedKeys === 1 ? 'key' : 'keys'}. Validity is checked with Plutonium at startup.`, href: '/server-keys', action: 'Manage keys' },
|
||||
{ id: 'first-server', title: 'First server ready', state: readyServers ? 'ready' : 'attention', detail: readyServers ? `${readyServers} of ${servers.length} servers are responding.` : servers.length ? 'Registered servers are awaiting fresh healthy reports. Inspect their host and diagnostics.' : 'Choose a host to launch your first server with the latest game files.', href: servers.length ? '/' : '/provisioning', action: servers.length ? 'Review fleet' : 'Create a server' }
|
||||
];
|
||||
return json({ checkedAt: new Date().toISOString(), checks, readyServers, totalServers: servers.length, savedKeys } satisfies SystemStatus, { headers: { 'cache-control': 'no-store' } });
|
||||
}
|
||||
const missing = [!config.komodo.address && 'KOMODO_ADDRESS', !config.komodo.apiKey && 'KOMODO_API_KEY', !config.komodo.apiSecret && 'KOMODO_API_SECRET'].filter(Boolean);
|
||||
const connection: SetupCheck = { id: 'komodo', title: 'Komodo connection', state: komodoConfigured() ? 'unverified' : 'unconfigured', detail: komodoConfigured() ? 'Credentials configured. Run connection check to verify host reachability.' : `Set ${missing.join(', ')} in the app environment.`, href: '#setup-guide', action: 'Setup instructions' };
|
||||
if (komodoConfigured() && url.searchParams.get('probe') === '1') {
|
||||
try {
|
||||
const address = new URL(config.komodo.address);
|
||||
if (!['http:', 'https:'].includes(address.protocol)) throw new Error('invalid address');
|
||||
const result = await Promise.race([
|
||||
tcpProbe('Komodo', address.hostname, Number(address.port || (address.protocol === 'https:' ? 443 : 80)), 3000),
|
||||
new Promise<null>(resolve => setTimeout(() => resolve(null), 4000))
|
||||
]);
|
||||
const reachable = result?.outcome === 'connected';
|
||||
connection.state = reachable ? 'ready' : 'attention';
|
||||
connection.detail = reachable ? 'Host reachable. This network check does not validate API credentials; deployment previews check launch prerequisites.' : 'Host unreachable from Electric Cherry. Check the address, DNS, port, and network access.';
|
||||
} catch { connection.state = 'attention'; connection.detail = 'Komodo address is invalid or could not be reached. Check KOMODO_ADDRESS.'; }
|
||||
}
|
||||
const readyHosts = hosts.filter(host => host.readiness === 'ready');
|
||||
const connected = hosts.filter(host => host.connected);
|
||||
const versions = listLibraries().flatMap(game => game.versions.filter(version => version.id === game.latestVersionId));
|
||||
const cached = versions.filter(version => version.caches.some(cache => cache.status === 'ready' && readyHosts.some(host => host.id === cache.hostId)));
|
||||
const runtimes = connected.filter(host => host.inventory.runtimeImages?.length);
|
||||
const checks: SetupCheck[] = [
|
||||
connection,
|
||||
{ id: 'target', title: 'Deployment host', state: config.komodo.targetServer ? 'ready' : 'unconfigured', detail: config.komodo.targetServer ? `New servers target ${config.komodo.targetServer}.` : 'Set KOMODO_TARGET_SERVER to the exact host name shown in Komodo.', href: '#setup-guide', action: 'Setup instructions' },
|
||||
{ id: 'rcon', title: 'Game server connection', state: !rconConfigured() ? 'unconfigured' : rconServers ? 'ready' : 'unverified', detail: !rconConfigured() ? 'Set RCON_PASSWORD to the password used by your game servers.' : rconServers ? `${rconServers} servers have fresh successful RCON reports.` : 'RCON password configured. A running server is needed to verify a response.', href: servers.length ? '/' : '#setup-guide', action: servers.length ? 'Review fleet' : 'Setup instructions' },
|
||||
{ id: 'game-files', title: 'Game files on the host', state: 'unverified', detail: 'T5 and T6 game-file volumes must be populated on the Komodo host. Their contents cannot be verified from this app.', href: '#game-files', action: 'Game-file instructions' },
|
||||
{ id: 'keys', title: 'Saved server keys', state: savedKeys ? 'ready' : 'attention', detail: `${savedKeys} saved ${savedKeys === 1 ? 'key' : 'keys'}. Key presence does not confirm validity with Plutonium.`, href: '/server-keys', action: savedKeys ? 'Manage keys' : 'Add a key' },
|
||||
{ id: 'first-server', title: 'First server ready', state: readyServers ? 'ready' : 'attention', detail: readyServers ? `${readyServers} of ${servers.length} servers are responding.` : servers.length ? 'Servers are registered but none has a fresh healthy report. Open diagnostics to investigate.' : 'Create your first server after completing the setup checks.', href: servers.length ? '/' : '/provisioning', action: servers.length ? 'Review fleet' : 'Create a server' }
|
||||
{ id: 'hosts', title: 'Host agents', state: readyHosts.length ? 'ready' : hosts.length ? 'attention' : 'unconfigured', detail: `${connected.length} connected, ${readyHosts.length} deployment-ready, ${hosts.length} enrolled or awaiting enrollment.`, href: '/hosts', action: hosts.length ? 'Review hosts' : 'Add host' },
|
||||
{ id: 'runtime', title: 'Plutainer runtime', state: runtimes.length ? 'ready' : 'attention', detail: runtimes.length ? `${runtimes.length} connected hosts report cached runtime images.` : 'Hosts prepare the configured Plutainer runtime automatically.', href: '/hosts', action: 'View host setup' },
|
||||
{ id: 'game-files', title: 'T5 / T6 game files', state: cached.length ? 'ready' : 'attention', detail: `${versions.length} games have uploaded files; ${cached.length} have their latest files verified on a ready host.`, href: '/game-files', action: 'Manage game files' },
|
||||
{ id: 'rcon', title: 'Agent-local game connection', state: rconServers ? 'ready' : 'unverified', detail: rconServers ? `${rconServers} servers have fresh RCON responses.` : 'The host agent checks RCON locally after deployment. Controller access to game ports is not required.', href: servers.length ? '/' : '/hosts', action: servers.length ? 'Review fleet' : 'Review hosts' },
|
||||
{ id: 'keys', title: 'Saved server keys', state: savedKeys ? 'ready' : 'attention', detail: `${savedKeys} saved ${savedKeys === 1 ? 'key' : 'keys'}. Validity is checked with Plutonium at startup.`, href: '/server-keys', action: 'Manage keys' },
|
||||
{ id: 'first-server', title: 'First server ready', state: readyServers ? 'ready' : 'attention', detail: readyServers ? `${readyServers} of ${servers.length} servers are responding.` : servers.length ? 'Registered servers are awaiting fresh healthy reports. Inspect their host and diagnostics.' : 'Choose a host to launch your first server with the latest game files.', href: servers.length ? '/' : '/provisioning', action: servers.length ? 'Review fleet' : 'Create a server' }
|
||||
];
|
||||
const body: SystemStatus = { checkedAt: new Date().toISOString(), checks, readyServers, totalServers: servers.length, savedKeys };
|
||||
return json(body, { headers: { 'cache-control': 'no-store' } });
|
||||
return json({ checkedAt: new Date().toISOString(), checks, readyServers, totalServers: servers.length, savedKeys } satisfies SystemStatus, { headers: { 'cache-control': 'no-store' } });
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
import ChatPanel from '$ui/components/ChatPanel.svelte';
|
||||
import { gametypeLabel } from '$ui/gametype.js';
|
||||
import { decodeRconError, rconFixHint } from '$ui/rconError.js';
|
||||
import { komodoFixHint, type KomodoPortFailure } from '$ui/komodoError.js';
|
||||
|
||||
import Card from '$ui/components/primitives/Card.svelte';
|
||||
import Badge from '$ui/components/primitives/Badge.svelte';
|
||||
@@ -41,29 +40,6 @@
|
||||
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
|
||||
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
|
||||
|
||||
interface KomodoProbeResult {
|
||||
ok: boolean;
|
||||
port?: number;
|
||||
reason?: KomodoPortFailure;
|
||||
detail?: string;
|
||||
httpStatus?: number;
|
||||
snippet?: string;
|
||||
url?: string;
|
||||
}
|
||||
type NetOutcome = 'connected' | 'refused' | 'timeout' | 'dns-fail' | 'unreachable' | 'error';
|
||||
interface NetTrial {
|
||||
label: string; host: string; port: number;
|
||||
resolvedIp: string | null; outcome: NetOutcome; elapsedMs: number; detail?: string;
|
||||
}
|
||||
interface NetDiagnosis {
|
||||
ok: boolean;
|
||||
config?: {
|
||||
komodoAddress: string | null; komodoHost: string | null; komodoPort: number | null;
|
||||
rconHost: string; probeTimeoutMs: number;
|
||||
};
|
||||
results?: NetTrial[];
|
||||
error?: string;
|
||||
}
|
||||
interface RconProbeResult {
|
||||
ok: boolean;
|
||||
endpoint?: { host: string; port: number } | null;
|
||||
@@ -82,7 +58,6 @@
|
||||
native_instance_id?: string | null; host_id?: string | null; instance_generation?: number | null; game_library_id?: string | null;
|
||||
host?: import('$lib/hosts.js').Host | null;
|
||||
network?: { bindAddress: string; localPort: number; publicAddress: string | null; publicPort: number | null } | null;
|
||||
komodo_type: string; komodo_name: string; komodo_server: string | null;
|
||||
game_port: number | null; rcon_host: string | null;
|
||||
game: string | null; map: string | null;
|
||||
last_players: number | null; last_checked_at: string | null;
|
||||
@@ -98,9 +73,6 @@
|
||||
relPath: string; kind?: string; scriptVersionId: number | null;
|
||||
sha256: string; size: number; updatedAt: string; source: string | null;
|
||||
}
|
||||
interface Integrations {
|
||||
rcon: boolean; komodo: boolean; rconTimeoutMs: number; defaultRconHost: string;
|
||||
}
|
||||
interface DeploymentRow {
|
||||
id: string; action: string; started_at: string; finished_at: string | null;
|
||||
servers: Array<{ server: string; restart_state: string; players: number | null; error: string | null; files_changed: number }>;
|
||||
@@ -108,7 +80,7 @@
|
||||
|
||||
let name = $derived($page.params.name ?? '');
|
||||
const detail = $derived(createFleetStore(name));
|
||||
const { fleet, fleetServers, fleetError, fleetIntegrations, refreshFleet } = $derived(detail);
|
||||
const { fleet, fleetServers, fleetError, refreshFleet } = $derived(detail);
|
||||
$effect(() => {
|
||||
detail.hydrateFleet(data.serverSnapshot);
|
||||
return detail.startFleetMonitor();
|
||||
@@ -117,13 +89,8 @@
|
||||
const visibleFleet = $derived($fleet.receivedAt ? $fleetServers : data.serverSnapshot.servers);
|
||||
let server = $derived((visibleFleet.find(s => s.name === name) as Server | undefined) ?? null);
|
||||
const native = $derived(!!server?.native_instance_id);
|
||||
let integrations = $derived($fleetIntegrations ?? data.serverSnapshot.integrations);
|
||||
let history = $state<DeploymentRow[]>([]);
|
||||
let inventory = $state<InventoryEntry[]>([]);
|
||||
let komodoProbe = $state<KomodoProbeResult | null>(null);
|
||||
let probing = $state(false);
|
||||
let netDiagnosis = $state<NetDiagnosis | null>(null);
|
||||
let diagnosing = $state(false);
|
||||
let rconProbe = $state<RconProbeResult | null>(null);
|
||||
let probingRcon = $state(false);
|
||||
let destroyOpen = $state(false);
|
||||
@@ -171,7 +138,7 @@
|
||||
tabLoading = tab === 'history' || tab === 'inventory';
|
||||
return pollVisible(signal => loadTab(currentName, tab, signal), 10000);
|
||||
});
|
||||
$effect(() => { name; komodoProbe = null; rconProbe = null; netDiagnosis = null; });
|
||||
$effect(() => { name; rconProbe = null; });
|
||||
const fresh = $derived(server ? freshServer(server, $uiNow) : false);
|
||||
|
||||
let effectivePlayers = $derived(fresh ? server?.last_players ?? null : null);
|
||||
@@ -214,29 +181,6 @@
|
||||
return `${ep.host}:${ep.port}`;
|
||||
}
|
||||
|
||||
async function probeKomodo(): Promise<void> {
|
||||
if (probing || !name) return;
|
||||
probing = true; komodoProbe = null;
|
||||
try {
|
||||
const res = await fetch(`/api/servers/${encodeURIComponent(name)}/probe-komodo`, { method: 'POST' });
|
||||
komodoProbe = (await res.json()) as KomodoProbeResult;
|
||||
if (komodoProbe.ok && server) {
|
||||
await refreshFleet();
|
||||
}
|
||||
} catch (err) {
|
||||
komodoProbe = { ok: false, reason: 'unreachable', detail: (err as Error).message };
|
||||
} finally { probing = false; }
|
||||
}
|
||||
async function diagnoseNetwork(): Promise<void> {
|
||||
if (diagnosing || !name) return;
|
||||
diagnosing = true; netDiagnosis = null;
|
||||
try {
|
||||
const res = await fetch(`/api/servers/${encodeURIComponent(name)}/diagnose-network`, { method: 'POST' });
|
||||
netDiagnosis = (await res.json()) as NetDiagnosis;
|
||||
} catch (err) {
|
||||
netDiagnosis = { ok: false, error: (err as Error).message };
|
||||
} finally { diagnosing = false; }
|
||||
}
|
||||
async function probeRcon(): Promise<void> {
|
||||
if (probingRcon || !name) return;
|
||||
probingRcon = true; rconProbe = null;
|
||||
@@ -337,8 +281,8 @@
|
||||
{#if $fleetError}<div class="ui-error" role="alert">{$fleetError}<Button size="sm" onclick={() => refreshFleet(true)}>Retry</Button></div>{/if}
|
||||
{#if hasDiagnosticIssue}
|
||||
<section class="connection-issue" aria-label="Server connection issue">
|
||||
<div><strong>{effectiveHealth === 'stale' ? 'Server report is outdated' : 'Server is not responding'}</strong><p>Player count is unknown. {effectiveSeen ? `Last successful report ${ago(effectiveSeen, $uiNow)}.` : 'No successful report has been received.'} {native && !server.host?.connected ? 'The host agent is offline; the game may still be running.' : !native && integrations && !integrations.rcon ? 'RCON is not configured.' : ''}</p></div>
|
||||
<div class="issue-actions"><Button size="sm" onclick={() => changeTab('diagnostics')}>Diagnose connection</Button><Button size="sm" variant="ghost" onclick={() => changeTab('logs')}>Open logs</Button>{#if native && server.host_id}<Button size="sm" href={`/hosts/${server.host_id}`}>Check host</Button>{:else if integrations && (!integrations.rcon || !integrations.komodo)}<Button size="sm" href="/settings">Check system setup</Button>{/if}</div>
|
||||
<div><strong>{effectiveHealth === 'stale' ? 'Server report is outdated' : 'Server is not responding'}</strong><p>Player count is unknown. {effectiveSeen ? `Last successful report ${ago(effectiveSeen, $uiNow)}.` : 'No successful report has been received.'} {native && !server.host?.connected ? 'The host agent is offline; the game may still be running.' : ''}</p></div>
|
||||
<div class="issue-actions"><Button size="sm" onclick={() => changeTab('diagnostics')}>Diagnose connection</Button><Button size="sm" variant="ghost" onclick={() => changeTab('logs')}>Open logs</Button>{#if native && server.host_id}<Button size="sm" href={`/hosts/${server.host_id}`}>Check host</Button>{:else}<Button size="sm" href="/settings">Check system setup</Button>{/if}</div>
|
||||
</section>
|
||||
{/if}
|
||||
<div class="tabs-row">
|
||||
@@ -410,13 +354,6 @@
|
||||
<p class="sub">Current player count: {server.status_source === 'telemetry' ? 'game telemetry' : server.status_source === 'rcon' ? 'RCON' : 'unavailable'}. RCON probes below show that connection's separate results.</p>
|
||||
{#if native}
|
||||
<div class="diag-grid"><div class="kv"><span class="k">Game connection</span><span class="v">RCON runs locally through the host agent</span></div><div class="kv"><span class="k">Host agent</span><span class="v {server.host?.connected ? 'ok' : 'bad'}">{server.host?.connectionState ?? 'unknown'}</span></div><div class="kv"><span class="k">Last heartbeat</span><span class="v">{server.host?.lastHeartbeat ? ago(server.host.lastHeartbeat, $uiNow) : 'Not received'}</span></div></div>
|
||||
{:else if integrations}
|
||||
<div class="diag-grid">
|
||||
<div class="kv"><span class="k">Endpoint</span><span class="v mono">{endpointLabel(effectiveEndpoint)}</span></div>
|
||||
<div class="kv"><span class="k">RCON password</span><span class="v mono {integrations.rcon ? 'ok' : 'bad'}">{integrations.rcon ? 'configured' : 'missing'}</span></div>
|
||||
<div class="kv"><span class="k">Komodo</span><span class="v mono {integrations.komodo ? 'ok' : 'bad'}">{integrations.komodo ? 'configured' : 'missing'}</span></div>
|
||||
<div class="kv"><span class="k">Timeout</span><span class="v mono">{integrations.rconTimeoutMs} ms</span></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if effectiveReason}
|
||||
@@ -445,14 +382,7 @@
|
||||
{native ? 'Probe RCON through agent' : 'Probe RCON'}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !native && !effectiveEndpoint && integrations?.komodo}
|
||||
<Button variant="secondary" size="sm" onclick={probeKomodo} disabled={probing} loading={probing}>
|
||||
Probe Komodo
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !native}<Button variant="secondary" size="sm" onclick={diagnoseNetwork} disabled={diagnosing} loading={diagnosing}>
|
||||
Diagnose network
|
||||
</Button>{:else if server.host_id}<Button variant="secondary" size="sm" href={`/hosts/${server.host_id}`}>Inspect host</Button>{/if}
|
||||
{#if server.host_id}<Button variant="secondary" size="sm" href={`/hosts/${server.host_id}`}>Inspect host</Button>{/if}
|
||||
</div>
|
||||
|
||||
{#if rconProbe?.ok}
|
||||
@@ -466,91 +396,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !native && netDiagnosis}
|
||||
{#if netDiagnosis.ok && netDiagnosis.results}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard users need to scroll the table.) -->
|
||||
<div class="table-scroll" role="region" aria-label="Network diagnostics" tabindex="0">
|
||||
<table class="net-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Trial</th><th>Resolved</th><th>Outcome</th>
|
||||
<th class="num">Elapsed</th><th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each netDiagnosis.results as r (r.label)}
|
||||
<tr>
|
||||
<td class="mono">{r.label}</td>
|
||||
<td class="mono">{r.resolvedIp ?? '—'}</td>
|
||||
<td>
|
||||
<Badge
|
||||
variant={r.outcome === 'connected' ? 'online' :
|
||||
r.outcome === 'refused' ? 'warning' :
|
||||
'danger'}
|
||||
uppercase
|
||||
>{r.outcome}</Badge>
|
||||
</td>
|
||||
<td class="mono num">{r.elapsedMs} ms</td>
|
||||
<td class="mono detail-cell">{r.detail ?? ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{#if netDiagnosis.config}
|
||||
<div class="net-meta">
|
||||
KOMODO_ADDRESS = <code>{netDiagnosis.config.komodoAddress ?? '(unset)'}</code>
|
||||
· RCON_HOST = <code>{netDiagnosis.config.rconHost}</code>
|
||||
· probe timeout = <code>{netDiagnosis.config.probeTimeoutMs} ms</code>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="alert alert-danger">
|
||||
<Badge variant="danger" uppercase>Error</Badge>
|
||||
<span class="alert-detail">{netDiagnosis.error ?? 'network diagnostic failed'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !native && komodoProbe}
|
||||
{#if komodoProbe.ok}
|
||||
<div class="alert alert-ok">
|
||||
<Badge variant="online" uppercase>Resolved</Badge>
|
||||
<span class="alert-detail">
|
||||
Port <strong>{komodoProbe.port}</strong> cached — RCON will retry on the next tick.
|
||||
</span>
|
||||
</div>
|
||||
{#if komodoProbe.snippet}
|
||||
<details class="snippet-details">
|
||||
<summary>Show response snippet</summary>
|
||||
<pre class="snippet">{komodoProbe.snippet}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="alert alert-danger">
|
||||
<Badge variant="danger" uppercase>{komodoProbe.reason}</Badge>
|
||||
<span class="alert-detail">
|
||||
{komodoProbe.detail}{#if komodoProbe.httpStatus} · HTTP {komodoProbe.httpStatus}{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if komodoProbe.url}
|
||||
<div class="url-row"><span class="k">Tried</span><code>{komodoProbe.url}</code></div>
|
||||
{/if}
|
||||
{#if komodoProbe.reason}
|
||||
<div class="hint">{komodoFixHint(komodoProbe.reason, komodoProbe.httpStatus)}</div>
|
||||
{/if}
|
||||
{#if komodoProbe.snippet}
|
||||
<pre class="snippet">{komodoProbe.snippet}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="panel-head divider">
|
||||
<h2>Configuration</h2>
|
||||
</div>
|
||||
<div class="diag-grid">
|
||||
{#if native}<div class="kv"><span class="k">Host</span><span class="v"><a href={`/hosts/${server.host_id}`}>{server.host?.name ?? server.host_id}</a></span></div><div class="kv"><span class="k">Instance</span><span class="v mono">{server.native_instance_id}</span></div><div class="kv"><span class="k">Generation</span><span class="v mono">{server.instance_generation}</span></div><div class="kv"><span class="k">Game-file version</span><span class="v mono"><a href="/game-files">{server.game_library_id}</a></span></div>{#if server.network}<div class="kv"><span class="k">Local UDP binding</span><span class="v mono">{server.network.bindAddress}:{server.network.localPort}</span></div><div class="kv"><span class="k">Public / NAT endpoint</span><span class="v mono">{server.network.publicAddress || server.host?.publicAddress || server.host?.hostname || 'Not configured'}:{server.network.publicPort ?? server.network.localPort}</span></div>{/if}
|
||||
{:else}<div class="kv"><span class="k">Komodo</span><span class="v mono">{server.komodo_type} · {server.komodo_name}</span></div><div class="kv"><span class="k">RCON override</span><span class="v mono">{server.rcon_host ?? '(default)'}</span></div>{/if}
|
||||
{/if}
|
||||
<div class="kv"><span class="k">Game port</span><span class="v mono">{server.game_port ?? '(unresolved)'}</span></div>
|
||||
<div class="kv"><span class="k">Game</span><span class="v mono">{server.game ?? '(unset)'}</span></div>
|
||||
<div class="kv"><span class="k">Map</span><span class="v mono">{server.map ?? '(unset)'}</span></div>
|
||||
@@ -777,19 +628,6 @@
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.url-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--surface-sunken);
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.url-row .k { color: var(--text-tertiary); }
|
||||
.url-row code { color: var(--text-primary); word-break: break-all; }
|
||||
|
||||
/* Snippet details */
|
||||
.snippet-details {
|
||||
margin-top: var(--space-3);
|
||||
@@ -818,60 +656,6 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Network table */
|
||||
.table-scroll { max-width: 100%; overflow-x: auto; }
|
||||
.table-scroll:focus-visible { box-shadow: var(--focus-ring); }
|
||||
.net-table {
|
||||
min-width: 600px;
|
||||
width: 100%;
|
||||
margin-top: var(--space-4);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: var(--surface-sunken);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.net-table thead th {
|
||||
text-align: left;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--text-tertiary);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
.net-table tbody td {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
color: var(--text-primary);
|
||||
vertical-align: top;
|
||||
}
|
||||
.net-table tbody tr:last-child td { border-bottom: none; }
|
||||
.net-table .num { text-align: right; }
|
||||
.net-table .detail-cell {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
word-break: break-word;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.net-meta {
|
||||
margin-top: var(--space-3);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.net-meta code {
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-sunken);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Inventory */
|
||||
.inv-list {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user