Show fresh player capacity on Fleet cards
This commit is contained in:
@@ -23,6 +23,8 @@ export interface FleetServer {
|
||||
map_slug?: string | null;
|
||||
game_mode?: string | null;
|
||||
last_players: number | null;
|
||||
capacity?: number | null;
|
||||
capacity_observed_at?: string | null;
|
||||
last_map: string | null;
|
||||
last_gametype: string | null;
|
||||
last_seen_at: string | null;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { freshStatusTimestamp } from './server-freshness.js';
|
||||
|
||||
export interface PlayerCapacityObservation {
|
||||
capacity: number | null;
|
||||
capacity_observed_at: string | null;
|
||||
}
|
||||
|
||||
/** A newer roster must never renew the capacity reported by an older process. */
|
||||
export function currentCapacity(server: Partial<PlayerCapacityObservation> & { telemetry_after?: string | null }, now: number): number | null {
|
||||
const value = server.capacity;
|
||||
const observedAt = server.capacity_observed_at;
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0
|
||||
|| !freshStatusTimestamp(observedAt ?? null, now)) return null;
|
||||
if (server.telemetry_after && !(Date.parse(observedAt!) > Date.parse(server.telemetry_after))) return null;
|
||||
return value;
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { freshStatusTimestamp } from '$lib/server-freshness.js';
|
||||
import { pollVisible } from '../poll.js';
|
||||
import { selectServerObservation } from '$lib/server-observation.js';
|
||||
import { currentCapacity } from '$lib/server-capacity.js';
|
||||
import { derived, writable, get } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import { statusByServer, statusReceivedAt, syncServerTime, serverTimeNow, matchEventsByServer, telemetryByServer, uiNow } from './servers.js';
|
||||
@@ -19,7 +20,7 @@ export function createFleetStore(serverName?: string) {
|
||||
const freshnessPhase = derived([fleet, statusByServer, telemetryByServer, uiNow], ([snapshot, live, reports, now]) => {
|
||||
const times = snapshot.servers.flatMap(server => {
|
||||
const status = live.get(server.name);
|
||||
return [status?.lastSeenAt, status?.containerCheckedAt, status?.rconStatus?.last_seen_at, status?.telemetry?.receivedAt,
|
||||
return [status?.capacityObservedAt, server.capacity_observed_at, status?.lastSeenAt, status?.containerCheckedAt, status?.rconStatus?.last_seen_at, status?.telemetry?.receivedAt,
|
||||
server.last_seen_at, server.container_checked_at, server.last_checked_at, server.rcon_status?.last_seen_at,
|
||||
server.telemetry?.receivedAt, reports.get(server.name)?.receivedAt];
|
||||
});
|
||||
@@ -32,7 +33,7 @@ export function createFleetStore(serverName?: string) {
|
||||
const status = (candidate?.receivedAt ?? received) > snapshot.receivedAt ? candidate : undefined;
|
||||
const match = matches.get(server.name);
|
||||
const report = reports.get(server.name);
|
||||
const times = [status?.lastSeenAt, status?.containerCheckedAt, status?.rconStatus?.last_seen_at,
|
||||
const times = [status?.capacityObservedAt, server.capacity_observed_at, status?.lastSeenAt, status?.containerCheckedAt, status?.rconStatus?.last_seen_at,
|
||||
status?.telemetry?.receivedAt, server.last_seen_at, server.container_checked_at, server.last_checked_at,
|
||||
server.rcon_status?.last_seen_at, server.telemetry?.receivedAt, report?.receivedAt];
|
||||
const phase = times.map(time => `${freshStatusTimestamp(time ?? null, now)}:${now - Date.parse(time ?? '') >= 0 && now - Date.parse(time ?? '') <= 120_000}`).join(',');
|
||||
@@ -40,6 +41,7 @@ export function createFleetStore(serverName?: string) {
|
||||
if (saved && saved.server === server && saved.status === status && saved.match === match && saved.report === report && saved.phase === phase) return saved.value;
|
||||
const remember = (value: FleetServer) => { observed.set(server.name, { server, status, match, report, phase, value }); return value; };
|
||||
const base = status ? { ...server, last_players: status.players, last_map: status.map,
|
||||
capacity: status.capacity ?? null, capacity_observed_at: status.capacityObservedAt ?? null,
|
||||
last_gametype: status.gametype, last_seen_at: status.lastSeenAt, last_error: status.lastError,
|
||||
roster: status.roster, health: status.health, endpoint: status.endpoint, replySnippet: status.replySnippet, active_since: status.activeSince,
|
||||
inactive_since: status.inactiveSince, game_started_at: status.gameStartedAt,
|
||||
@@ -48,6 +50,8 @@ export function createFleetStore(serverName?: string) {
|
||||
telemetry_after: status.telemetryAfter ?? server.telemetry_after, rcon_status: status.rconStatus,
|
||||
telemetry: status.telemetry ?? server.telemetry } : server;
|
||||
const current = selectServerObservation(base, reports.get(server.name), now);
|
||||
current.capacity = currentCapacity(current, now);
|
||||
if (current.capacity === null) current.capacity_observed_at = null;
|
||||
// Apply lifecycle events immediately, until a newer status observes them
|
||||
// or confirms that the lobby has emptied.
|
||||
if (!match || current.health !== 'online'
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface ServerStatus {
|
||||
rconStatus?: GameObservation;
|
||||
telemetry?: GameTelemetry;
|
||||
players: number | null;
|
||||
capacity?: number | null;
|
||||
capacityObservedAt?: string | null;
|
||||
map: string | null;
|
||||
gametype: string | null;
|
||||
roster: RosterPlayer[] | null;
|
||||
|
||||
Reference in New Issue
Block a user