Improve application responsiveness and background processing

This commit is contained in:
2026-10-04 19:56:06 -04:00
parent 1392a082da
commit ed004ddbcf
70 changed files with 1324 additions and 345 deletions
+11 -3
View File
@@ -1,9 +1,17 @@
import { json } from '@sveltejs/kit';
import { pendingHostOperations } from '$lib/server/hosts/index.js';
import { pendingHostOperations, waitForHostWork } from '$lib/server/hosts/index.js';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { reconcileHostPreparation } from '$lib/server/hosts/preparation.js';
export function GET({ request }: { request: Request }) {
export async function GET({ request }: { request: Request }) {
const host = authenticateAgent(request);
reconcileHostPreparation(host.id);
return json({ operations: pendingHostOperations(host.id), serverTime: new Date().toISOString() }, { headers: { 'Cache-Control': 'no-store' } });
const params = new URL(request.url).searchParams;
const lane = params.get('lane') === 'read' ? 'read' : params.get('lane') === 'write' ? 'write' : 'all';
let operations = pendingHostOperations(host.id, lane);
if (!operations.length && params.get('wait') === '20') {
await waitForHostWork(host.id, request.signal);
authenticateAgent(request); // Revocation still applies after waiting.
operations = pendingHostOperations(host.id, lane);
}
return json({ operations, serverTime: new Date().toISOString() }, { headers: { 'Cache-Control': 'no-store' } });
}
+19 -6
View File
@@ -1,9 +1,22 @@
import { json } from '@sveltejs/kit';
import { serverTitle } from '$lib/ui/server-labels.js';
import { db } from '$lib/server/db/index.js';
export async function GET({ url }: { url: URL }) {
const activeOnly = url.searchParams.get('active') === 'true';
const historyOnly = url.searchParams.get('view') === 'history';
const server = url.searchParams.get('server');
const query = (url.searchParams.get('q') ?? '').slice(0, 100).toLowerCase();
const limit = Math.min(500, Math.max(1, Math.floor(Number(url.searchParams.get('limit')) || 25)));
const offset = Math.min(1_000_000, Math.max(0, Math.floor(Number(url.searchParams.get('offset')) || 0)));
const active = `EXISTS (SELECT 1 FROM deployment_servers a WHERE a.deployment_id=d.id AND a.server_deleted_at IS NULL AND a.restart_state IN ('pending','failed'))`;
const names = query ? (db().prepare('SELECT DISTINCT server FROM deployment_servers').all() as { server: string }[])
.map(row => row.server).filter(name => `${name} ${serverTitle(name)}`.toLowerCase().includes(query)) : [];
const where = `(? IS NULL OR EXISTS (SELECT 1 FROM deployment_servers target WHERE target.deployment_id=d.id AND target.server=?))
${activeOnly ? `AND ${active}` : historyOnly ? `AND NOT ${active}` : ''}
${query ? `AND EXISTS (SELECT 1 FROM deployment_servers target WHERE target.deployment_id=d.id AND target.server IN (${names.map(() => '?').join(',') || 'NULL'}))` : ''}`;
const parameters = [server, server, ...names];
const { total } = db().prepare(`SELECT COUNT(*) AS total FROM deployments d WHERE ${where}`).get(...parameters) as { total: number };
const deployments = db()
.prepare(
@@ -15,11 +28,11 @@ export async function GET({ url }: { url: URL }) {
LEFT JOIN scripts s ON s.id = d.script_id
LEFT JOIN script_versions sv ON sv.id = d.script_version_id
LEFT JOIN script_versions pv ON pv.id = d.prior_version_id
WHERE (? IS NULL OR EXISTS (SELECT 1 FROM deployment_servers target WHERE target.deployment_id = d.id AND target.server = ?))
ORDER BY d.started_at DESC
LIMIT 500`
WHERE ${where}
ORDER BY d.started_at DESC, d.rowid DESC
LIMIT ? OFFSET ?`
)
.all(server, server) as Array<{
.all(...parameters, limit, offset) as Array<{
id: string;
action: string;
started_at: string;
@@ -36,7 +49,7 @@ export async function GET({ url }: { url: URL }) {
}>;
const ids = deployments.map(deployment => deployment.id);
if (!ids.length) return json({ deployments: [] });
if (!ids.length) return json({ deployments: [], total, limit, offset });
const placeholders = ids.map(() => '?').join(',');
const serversByDep = db()
.prepare(
@@ -95,5 +108,5 @@ export async function GET({ url }: { url: URL }) {
}))
.filter((d) => (activeOnly ? d.servers.some((s) => !s.server_deleted_at && (s.restart_state === 'pending' || s.restart_state === 'failed')) : true));
return json({ deployments: rows });
return json({ deployments: rows, total, limit, offset });
}
+1 -1
View File
@@ -21,7 +21,7 @@ export async function GET() {
event.type === 'server-state' ||
event.type === 'deployment-opened' ||
event.type === 'server-changed' ||
event.type === 'sweep-tick'
event.type === 'sweep-tick' || event.type === 'fleet-invalidated'
) {
send(controller, event);
}
+6
View File
@@ -1,3 +1,6 @@
import { observationReader } from '$lib/server/fleet/status.js';
import { listServers } from '$lib/server/fleet/servers.js';
import { statusEntry } from '$lib/server/pipeline/worker.js';
import { subscribe } from '$lib/server/pipeline/bus.js';
import { listTelemetry } from '$lib/server/fleet/telemetry.js';
@@ -15,7 +18,10 @@ export async function GET() {
controller.enqueue(encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify({ ...event, serverNow: Date.now() })}\n\n`));
}
});
const observation = observationReader();
const observe = (server: ReturnType<typeof listServers>[number]) => statusEntry(server, observation);
controller.enqueue(encoder.encode(': hello\n\n'));
controller.enqueue(encoder.encode(`event: server-status\ndata: ${JSON.stringify({ entries: listServers().map(server => observe(server)), snapshot: true, serverNow: Date.now() })}\n\n`));
controller.enqueue(encoder.encode(`event: server-telemetry\ndata: ${JSON.stringify({ entries: listTelemetry(), snapshot: true, serverNow: Date.now() })}\n\n`));
heartbeat = setInterval(() => controller.enqueue(encoder.encode(': ping\n\n')), HEARTBEAT_MS);
},
+1 -1
View File
@@ -8,7 +8,7 @@ export function GET({ request, params }: Event) {
requireManagementOrigin(request); const host = getHost(params.id); if (!host) error(404, 'Host not found.');
// Payloads and results can contain RCON keys or staged credentials. History
// exposes metadata only; agent APIs remain the only command delivery path.
return json({ host: presentHostPreparation(host), defaultRuntimeImage: config.komodo.defaultImage, deletion: hostDeletionStatus(host.id), operations: listHostOperations(host.id).map(({ payload: _payload, result: _result, ...operation }) => operation) });
return json({ host: presentHostPreparation(host), defaultRuntimeImage: config.komodo.defaultImage, deletion: hostDeletionStatus(host.id), operations: listHostOperations(host.id, 100, true).map(({ payload: _payload, result: _result, ...operation }) => operation) });
}
export async function PATCH({ request, params }: Event) {
requireManagementOrigin(request);
+4 -3
View File
@@ -1,7 +1,8 @@
import { leaderboardReport } from '$lib/server/reports/index.js';
import { json } from '@sveltejs/kit';
import { getLeaderboard, isLeaderboardMetric, MAX_LEADERBOARD_OFFSET } from '$lib/server/fleet/leaderboard.js';
import { isLeaderboardMetric, MAX_LEADERBOARD_OFFSET } from '$lib/server/fleet/leaderboard.js';
export function GET({ url }: { url: URL }) {
export async function GET({ url }: { url: URL }) {
const params = url.searchParams;
const game = params.get('game');
const map = params.get('map');
@@ -20,7 +21,7 @@ export function GET({ url }: { url: URL }) {
|| !Number.isFinite(offset) || offset < 0 || offset > MAX_LEADERBOARD_OFFSET) {
return json({ ok: false, kind: 'bad-request', error: 'Use game t5 or t6, a supported metric, map key up to 1024 characters, q up to 100 characters, limit 1–100, and offset 0–1000000.' }, { status: 400 });
}
return json(getLeaderboard({ game: game ?? undefined, map: map || undefined,
return json(await leaderboardReport({ game: game ?? undefined, map: map || undefined,
metric: metric !== null && isLeaderboardMetric(metric) ? metric : undefined,
q: q ?? undefined, limit, offset }));
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { json } from '@sveltejs/kit';
import { parseMetricsQuery } from '$lib/metrics.js';
import { readMetrics } from '$lib/server/fleet/metrics.js';
import { metricsReport } from '$lib/server/reports/index.js';
export function GET({ url }: { url: URL }) {
return json(readMetrics(parseMetricsQuery(url.searchParams)), {
export async function GET({ url }: { url: URL }) {
return json(await metricsReport(parseMetricsQuery(url.searchParams)), {
headers: { 'cache-control': 'no-store' }
});
}
+2 -40
View File
@@ -1,48 +1,10 @@
import { db } from '$lib/server/db/index.js';
import { fleetSnapshot } from '$lib/server/fleet/snapshot.js';
import { kick } from '$lib/server/pipeline/worker.js';
import { json } from '@sveltejs/kit';
import { listServers, heldByBeta } from '$lib/server/fleet/servers.js';
import { serverObservation } from '$lib/server/fleet/status.js';
import { queueCreateServer } from '$lib/server/provisioning/index.js';
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
import type { GameMode, ZombieGame } from '$lib/server/fleet/map-aliases.js';
import { runningVersions } from '$lib/server/integrations/plutonium.js';
import { listHosts } from '$lib/server/hosts/index.js';
import { nativeInstance } from '$lib/server/backends/native.js';
export async function GET() {
const rconHost = config.rcon.host;
const hosts = listHosts();
const hostsById = new Map(hosts.map(host => [host.id, host]));
const versions = new Map(runningVersions().map(version => [version.server, version]));
const servers = listServers().map((s) => {
const instance = s.native_instance_id ? nativeInstance(s.name) : null;
return {
...serverObservation(s),
host: s.host_id ? hostsById.get(s.host_id) ?? null : null,
network: instance ? { bindAddress: instance.bind_address, localPort: instance.port, publicAddress: instance.public_address, publicPort: instance.public_port } : null,
plutonium: versions.get(s.name),
// Derived endpoint — same logic as endpointFor() but without asking
// Komodo. If we don't have a port yet, endpoint is null.
endpoint: instance ? { host: `agent:${instance.host_id}`, port: instance.port } : s.game_port ? { host: s.rcon_host ?? rconHost, port: s.game_port } : null,
held: [...heldByBeta(s.name).entries()].map(([target, betaId]) => ({ target, betaId }))
};
});
return json({
servers,
serverNow: Date.now(),
reservedNames: (db().prepare("SELECT j.server FROM server_creation_jobs j JOIN server_deployments d ON d.id = j.deployment_id WHERE d.state IN ('queued', 'running', 'awaiting-health')").all() as { server: string }[]).map(row => row.server),
integrations: {
native: hosts.length > 0 || !komodoConfigured(),
hostsReady: hosts.filter(host => host.readiness === 'ready').length,
hostsTotal: hosts.length,
rcon: rconConfigured(),
komodo: komodoConfigured(),
rconTimeoutMs: config.rcon.timeoutMs,
defaultRconHost: rconHost
}
});
}
export function GET() { return json(fleetSnapshot()); }
export async function POST({ request }: { request: Request }) {
const body = (await request.json().catch(() => ({}))) as {