626 lines
27 KiB
TypeScript
626 lines
27 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import type Database from 'better-sqlite3';
|
|
import { zombieMapBySlug } from '../src/lib/map-aliases.js';
|
|
|
|
export const FIXTURE_KEY = 'development_fixture_v1';
|
|
export const FIXTURE_VERSION = 'electric-cherry-ui-v1';
|
|
const MINUTE = 60_000;
|
|
const DAY = 24 * 60 * MINUTE;
|
|
const GIB = 1024 ** 3;
|
|
const image = 'fixture.invalid/electric-cherry/preview:local';
|
|
|
|
type Scenario = {
|
|
game: 't5' | 't6';
|
|
slug: string;
|
|
players: number;
|
|
round: number;
|
|
minutes: number;
|
|
state: 'active' | 'idle' | 'stopped' | 'stale' | 'starting' | 'host-offline';
|
|
};
|
|
const scenarios: Scenario[] = [
|
|
{ game: 't5', slug: 'theater', players: 4, round: 27, minutes: 76, state: 'active' },
|
|
{ game: 't5', slug: 'cosmodrome', players: 2, round: 16, minutes: 39, state: 'active' },
|
|
{ game: 't5', slug: 'moon', players: 0, round: 0, minutes: 0, state: 'idle' },
|
|
{ game: 't5', slug: 'temple', players: 3, round: 32, minutes: 84, state: 'active' },
|
|
{ game: 't5', slug: 'pentagon', players: 0, round: 0, minutes: 0, state: 'stopped' },
|
|
{ game: 't5', slug: 'coast', players: 0, round: 0, minutes: 0, state: 'stale' },
|
|
{ game: 't6', slug: 'tomb', players: 3, round: 22, minutes: 58, state: 'active' },
|
|
{ game: 't6', slug: 'town', players: 2, round: 12, minutes: 23, state: 'active' },
|
|
{ game: 't6', slug: 'buried', players: 0, round: 0, minutes: 0, state: 'idle' },
|
|
{ game: 't6', slug: 'prison', players: 4, round: 41, minutes: 119, state: 'active' },
|
|
{ game: 't6', slug: 'nuked', players: 0, round: 0, minutes: 0, state: 'starting' },
|
|
{ game: 't6', slug: 'highrise', players: 0, round: 0, minutes: 0, state: 'host-offline' }
|
|
];
|
|
|
|
export const fixtureServers = scenarios.map((scenario, index) => ({
|
|
...scenario,
|
|
name: `${scenario.game}_zombie_${scenario.slug}_1`,
|
|
host: `fixture-host-${index === 11 ? 3 : (index % 2) + 1}`,
|
|
instance: `fixture-instance-${index + 1}`,
|
|
port: 28960 + index,
|
|
map: zombieMapBySlug(scenario.game, scenario.slug)!
|
|
}));
|
|
const names = [
|
|
'Jamie Cross',
|
|
'Alex',
|
|
'Sam',
|
|
'Morgan',
|
|
'Riley',
|
|
'Taylor',
|
|
'Jordan',
|
|
'Casey',
|
|
'Quinn',
|
|
'Avery',
|
|
'Drew',
|
|
'Kai'
|
|
];
|
|
const iso = (at: number) => new Date(at).toISOString();
|
|
const player = (index: number) => ({
|
|
guid: index === 0 ? 'fixture-jamie' : `fixture-player-${index}`,
|
|
name:
|
|
index === 0
|
|
? names[0]
|
|
: `${names[index % names.length]} ${Math.floor(index / names.length) + 1}`
|
|
});
|
|
const rosterFor = (server: (typeof fixtureServers)[number], index: number) =>
|
|
Array.from({ length: server.players }, (_, slot) => ({
|
|
...player(slot === 0 ? 0 : index * 4 + slot),
|
|
ping: 29 + slot * 7,
|
|
kills: (slot + 1) * 137,
|
|
headshots: (slot + 1) * 41,
|
|
downs: slot % 2,
|
|
revives: slot,
|
|
score: (slot + 1) * 4100
|
|
}));
|
|
const seenAt = (server: Scenario, now: number) =>
|
|
iso(
|
|
now -
|
|
(['stopped', 'host-offline'].includes(server.state)
|
|
? 15 * MINUTE
|
|
: server.state === 'stale'
|
|
? 105_000
|
|
: 0)
|
|
);
|
|
const containerState = (server: Scenario) =>
|
|
server.state === 'stopped' ? 'exited' : server.state === 'starting' ? 'starting' : 'running';
|
|
const instanceStates = (host: string, now: number) =>
|
|
fixtureServers
|
|
.filter((server) => server.host === host)
|
|
.map((server) => ({
|
|
instanceId: server.instance,
|
|
generation: 1,
|
|
state: containerState(server),
|
|
healthy: !['stopped', 'starting'].includes(server.state),
|
|
observedAt: iso(now - (host === 'fixture-host-3' ? 10 * MINUTE : 0))
|
|
}));
|
|
|
|
/** Uses only a caller-owned, fully migrated database. No host or worker code is imported. */
|
|
export function seedFixtureData(handle: Database.Database, now: number): void {
|
|
if (!Number.isFinite(now)) throw new Error('Invalid fixture time.');
|
|
const occupied = [
|
|
'servers',
|
|
'leaderboard_matches',
|
|
'ec_hosts',
|
|
'auth_users',
|
|
'scripts',
|
|
'server_deployments',
|
|
'deployments',
|
|
'betas',
|
|
'game_file_libraries'
|
|
].some((table) => handle.prepare(`SELECT 1 FROM ${table} LIMIT 1`).get());
|
|
if (occupied || handle.prepare('SELECT 1 FROM settings WHERE key=?').get(FIXTURE_KEY))
|
|
throw new Error('Fixture seeding requires a new database.');
|
|
const insert = (table: string, values: Record<string, unknown>) => {
|
|
const keys = Object.keys(values);
|
|
handle
|
|
.prepare(
|
|
`INSERT INTO ${table} (${keys.join(',')}) VALUES (${keys.map(() => '?').join(',')})`
|
|
)
|
|
.run(...Object.values(values));
|
|
};
|
|
const started = iso(now - 30 * DAY);
|
|
const rememberPlayer =
|
|
handle.prepare(`INSERT INTO leaderboard_players(game,guid,name,first_seen_at,last_seen_at) VALUES(?,?,?,?,?)
|
|
ON CONFLICT(game,guid) DO UPDATE SET first_seen_at=MIN(first_seen_at,excluded.first_seen_at),last_seen_at=MAX(last_seen_at,excluded.last_seen_at)`);
|
|
handle.transaction(() => {
|
|
insert('settings', { key: FIXTURE_KEY, value: FIXTURE_VERSION });
|
|
// Preview observations must never opt into automatic lifecycle changes.
|
|
for (const [key, value] of Object.entries({
|
|
maintenance_paused: '1',
|
|
maintenance_health: '0',
|
|
maintenance_drift: '0',
|
|
maintenance_updates: '0',
|
|
watchdog_enabled: '0',
|
|
autoscaling_paused: '1'
|
|
})) {
|
|
handle
|
|
.prepare(
|
|
'INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value'
|
|
)
|
|
.run(key, value);
|
|
}
|
|
for (const [index, name] of [
|
|
'Chicago · Primary',
|
|
'Dallas · Overflow',
|
|
'Frankfurt · Europe'
|
|
].entries()) {
|
|
const id = `fixture-host-${index + 1}`;
|
|
insert('ec_hosts', {
|
|
id,
|
|
name,
|
|
hostname: `host-${index + 1}.fixture.invalid`,
|
|
created_at: started,
|
|
enrolled_at: started,
|
|
last_heartbeat: iso(now - (index === 2 ? 10 * MINUTE : 0)),
|
|
inventory: JSON.stringify({
|
|
protocolVersion: 1,
|
|
agentVersion: '1.7.0',
|
|
cpuCount: 16,
|
|
cpuUsagePercent: 24 + index * 9,
|
|
memoryTotal: 32 * GIB,
|
|
memoryAvailable: (20 - index * 3) * GIB,
|
|
storageTotal: 500 * GIB,
|
|
storageAvailable: 240 * GIB,
|
|
dockerRunning: true,
|
|
directoriesWritable: true,
|
|
runtimeImages: [image],
|
|
libraries: [
|
|
{ id: 'fixture-t5', state: 'ready' },
|
|
{ id: 'fixture-t6', state: 'ready' }
|
|
],
|
|
capabilities: ['instance.heartbeat-state'],
|
|
instanceStates: instanceStates(id, now)
|
|
})
|
|
});
|
|
// Deliberately not a token hash; no bearer token can authenticate as these agents.
|
|
insert('ec_host_credentials', {
|
|
id: `fixture-credential-${index + 1}`,
|
|
host_id: id,
|
|
token_hash: `invalid-fixture-${index + 1}`,
|
|
created_at: started
|
|
});
|
|
for (let minute = 0; minute < 7 * 1440; minute += 15) {
|
|
insert('ec_host_resource_samples', {
|
|
host_id: id,
|
|
sampled_at: iso(now - minute * MINUTE),
|
|
cpu_count: 16,
|
|
cpu_usage_percent: 28 + index * 6 + 10 * Math.sin(minute / 95),
|
|
memory_total: 32 * GIB,
|
|
memory_available: Math.round((18 - index * 2 + Math.sin(minute / 110)) * GIB)
|
|
});
|
|
}
|
|
}
|
|
let liveId = 495;
|
|
for (const [index, server] of fixtureServers.entries()) {
|
|
const beginning = iso(now - server.minutes * MINUTE - 12_000);
|
|
const roster = rosterFor(server, index);
|
|
insert('servers', {
|
|
name: server.name,
|
|
game: server.game,
|
|
map: server.map.canonical,
|
|
map_slug: server.slug,
|
|
game_mode: 'zombie',
|
|
game_port: server.port,
|
|
game_id: server.game === 't5' ? 't5sp' : 't6zm',
|
|
image,
|
|
created_by: 'development fixture',
|
|
created_at: started,
|
|
last_players: server.players,
|
|
last_map: server.map.canonical,
|
|
last_gametype: 'zclassic',
|
|
last_roster: JSON.stringify(roster),
|
|
last_seen_at: seenAt(server, now),
|
|
last_checked_at: iso(now),
|
|
last_error: server.state === 'stale' ? 'timeout: no reply from game server' : null,
|
|
rcon_failures: server.state === 'stale' ? 2 : 0,
|
|
container_state: containerState(server),
|
|
container_checked_at: iso(now),
|
|
desired_state: server.state === 'stopped' ? 'stopped' : 'running',
|
|
active_since: server.players ? beginning : null,
|
|
inactive_since: server.players ? null : iso(now - 5 * MINUTE),
|
|
game_started_at: server.players ? beginning : null
|
|
});
|
|
insert('native_instances', {
|
|
id: server.instance,
|
|
server: server.name,
|
|
host_id: server.host,
|
|
library_id: `fixture-${server.game}`,
|
|
image,
|
|
port: server.port,
|
|
created_at: started
|
|
});
|
|
insert('plutonium_versions', {
|
|
server: server.name,
|
|
revision: 4878,
|
|
checked_at: iso(now),
|
|
error: null
|
|
});
|
|
if (server.state === 'active' || server.state === 'idle') {
|
|
const matchId = server.players ? liveId++ : null;
|
|
const payload = {
|
|
server: server.name,
|
|
map: server.map.canonical,
|
|
event: 'heartbeat',
|
|
round: server.round,
|
|
players: roster,
|
|
receivedAt: iso(now),
|
|
timing: {
|
|
matchId,
|
|
players: Object.fromEntries(
|
|
roster.map((p) => [
|
|
p.guid,
|
|
{
|
|
serverJoinedAt: beginning,
|
|
matchJoinedAt: beginning,
|
|
roundStarted: 1
|
|
}
|
|
])
|
|
)
|
|
}
|
|
};
|
|
insert('server_telemetry', {
|
|
server: server.name,
|
|
round: server.round,
|
|
players: server.players,
|
|
received_at: iso(now),
|
|
payload: JSON.stringify(payload)
|
|
});
|
|
if (matchId !== null) {
|
|
insert('leaderboard_matches', {
|
|
id: matchId,
|
|
server: server.name,
|
|
game: server.game,
|
|
map: server.map.canonical,
|
|
map_key: server.slug,
|
|
map_name: server.map.friendly,
|
|
started_at: beginning,
|
|
last_received_at: iso(now),
|
|
status: 'active',
|
|
start_observed: 1,
|
|
start_players: server.players,
|
|
highest_round: server.round,
|
|
last_event: 'heartbeat',
|
|
last_fingerprint: `fixture-live-${index}`
|
|
});
|
|
for (const p of roster) {
|
|
rememberPlayer.run(server.game, p.guid, p.name, beginning, iso(now));
|
|
insert('leaderboard_match_players', {
|
|
match_id: matchId,
|
|
guid: p.guid,
|
|
highest_round: server.round,
|
|
rounds_survived: server.round,
|
|
kills: p.kills,
|
|
headshots: p.headshots,
|
|
downs: p.downs,
|
|
revives: p.revives,
|
|
best_score: p.score,
|
|
last_round: server.round,
|
|
present: 1,
|
|
has_stats: 1,
|
|
completed: 0,
|
|
first_seen_at: beginning,
|
|
last_seen_at: iso(now),
|
|
first_observed_round: 1,
|
|
present_at_start: 1
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (const phase of ['startup', 'running']) {
|
|
for (let minute = 0; minute < 60; minute++)
|
|
insert('ec_instance_resource_samples', {
|
|
host_id: server.host,
|
|
instance_id: server.instance,
|
|
generation: 1,
|
|
phase,
|
|
bucket: iso(now - minute * MINUTE),
|
|
cpu_cores: phase === 'startup' ? 0.7 + index / 40 : 0.25 + index / 50,
|
|
memory_bytes: Math.round((phase === 'startup' ? 1.1 : 0.7) * GIB)
|
|
});
|
|
}
|
|
}
|
|
for (let index = 0; index < 494; index++) {
|
|
// A large Kino duo cohort exercises ranking and profile pagination.
|
|
const server = fixtureServers[index < 64 ? 0 : index % fixtureServers.length];
|
|
const squad = index < 64 ? 2 : (Math.floor(index / 12) % 4) + 1;
|
|
const unknownStart = index >= 64 && index % 37 === 0;
|
|
const interrupted = index >= 64 && index % 31 === 0;
|
|
const end = now - (180 + index * 37) * MINUTE;
|
|
const beginning = end - (30 + (index % 100)) * MINUTE;
|
|
const round = 10 + ((index * 7) % 56);
|
|
insert('leaderboard_matches', {
|
|
id: index + 1,
|
|
server: server.name,
|
|
game: server.game,
|
|
map: server.map.canonical,
|
|
map_key: server.slug,
|
|
map_name: server.map.friendly,
|
|
started_at: iso(beginning),
|
|
ended_at: iso(end),
|
|
last_received_at: iso(end),
|
|
status: interrupted ? 'interrupted' : 'completed',
|
|
start_observed: unknownStart ? 0 : 1,
|
|
start_players: unknownStart ? null : squad,
|
|
highest_round: round,
|
|
last_event: interrupted ? 'interrupted' : 'game_end',
|
|
last_fingerprint: `fixture-match-${index + 1}`
|
|
});
|
|
for (let slot = 0; slot < squad; slot++) {
|
|
const p = player(slot === 0 ? 0 : ((index + slot * 19) % 80) + 1);
|
|
rememberPlayer.run(server.game, p.guid, p.name, iso(beginning), iso(end));
|
|
insert('leaderboard_match_players', {
|
|
match_id: index + 1,
|
|
guid: p.guid,
|
|
highest_round: round,
|
|
rounds_survived: round - (unknownStart ? 4 : 0),
|
|
kills: round * 35 + slot * 71,
|
|
headshots: round * 9,
|
|
downs: index % 4,
|
|
revives: slot + (index % 3),
|
|
best_score: round * 2100 + slot * 1000,
|
|
last_round: round,
|
|
present: slot > 0 && index % 9 === 0 ? 0 : 1,
|
|
has_stats: 1,
|
|
completed: !interrupted && !(slot > 0 && index % 9 === 0) ? 1 : 0,
|
|
first_seen_at: iso(beginning),
|
|
last_seen_at: iso(end),
|
|
first_observed_round: unknownStart ? 5 : 1,
|
|
present_at_start: unknownStart ? null : 1
|
|
});
|
|
}
|
|
}
|
|
const metric = handle.prepare(
|
|
'INSERT INTO metric_samples(at,server,game,map,players,capacity) VALUES(?,?,?,?,?,4)'
|
|
);
|
|
for (let minute = 0; minute < 7 * 1440; minute++) {
|
|
for (const [index, server] of fixtureServers.entries()) {
|
|
const count = Math.floor((Math.sin(minute / 90 + index) + 1) * 2.49);
|
|
metric.run(
|
|
now - minute * MINUTE,
|
|
server.name,
|
|
server.game,
|
|
server.map.friendly,
|
|
minute % 720 < 8 && index === 5 ? null : count
|
|
);
|
|
}
|
|
}
|
|
for (const scope of ['*', ...[1, 2, 3].map((index) => `fixture-host-${index}`)])
|
|
for (const phase of ['startup', 'running']) {
|
|
insert('ec_resource_allowances', {
|
|
scope,
|
|
phase,
|
|
cpu_cores: phase === 'startup' ? 1 : 0.5,
|
|
memory_bytes: Math.round((phase === 'startup' ? 1.2 : 0.8) * GIB),
|
|
samples: 240,
|
|
window_start: iso(now - DAY),
|
|
observed_at: iso(now),
|
|
computed_at: iso(now),
|
|
usage_margin: 1
|
|
});
|
|
}
|
|
// Real artifact blobs, so details and downloads work without game files.
|
|
for (const [index, kind] of ['script', 'baseline', 'config'].entries()) {
|
|
const content = Buffer.from(
|
|
kind === 'script'
|
|
? '// Synthetic UI fixture; no gameplay behavior.\n'
|
|
: '// Synthetic UI fixture configuration.\nset sv_maxclients 4\n'
|
|
);
|
|
const hash = createHash('sha256').update(content).digest('hex');
|
|
handle
|
|
.prepare('INSERT OR IGNORE INTO file_blobs(sha256,size,content) VALUES(?,?,?)')
|
|
.run(hash, content.length, content);
|
|
insert('scripts', {
|
|
id: index + 1,
|
|
name: [
|
|
'Fixture telemetry',
|
|
'Fixture Black Ops I baseline',
|
|
'Fixture lobby settings'
|
|
][index],
|
|
target_rel_path:
|
|
index === 0 ? 'fixture.gsc' : index === 1 ? 'dedicated_sp.cfg' : 'fixture.cfg',
|
|
scope_type: 'game',
|
|
scope_value: 't5',
|
|
current_version_id: index + 1,
|
|
kind,
|
|
created_at: started
|
|
});
|
|
insert('script_versions', {
|
|
id: index + 1,
|
|
script_id: index + 1,
|
|
version: 1,
|
|
sha256: hash,
|
|
content_hash: hash,
|
|
size: content.length,
|
|
notes: 'Synthetic preview content.',
|
|
state: 'tested',
|
|
kind,
|
|
uploaded_at: started
|
|
});
|
|
}
|
|
for (let index = 0; index < 36; index++) {
|
|
const failed = index % 7 === 0;
|
|
const server = fixtureServers[failed ? 5 : index % fixtureServers.length];
|
|
const id = `fixture-operation-${index + 1}`;
|
|
const state = failed ? 'failed' : index % 11 === 0 ? 'cancelled' : 'done';
|
|
insert('server_deployments', {
|
|
id,
|
|
server: server.name,
|
|
action: ['restart', 'start', 'rebuild', 'stop'][index % 4],
|
|
state,
|
|
stage: failed
|
|
? 'Verification failed'
|
|
: state === 'done'
|
|
? 'Completed'
|
|
: 'Cancelled before dispatch',
|
|
started_at: iso(now - (60 + index * 2) * MINUTE),
|
|
finished_at: iso(now - (59 + index * 2) * MINUTE),
|
|
triggered_by: 'Fixture operator',
|
|
error: failed ? 'Synthetic verification timeout. No host command was sent.' : null,
|
|
warnings: '[]'
|
|
});
|
|
insert('native_provisioning', {
|
|
deployment_id: id,
|
|
instance_id: server.instance,
|
|
generation: 1
|
|
});
|
|
insert('server_deployment_events', {
|
|
deployment_id: id,
|
|
created_at: iso(now - (59 + index * 2) * MINUTE),
|
|
message: 'Synthetic retained history; no command was sent.'
|
|
});
|
|
}
|
|
for (let index = 0; index < 8; index++) {
|
|
const server = fixtureServers[index % fixtureServers.length];
|
|
insert('ec_host_operations', {
|
|
id: `fixture-host-command-${index + 1}`,
|
|
host_id: server.host,
|
|
instance_id: server.instance,
|
|
server_name: server.name,
|
|
kind: index % 2 ? 'instance.inspect' : 'instance.restart',
|
|
payload: '{}',
|
|
state: index % 3 ? 'completed' : 'failed',
|
|
result: '{}',
|
|
error: index % 3 ? null : 'Synthetic command failure.',
|
|
created_at: iso(now - (90 + index * 3) * MINUTE),
|
|
updated_at: iso(now - (89 + index * 3) * MINUTE),
|
|
source: 'Fixture operator'
|
|
});
|
|
}
|
|
for (let index = 0; index < 3; index++) {
|
|
const server = fixtureServers[index === 0 ? 1 : index === 1 ? 5 : index];
|
|
insert('maintenance_jobs', {
|
|
id: index + 1,
|
|
server: server.name,
|
|
state: index === 1 ? 'failed' : 'done',
|
|
target_revision: 4878,
|
|
requested_at: iso(now - (120 + index * 4) * MINUTE),
|
|
finished_at: iso(now - (119 + index * 4) * MINUTE),
|
|
detail:
|
|
index === 1
|
|
? 'Synthetic update could not be verified.'
|
|
: 'Synthetic scheduled restart completed.',
|
|
kind: 'schedule',
|
|
origin: 'manual',
|
|
provisioning_id: index === 0 ? 'fixture-operation-2' : null
|
|
});
|
|
insert('watchdog_incidents', {
|
|
id: `fixture-recovery-${index + 1}`,
|
|
server: server.name,
|
|
kind: 'health',
|
|
state: 'resolved',
|
|
summary: 'Synthetic recovery finding resolved.',
|
|
details: '{}',
|
|
first_seen_at: iso(now - (130 + index * 4) * MINUTE),
|
|
last_seen_at: iso(now - (129 + index * 4) * MINUTE),
|
|
finished_at: iso(now - (129 + index * 4) * MINUTE)
|
|
});
|
|
insert('deployments', {
|
|
id: `fixture-campaign-${index + 1}`,
|
|
action: 'drift',
|
|
started_at: iso(now - (140 + index * 4) * MINUTE),
|
|
finished_at: iso(now - (139 + index * 4) * MINUTE),
|
|
triggered_by: 'Fixture operator'
|
|
});
|
|
insert('deployment_servers', {
|
|
deployment_id: `fixture-campaign-${index + 1}`,
|
|
server: server.name,
|
|
files_changed: 1,
|
|
restart_state: 'done',
|
|
players: 0
|
|
});
|
|
}
|
|
for (let index = 0; index < 2; index++) {
|
|
insert('betas', {
|
|
id: index + 1,
|
|
script_id: 1,
|
|
script_version_id: 1,
|
|
trigger_kind: 'ui',
|
|
state: index ? 'rolled-back' : 'promoted',
|
|
opened_at: iso(now - (150 + index * 3) * MINUTE),
|
|
closed_at: iso(now - (149 + index * 3) * MINUTE),
|
|
notes: 'Synthetic completed beta.'
|
|
});
|
|
insert('beta_scripts', {
|
|
beta_id: index + 1,
|
|
script_id: 1,
|
|
script_version_id: 1,
|
|
kind: 'script'
|
|
});
|
|
insert('beta_servers', {
|
|
beta_id: index + 1,
|
|
server: fixtureServers[index].name,
|
|
had_original: 0,
|
|
stage: 'ready',
|
|
updated_at: iso(now - (149 + index * 3) * MINUTE)
|
|
});
|
|
}
|
|
insert('provisioning_sources', {
|
|
operation_id: 'fixture-operation-2',
|
|
kind: 'maintenance',
|
|
source_id: '1',
|
|
server: fixtureServers[1].name
|
|
});
|
|
insert('provisioning_sources', {
|
|
operation_id: 'fixture-operation-3',
|
|
kind: 'autoscaling',
|
|
source_id: 'fixture-paused-plan',
|
|
server: fixtureServers[2].name
|
|
});
|
|
})();
|
|
}
|
|
|
|
/** Refreshes existing synthetic observations only; never queues work or writes real files. */
|
|
export function refreshFixtureData(handle: Database.Database, now: number): void {
|
|
const marker = handle.prepare('SELECT value FROM settings WHERE key=?').get(FIXTURE_KEY) as
|
|
{ value: string } | undefined;
|
|
if (marker?.value !== FIXTURE_VERSION)
|
|
throw new Error('Refusing to refresh an unmarked database.');
|
|
if (!Number.isFinite(now)) throw new Error('Invalid fixture time.');
|
|
handle.transaction(() => {
|
|
for (const [index, server] of fixtureServers.entries()) {
|
|
handle
|
|
.prepare(
|
|
`UPDATE servers SET last_seen_at=?,last_checked_at=?,container_checked_at=?,container_state=?,
|
|
last_players=?,last_map=?,last_roster=?,last_error=?,rcon_failures=?,last_reply_snippet=NULL WHERE name=?`
|
|
)
|
|
.run(
|
|
seenAt(server, now),
|
|
iso(now),
|
|
iso(now),
|
|
containerState(server),
|
|
server.players,
|
|
server.map.canonical,
|
|
JSON.stringify(rosterFor(server, index)),
|
|
server.state === 'stale' ? 'timeout: no reply from game server' : null,
|
|
server.state === 'stale' ? 2 : 0,
|
|
server.name
|
|
);
|
|
handle
|
|
.prepare(
|
|
`UPDATE server_telemetry SET received_at=?,payload=json_set(payload,'$.receivedAt',?) WHERE server=?`
|
|
)
|
|
.run(iso(now), iso(now), server.name);
|
|
handle
|
|
.prepare(
|
|
"UPDATE leaderboard_matches SET last_received_at=? WHERE server=? AND status='active' AND id BETWEEN 495 AND 500"
|
|
)
|
|
.run(iso(now), server.name);
|
|
handle
|
|
.prepare('UPDATE plutonium_versions SET checked_at=? WHERE server=?')
|
|
.run(iso(now), server.name);
|
|
}
|
|
for (let index = 1; index <= 3; index++) {
|
|
const id = `fixture-host-${index}`;
|
|
handle
|
|
.prepare(
|
|
"UPDATE ec_hosts SET last_heartbeat=?,inventory=json_set(inventory,'$.instanceStates',json(?)) WHERE id=?"
|
|
)
|
|
.run(
|
|
iso(now - (index === 3 ? 10 * MINUTE : 0)),
|
|
JSON.stringify(instanceStates(id, now)),
|
|
id
|
|
);
|
|
}
|
|
})();
|
|
}
|