fix: something
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
-- Per-game shutdown controls and container mount overrides.
|
||||
ALTER TABLE "games"
|
||||
ADD COLUMN IF NOT EXISTS "stop_timeout_seconds" integer NOT NULL DEFAULT 30;
|
||||
|
||||
ALTER TABLE "games"
|
||||
ADD COLUMN IF NOT EXISTS "container_data_path" text;
|
||||
|
||||
-- Mount points the daemon previously derived from the image name. Storing them
|
||||
-- makes the mapping visible and editable instead of hardcoded.
|
||||
UPDATE "games" SET "container_data_path" = '/home/steam/cs2-dedicated' WHERE "slug" = 'cs2' AND "container_data_path" IS NULL;
|
||||
UPDATE "games" SET "container_data_path" = '/config' WHERE "slug" IN ('fivem', 'satisfactory') AND "container_data_path" IS NULL;
|
||||
UPDATE "games" SET "container_data_path" = '/data' WHERE "slug" IN ('minecraft-java', 'minecraft-bedrock', 'terraria', 'rust') AND "container_data_path" IS NULL;
|
||||
|
||||
-- Shutdown budgets. Source servers quit instantly once they get the command;
|
||||
-- ARK and Satisfactory need to flush a world save first.
|
||||
UPDATE "games" SET "stop_timeout_seconds" = 60 WHERE "slug" IN ('minecraft-java', 'minecraft-bedrock');
|
||||
UPDATE "games" SET "stop_timeout_seconds" = 120 WHERE "slug" = 'satisfactory';
|
||||
|
||||
-- ARK: Survival Evolved
|
||||
INSERT INTO "games" (
|
||||
"slug",
|
||||
"name",
|
||||
"docker_image",
|
||||
"default_port",
|
||||
"config_files",
|
||||
"automation_rules",
|
||||
"startup_command",
|
||||
"stop_command",
|
||||
"stop_timeout_seconds",
|
||||
"container_data_path",
|
||||
"environment_vars",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
VALUES (
|
||||
'ark-se',
|
||||
'ARK: Survival Evolved',
|
||||
'hermsi/ark-server:latest',
|
||||
7777,
|
||||
'[
|
||||
{
|
||||
"path": "server/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini",
|
||||
"parser": "properties",
|
||||
"editableKeys": [
|
||||
"SessionName",
|
||||
"ServerPassword",
|
||||
"ServerAdminPassword",
|
||||
"MaxPlayers",
|
||||
"ServerPVE",
|
||||
"ServerCrosshair",
|
||||
"ServerHardcore",
|
||||
"AllowThirdPersonPlayer",
|
||||
"ShowMapPlayerLocation",
|
||||
"GlobalVoiceChat",
|
||||
"ProximityChat",
|
||||
"NoTributeDownloads",
|
||||
"AllowAnyoneBabyImprintCuddle",
|
||||
"DifficultyOffset",
|
||||
"XPMultiplier",
|
||||
"TamingSpeedMultiplier",
|
||||
"HarvestAmountMultiplier",
|
||||
"DayCycleSpeedScale",
|
||||
"NightTimeSpeedScale",
|
||||
"PlayerCharacterWaterDrainMultiplier",
|
||||
"PlayerCharacterFoodDrainMultiplier",
|
||||
"StructureDamageMultiplier",
|
||||
"StructureResistanceMultiplier",
|
||||
"RCONEnabled",
|
||||
"RCONPort"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "server/ShooterGame/Saved/Config/LinuxServer/Game.ini",
|
||||
"parser": "properties"
|
||||
}
|
||||
]'::jsonb,
|
||||
'[]'::jsonb,
|
||||
'',
|
||||
'',
|
||||
300,
|
||||
'/app',
|
||||
'[
|
||||
{
|
||||
"key": "SESSION_NAME",
|
||||
"default": "SourceGamePanel ARK Server",
|
||||
"description": "Server name shown in the ARK server browser",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "SERVER_MAP",
|
||||
"default": "TheIsland",
|
||||
"description": "Map to load (TheIsland, TheCenter, Ragnarok, Valguero, CrystalIsles, ...)",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "ADMIN_PASSWORD",
|
||||
"default": "",
|
||||
"description": "Admin/RCON password. Required — RCON console commands use it.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "SERVER_PASSWORD",
|
||||
"default": "",
|
||||
"description": "Password players need to join. Leave empty for a public server.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "MAX_PLAYERS",
|
||||
"default": "20",
|
||||
"description": "Maximum player count",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "UPDATE_ON_START",
|
||||
"label": "Update on start",
|
||||
"default": "false",
|
||||
"description": "Run a SteamCMD update every time the server starts",
|
||||
"required": false,
|
||||
"inputType": "boolean",
|
||||
"enabledLabel": "Aktif",
|
||||
"disabledLabel": "Pasif"
|
||||
},
|
||||
{
|
||||
"key": "BACKUP_ON_STOP",
|
||||
"label": "Backup on stop",
|
||||
"default": "false",
|
||||
"description": "Create a world backup during shutdown (makes stopping slower)",
|
||||
"required": false,
|
||||
"inputType": "boolean",
|
||||
"enabledLabel": "Aktif",
|
||||
"disabledLabel": "Pasif"
|
||||
},
|
||||
{
|
||||
"key": "WARN_ON_STOP",
|
||||
"label": "Warn players on stop",
|
||||
"default": "false",
|
||||
"description": "Broadcast a shutdown warning to connected players before stopping",
|
||||
"required": false,
|
||||
"inputType": "boolean",
|
||||
"enabledLabel": "Aktif",
|
||||
"disabledLabel": "Pasif"
|
||||
},
|
||||
{
|
||||
"key": "PRE_UPDATE_BACKUP",
|
||||
"label": "Backup before update",
|
||||
"default": "true",
|
||||
"description": "Back the world up before applying a SteamCMD update",
|
||||
"required": false,
|
||||
"inputType": "boolean",
|
||||
"enabledLabel": "Aktif",
|
||||
"disabledLabel": "Pasif"
|
||||
}
|
||||
]'::jsonb,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT ("slug") DO NOTHING;
|
||||
@@ -9,7 +9,9 @@
|
||||
"build": "tsc",
|
||||
"lint": "eslint src/",
|
||||
"db:generate": "dotenv -e ../../.env -- drizzle-kit generate",
|
||||
"db:migrate": "dotenv -e ../../.env -- drizzle-kit migrate",
|
||||
"db:push": "dotenv -e ../../.env -- drizzle-kit push --force",
|
||||
"db:migrate": "pnpm db:push && dotenv -e ../../.env -- tsx src/migrate.ts",
|
||||
"db:migrate:data": "dotenv -e ../../.env -- tsx src/migrate.ts",
|
||||
"db:seed": "dotenv -e ../../.env -- tsx src/seed.ts",
|
||||
"db:studio": "dotenv -e ../../.env -- drizzle-kit studio"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import postgres from 'postgres';
|
||||
|
||||
/**
|
||||
* Applies the hand-written data migrations in `drizzle/*.sql`.
|
||||
*
|
||||
* Table structure itself comes from `drizzle-kit push`, which diffs the live
|
||||
* database against `src/schema` — that keeps a fresh install working without
|
||||
* shipping a full generated migration chain. These SQL files carry the data
|
||||
* changes push cannot know about (default game rows, automation rule updates).
|
||||
*
|
||||
* Every file is recorded in `gamepanel_data_migrations`, so re-running is safe.
|
||||
*/
|
||||
|
||||
const MIGRATIONS_TABLE = 'gamepanel_data_migrations';
|
||||
|
||||
async function main() {
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
console.error('DATABASE_URL is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const migrationsDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'drizzle',
|
||||
);
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = (await readdir(migrationsDir))
|
||||
.filter((file) => file.endsWith('.sql'))
|
||||
.sort();
|
||||
} catch {
|
||||
console.log('No data migrations directory found, nothing to apply.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No data migrations to apply.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = postgres(databaseUrl, { max: 1 });
|
||||
|
||||
try {
|
||||
await sql.unsafe(`
|
||||
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
||||
name text PRIMARY KEY,
|
||||
applied_at timestamptz NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
const applied = await sql.unsafe<{ name: string }[]>(
|
||||
`SELECT name FROM ${MIGRATIONS_TABLE}`,
|
||||
);
|
||||
const appliedNames = new Set(applied.map((row) => row.name));
|
||||
|
||||
for (const file of files) {
|
||||
if (appliedNames.has(file)) continue;
|
||||
|
||||
const contents = await readFile(path.join(migrationsDir, file), 'utf8');
|
||||
if (!contents.trim()) continue;
|
||||
|
||||
console.log(`Applying data migration: ${file}`);
|
||||
await sql.begin(async (tx) => {
|
||||
await tx.unsafe(contents);
|
||||
await tx.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name) VALUES ($1)`, [file]);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Data migrations up to date.');
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Data migration failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -10,6 +10,11 @@ export const games = pgTable('games', {
|
||||
automationRules: jsonb('automation_rules').default([]).notNull(),
|
||||
startupCommand: text('startup_command').notNull(),
|
||||
stopCommand: text('stop_command'),
|
||||
/// Total budget for a graceful shutdown before the container is killed.
|
||||
stopTimeoutSeconds: integer('stop_timeout_seconds').default(30).notNull(),
|
||||
/// Mount point of the server data directory inside the container. Null means
|
||||
/// "let the daemon derive it from the image".
|
||||
containerDataPath: text('container_data_path'),
|
||||
environmentVars: jsonb('environment_vars').default([]).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
|
||||
@@ -100,6 +100,8 @@ async function seed() {
|
||||
defaultPort: 25565,
|
||||
startupCommand: '/start',
|
||||
stopCommand: 'stop',
|
||||
stopTimeoutSeconds: 60,
|
||||
containerDataPath: '/data',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server.properties',
|
||||
@@ -147,6 +149,8 @@ async function seed() {
|
||||
defaultPort: 27015,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
stopTimeoutSeconds: 30,
|
||||
containerDataPath: '/home/steam/cs2-dedicated',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'game/csgo/cfg/server.cfg',
|
||||
@@ -332,6 +336,8 @@ async function seed() {
|
||||
defaultPort: 19132,
|
||||
startupCommand: '',
|
||||
stopCommand: 'stop',
|
||||
stopTimeoutSeconds: 60,
|
||||
containerDataPath: '/data',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server.properties',
|
||||
@@ -366,6 +372,8 @@ async function seed() {
|
||||
defaultPort: 7777,
|
||||
startupCommand: '',
|
||||
stopCommand: 'exit',
|
||||
stopTimeoutSeconds: 45,
|
||||
containerDataPath: '/data',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'serverconfig.txt',
|
||||
@@ -391,6 +399,8 @@ async function seed() {
|
||||
defaultPort: 28015,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
stopTimeoutSeconds: 60,
|
||||
containerDataPath: '/data',
|
||||
configFiles: [],
|
||||
environmentVars: [
|
||||
{
|
||||
@@ -421,6 +431,8 @@ async function seed() {
|
||||
defaultPort: 7777,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
stopTimeoutSeconds: 120,
|
||||
containerDataPath: '/config',
|
||||
configFiles: [],
|
||||
automationRules: [],
|
||||
environmentVars: [
|
||||
@@ -461,6 +473,8 @@ async function seed() {
|
||||
defaultPort: 30120,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
stopTimeoutSeconds: 30,
|
||||
containerDataPath: '/config',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server.cfg',
|
||||
@@ -486,6 +500,129 @@ async function seed() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'ark-se',
|
||||
name: 'ARK: Survival Evolved',
|
||||
dockerImage: 'hermsi/ark-server:latest',
|
||||
defaultPort: 7777,
|
||||
startupCommand: '',
|
||||
// The image traps SIGTERM and runs `arkmanager stop --saveworld`, so a
|
||||
// long SIGTERM budget is the correct shutdown path here.
|
||||
stopCommand: '',
|
||||
stopTimeoutSeconds: 300,
|
||||
containerDataPath: '/app',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini',
|
||||
parser: 'properties',
|
||||
editableKeys: [
|
||||
'SessionName',
|
||||
'ServerPassword',
|
||||
'ServerAdminPassword',
|
||||
'MaxPlayers',
|
||||
'ServerPVE',
|
||||
'ServerCrosshair',
|
||||
'ServerHardcore',
|
||||
'AllowThirdPersonPlayer',
|
||||
'ShowMapPlayerLocation',
|
||||
'GlobalVoiceChat',
|
||||
'ProximityChat',
|
||||
'NoTributeDownloads',
|
||||
'AllowAnyoneBabyImprintCuddle',
|
||||
'DifficultyOffset',
|
||||
'XPMultiplier',
|
||||
'TamingSpeedMultiplier',
|
||||
'HarvestAmountMultiplier',
|
||||
'DayCycleSpeedScale',
|
||||
'NightTimeSpeedScale',
|
||||
'PlayerCharacterWaterDrainMultiplier',
|
||||
'PlayerCharacterFoodDrainMultiplier',
|
||||
'StructureDamageMultiplier',
|
||||
'StructureResistanceMultiplier',
|
||||
'RCONEnabled',
|
||||
'RCONPort',
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'server/ShooterGame/Saved/Config/LinuxServer/Game.ini',
|
||||
parser: 'properties',
|
||||
},
|
||||
],
|
||||
automationRules: [],
|
||||
environmentVars: [
|
||||
{
|
||||
key: 'SESSION_NAME',
|
||||
default: 'SourceGamePanel ARK Server',
|
||||
description: 'Server name shown in the ARK server browser',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'SERVER_MAP',
|
||||
default: 'TheIsland',
|
||||
description:
|
||||
'Map to load (TheIsland, TheCenter, Ragnarok, Valguero, CrystalIsles, ...)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'ADMIN_PASSWORD',
|
||||
default: '',
|
||||
description: 'Admin/RCON password. Required — RCON console commands use it.',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'SERVER_PASSWORD',
|
||||
default: '',
|
||||
description: 'Password players need to join. Leave empty for a public server.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'MAX_PLAYERS',
|
||||
default: '20',
|
||||
description: 'Maximum player count',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'UPDATE_ON_START',
|
||||
label: 'Update on start',
|
||||
default: 'false',
|
||||
description: 'Run a SteamCMD update every time the server starts',
|
||||
required: false,
|
||||
inputType: 'boolean',
|
||||
enabledLabel: 'Aktif',
|
||||
disabledLabel: 'Pasif',
|
||||
},
|
||||
{
|
||||
key: 'BACKUP_ON_STOP',
|
||||
label: 'Backup on stop',
|
||||
default: 'false',
|
||||
description: 'Create a world backup during shutdown (makes stopping slower)',
|
||||
required: false,
|
||||
inputType: 'boolean',
|
||||
enabledLabel: 'Aktif',
|
||||
disabledLabel: 'Pasif',
|
||||
},
|
||||
{
|
||||
key: 'WARN_ON_STOP',
|
||||
label: 'Warn players on stop',
|
||||
default: 'false',
|
||||
description: 'Broadcast a shutdown warning to connected players before stopping',
|
||||
required: false,
|
||||
inputType: 'boolean',
|
||||
enabledLabel: 'Aktif',
|
||||
disabledLabel: 'Pasif',
|
||||
},
|
||||
{
|
||||
key: 'PRE_UPDATE_BACKUP',
|
||||
label: 'Backup before update',
|
||||
default: 'true',
|
||||
description: 'Back the world up before applying a SteamCMD update',
|
||||
required: false,
|
||||
inputType: 'boolean',
|
||||
enabledLabel: 'Aktif',
|
||||
disabledLabel: 'Pasif',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
.onConflictDoNothing();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user