panel v0.9.2 — public release

Self-hostable game server control panel: Go controller + agent, 26 game
modules, embedded web UI. One-line install via install.sh / install.ps1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:43:35 -07:00
commit 4ccccc6fe2
2164 changed files with 301480 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
package db
import (
"context"
"embed"
"fmt"
"sort"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// migrateLockKey is the pg advisory-lock key serializing Migrate across
// controllers sharing one database. Arbitrary but fixed: fnv-ish of
// "panel:schema_migrations". Session-scoped (not transaction-scoped) so it
// spans the whole multi-transaction run and releases on disconnect if the
// process dies mid-migrate.
const migrateLockKey int64 = 0x70616e656c6d6967 // "panelmig"
// Migrate applies any pending SQL files in migrations/ in filename-sorted
// order. Each file is wrapped in a transaction alongside its schema_migrations
// insert so failure leaves the DB in a consistent state. A session-level
// pg advisory lock serializes concurrent controllers: the second one blocks
// here, then finds every version already recorded and no-ops.
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
// Dedicated connection: session advisory locks belong to a connection,
// and pool.Exec could lock on one conn and unlock on another.
lockConn, err := pool.Acquire(ctx)
if err != nil {
return fmt.Errorf("acquire migrate lock conn: %w", err)
}
defer lockConn.Release()
if _, err := lockConn.Exec(ctx, `SELECT pg_advisory_lock($1)`, migrateLockKey); err != nil {
return fmt.Errorf("acquire migrate advisory lock: %w", err)
}
defer func() {
_, _ = lockConn.Exec(context.WithoutCancel(ctx), `SELECT pg_advisory_unlock($1)`, migrateLockKey)
}()
if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`); err != nil {
return fmt.Errorf("create schema_migrations: %w", err)
}
entries, err := migrationsFS.ReadDir("migrations")
if err != nil {
return fmt.Errorf("read embedded migrations: %w", err)
}
var files []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
files = append(files, e.Name())
}
}
sort.Strings(files)
for _, name := range files {
var exists bool
if err := pool.QueryRow(ctx,
`SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)`, name,
).Scan(&exists); err != nil {
return fmt.Errorf("check %s: %w", name, err)
}
if exists {
continue
}
body, err := migrationsFS.ReadFile("migrations/" + name)
if err != nil {
return fmt.Errorf("read %s: %w", name, err)
}
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx for %s: %w", name, err)
}
if _, err := tx.Exec(ctx, string(body)); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("apply %s: %w", name, err)
}
if _, err := tx.Exec(ctx, `INSERT INTO schema_migrations(version) VALUES ($1)`, name); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("record %s: %w", name, err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit %s: %w", name, err)
}
}
return nil
}