fix: something

This commit is contained in:
hibna
2026-08-02 20:26:54 +03:00
parent 5215560ede
commit 11924416a9
38 changed files with 2198 additions and 466 deletions
@@ -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;
+3 -1
View File
@@ -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"
},
+84
View File
@@ -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);
});
+5
View File
@@ -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(),
+137
View File
@@ -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();
+14
View File
@@ -44,6 +44,13 @@ message CreateServerRequest {
map<string, string> environment = 7;
repeated PortMapping ports = 8;
repeated string install_plugin_urls = 9;
// Mount point of the server data directory inside the container.
// Empty means "derive from the image".
string data_path = 10;
// In-game command that shuts the server down cleanly (e.g. "stop", "quit").
string stop_command = 11;
// Total budget for a graceful shutdown before the container is killed.
int32 stop_timeout_seconds = 12;
}
message UpdateServerRequest {
@@ -55,6 +62,9 @@ message UpdateServerRequest {
string startup_command = 6;
map<string, string> environment = 7;
repeated PortMapping ports = 8;
string data_path = 9;
string stop_command = 10;
int32 stop_timeout_seconds = 11;
}
message ServerResponse {
@@ -106,6 +116,10 @@ enum PowerAction {
message PowerRequest {
string uuid = 1;
PowerAction action = 2;
// Optional per-request overrides. When empty the daemon falls back to the
// values captured when the container was created.
string stop_command = 3;
int32 stop_timeout_seconds = 4;
}
// === Server Status ===