Show fresh player capacity on Fleet cards

This commit is contained in:
2026-10-07 20:16:42 -04:00
parent 5b3e8cb1a3
commit 6146bef51d
14 changed files with 234 additions and 7 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ interface Record {
/** A pass shares its reads; each asynchronous evidence boundary refreshes in one joined query. */
export class MonitoringSnapshot {
private records = new Map<string, Record>();
constructor() { ensureHostSchema(db()); this.refresh(); }
constructor(serverName?: string) { ensureHostSchema(db()); this.refresh(serverName); }
refresh(server?: string): void {
const rows = server === undefined ? db().prepare(sql + ' WHERE s.destroyed_at IS NULL ORDER BY s.name').all() as Row[]
: db().prepare(sql + ' WHERE s.name=? AND s.destroyed_at IS NULL').all(server) as Row[];
+18
View File
@@ -0,0 +1,18 @@
import type { PlayerCapacityObservation } from '../../server-capacity.js';
import { monitoringStatus } from './health-observations.js';
import { MonitoringSnapshot } from './monitoring-snapshot.js';
/** Read only the accepted, current-instance sample; never dispatch a probe. */
export function observedPlayerCapacity(server: string, snapshot: MonitoringSnapshot): PlayerCapacityObservation {
const game = monitoringStatus(server, snapshot)?.game;
const capacity = game?.result.ok ? game.result.status.capacity : null;
return typeof capacity === 'number' && Number.isSafeInteger(capacity) && capacity > 0
? { capacity, capacity_observed_at: game!.observedAt }
: { capacity: null, capacity_observed_at: null };
}
/** One joined identity read for a fleet, or a single server on a detail route. */
export function playerCapacityReader(serverName?: string) {
const snapshot = new MonitoringSnapshot(serverName);
return (server: string) => observedPlayerCapacity(server, snapshot);
}
+3
View File
@@ -7,6 +7,7 @@ import { listHosts } from '../hosts/index.js';
import type { NativeInstance } from '../backends/native.js';
import { serverOperations } from './overview.js';
import { hostListStatus } from '../../ui/host-overview.js';
import { playerCapacityReader } from './player-capacity.js';
export function fleetSnapshot(serverName?: string) {
const bindings = serverName === undefined ? [] : [serverName];
@@ -27,12 +28,14 @@ export function fleetSnapshot(serverName?: string) {
const entries = holds.get(row.server) ?? []; entries.push({ target: row.target, betaId: row.betaId }); holds.set(row.server, entries);
}
const observe = serverName === undefined ? observationReader() : serverObservation;
const capacity = playerCapacityReader(serverName);
const selected = serverName === undefined ? undefined : getServer(serverName);
const rows = serverName === undefined ? listServers() : selected && !selected.destroyed_at ? [selected] : [];
const servers = rows.map((s) => {
const instance = s.native_instance_id ? instances.get(s.name) : null;
return {
...observe(s),
...capacity(s.name),
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),
+2
View File
@@ -16,6 +16,8 @@ export interface ServerStatusEntry {
rconStatus?: GameObservation;
telemetry?: GameTelemetry;
players: number | null;
capacity?: number | null;
capacityObservedAt?: string | null;
map: string | null;
gametype: string | null;
roster: StatusPlayer[] | null;
+6 -3
View File
@@ -19,6 +19,8 @@ import { runMaintenance } from '../maintenance/index.js';
import { cleanupControllerLibraries } from '../game-files/index.js';
import { trackActivity } from './activity.js';
import { MonitoringSnapshot } from '../fleet/monitoring-snapshot.js';
import { observedPlayerCapacity } from '../fleet/player-capacity.js';
import type { PlayerCapacityObservation } from '../../server-capacity.js';
import { withHostRequestContext } from '../hosts/request-context.js';
import { withOperationSource } from '../hosts/operation-context.js';
@@ -125,7 +127,7 @@ function queueStatus(srv: ServerRow, snapshot: MonitoringSnapshot, probe?: () =>
capacity = await withOperationSource('Status monitor', () => refreshServerStatus(server, snapshot, probe));
// Publish this server's latest evidence without waiting for the fleet.
const current = snapshot.get(server)?.server;
if (current) publishStatus(statusEntry(current, snapshot.observe));
if (current) publishStatus(statusEntry(current, snapshot.observe, observedPlayerCapacity(server, snapshot)));
} catch (err) { console.error(`[worker] status error for ${server}:`, err); }
finally { pendingStatus.delete(server); finish(capacity); }
});
@@ -252,9 +254,10 @@ async function refreshServerStatus(server: string, snapshot: MonitoringSnapshot,
return capacity;
}
export function statusEntry(srv: ServerRow, observe = serverObservation): ServerStatusEntry {
export function statusEntry(srv: ServerRow, observe = serverObservation, capacity?: PlayerCapacityObservation): ServerStatusEntry {
const final = observe(srv);
return { server: srv.name, players: final.last_players, map: final.last_map ?? srv.map,
return { server: srv.name, players: final.last_players, capacity: capacity?.capacity ?? null,
capacityObservedAt: capacity?.capacity_observed_at ?? null, map: final.last_map ?? srv.map,
gametype: final.last_gametype, roster: final.roster, health: final.health, lastSeenAt: final.last_seen_at,
lastError: final.last_error, replySnippet: final.replySnippet,
endpoint: srv.game_port ? { host: srv.rcon_host ?? config.rcon.host, port: srv.game_port } : null, reason: final.last_error ? final.last_error.split(':')[0] as RconFailure : null,