Phase7
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 14s

This commit is contained in:
smueller
2026-06-26 13:20:55 +02:00
parent 49a98ee31d
commit ab21f53cdd
86 changed files with 1207 additions and 63 deletions

View File

@@ -0,0 +1,79 @@
-- Phase 7: Idle shutdown, credits, usage metering, plan quotas
CREATE TYPE "CreditTransactionType" AS ENUM ('GRANT', 'USAGE_DEBIT', 'ADJUSTMENT', 'EXPIRE');
ALTER TABLE "plans"
ADD COLUMN IF NOT EXISTS "maxServersPerUser" INTEGER,
ADD COLUMN IF NOT EXISTS "idleTimeoutMinutes" INTEGER NOT NULL DEFAULT 15,
ADD COLUMN IF NOT EXISTS "idleGraceAfterStartMinutes" INTEGER NOT NULL DEFAULT 3,
ADD COLUMN IF NOT EXISTS "idleCountdownSeconds" INTEGER NOT NULL DEFAULT 120,
ADD COLUMN IF NOT EXISTS "queuePriority" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "monthlyCreditGrant" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "game_servers"
ADD COLUMN IF NOT EXISTS "idleShutdownEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS "lastPlayerSeenAt" TIMESTAMPTZ(3),
ADD COLUMN IF NOT EXISTS "idleStopAt" TIMESTAMPTZ(3),
ADD COLUMN IF NOT EXISTS "runningSince" TIMESTAMPTZ(3),
ADD COLUMN IF NOT EXISTS "lastMeteredAt" TIMESTAMPTZ(3);
CREATE TABLE "credit_wallets" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"balance" INTEGER NOT NULL DEFAULT 0,
"periodStart" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "credit_wallets_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "credit_wallets_userId_key" ON "credit_wallets"("userId");
ALTER TABLE "credit_wallets"
ADD CONSTRAINT "credit_wallets_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "credit_transactions" (
"id" TEXT NOT NULL,
"walletId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"type" "CreditTransactionType" NOT NULL,
"referenceType" TEXT,
"referenceId" TEXT,
"idempotencyKey" TEXT NOT NULL,
"metadata" JSONB,
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "credit_transactions_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "credit_transactions_idempotencyKey_key" ON "credit_transactions"("idempotencyKey");
CREATE INDEX "credit_transactions_walletId_createdAt_idx" ON "credit_transactions"("walletId", "createdAt");
ALTER TABLE "credit_transactions"
ADD CONSTRAINT "credit_transactions_walletId_fkey"
FOREIGN KEY ("walletId") REFERENCES "credit_wallets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "usage_records" (
"id" TEXT NOT NULL,
"serverId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"ramMb" INTEGER NOT NULL,
"durationSeconds" INTEGER NOT NULL,
"creditsCharged" INTEGER NOT NULL,
"periodStart" TIMESTAMPTZ(3) NOT NULL,
"periodEnd" TIMESTAMPTZ(3) NOT NULL,
"idempotencyKey" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "usage_records_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "usage_records_idempotencyKey_key" ON "usage_records"("idempotencyKey");
CREATE INDEX "usage_records_serverId_periodStart_idx" ON "usage_records"("serverId", "periodStart");
CREATE INDEX "usage_records_userId_periodStart_idx" ON "usage_records"("userId", "periodStart");
ALTER TABLE "usage_records"
ADD CONSTRAINT "usage_records_serverId_fkey"
FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -121,6 +121,13 @@ enum AddonInstallStatus {
REMOVED
}
enum CreditTransactionType {
GRANT
USAGE_DEBIT
ADJUSTMENT
EXPIRE
}
model User {
id String @id @default(uuid())
email String @unique
@@ -141,6 +148,7 @@ model User {
emailVerificationTokens EmailVerificationToken[]
passwordResetTokens PasswordResetToken[]
totpCredential TotpCredential?
creditWallet CreditWallet?
@@map("users")
}
@@ -276,10 +284,15 @@ model GameServer {
hostPort Int?
containerId String?
dataPath String?
ramMb Int @default(1024)
activeWorldName String @default("world")
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
ramMb Int @default(1024)
activeWorldName String @default("world")
idleShutdownEnabled Boolean @default(true)
lastPlayerSeenAt DateTime? @db.Timestamptz(3)
idleStopAt DateTime? @db.Timestamptz(3)
runningSince DateTime? @db.Timestamptz(3)
lastMeteredAt DateTime? @db.Timestamptz(3)
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id])
plan Plan? @relation(fields: [planId], references: [id])
@@ -292,6 +305,7 @@ model GameServer {
worldUploads WorldUpload[]
installedAddons InstalledAddon[]
startQueueEntry ServerStartQueueEntry?
usageRecords UsageRecord[]
@@index([userId])
@@index([nodeId])
@@ -397,18 +411,24 @@ model ServerStartQueueEntry {
}
model Plan {
id String @id @default(uuid())
name String @unique
slug String @unique
description String?
isFree Boolean @default(false)
isActive Boolean @default(true)
maxRamMb Int
maxCpuCores Float
maxStorageMb Int
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
id String @id @default(uuid())
name String @unique
slug String @unique
description String?
isFree Boolean @default(false)
isActive Boolean @default(true)
maxRamMb Int
maxCpuCores Float
maxStorageMb Int
maxServersPerUser Int?
idleTimeoutMinutes Int @default(15)
idleGraceAfterStartMinutes Int @default(3)
idleCountdownSeconds Int @default(120)
queuePriority Int @default(0)
monthlyCreditGrant Int @default(0)
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
gameServers GameServer[]
@@ -475,6 +495,56 @@ model JobRecord {
@@map("job_records")
}
model CreditWallet {
id String @id @default(uuid())
userId String @unique
balance Int @default(0)
periodStart DateTime @default(now()) @db.Timestamptz(3)
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
transactions CreditTransaction[]
@@map("credit_wallets")
}
model CreditTransaction {
id String @id @default(uuid())
walletId String
amount Int
type CreditTransactionType
referenceType String?
referenceId String?
idempotencyKey String @unique
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
wallet CreditWallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
@@index([walletId, createdAt])
@@map("credit_transactions")
}
model UsageRecord {
id String @id @default(uuid())
serverId String
userId String
ramMb Int
durationSeconds Int
creditsCharged Int
periodStart DateTime @db.Timestamptz(3)
periodEnd DateTime @db.Timestamptz(3)
idempotencyKey String @unique
createdAt DateTime @default(now()) @db.Timestamptz(3)
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
@@index([serverId, periodStart])
@@index([userId, periodStart])
@@map("usage_records")
}
model ServerBackup {
id String @id @default(uuid())
serverId String

View File

@@ -29,9 +29,21 @@ async function main(): Promise<void> {
maxRamMb: 2048,
maxCpuCores: 1,
maxStorageMb: 5120,
maxServersPerUser: 3,
idleTimeoutMinutes: 10,
idleGraceAfterStartMinutes: 2,
idleCountdownSeconds: 60,
queuePriority: 0,
monthlyCreditGrant: 5000,
},
update: {
isActive: true,
maxServersPerUser: 3,
idleTimeoutMinutes: 10,
idleGraceAfterStartMinutes: 2,
idleCountdownSeconds: 60,
queuePriority: 0,
monthlyCreditGrant: 5000,
},
});

View File

@@ -20,6 +20,9 @@ export type {
GameNode,
GameNodeHeartbeat,
ServerStartQueueEntry,
CreditWallet,
CreditTransaction,
UsageRecord,
Plan,
FeatureFlag,
SystemSetting,

File diff suppressed because one or more lines are too long