feat: implement full backend + frontend server detail, settings, and create server pages

Backend:
- Complete FastAPI backend with 42+ REST endpoints (auth, servers, config,
  players, bans, missions, mods, games, system)
- Game adapter architecture with Arma 3 as first-class adapter
- WebSocket real-time events for status, metrics, logs, players
- Background thread system (process monitor, metrics, log tail, RCon poller)
- Fernet encryption for sensitive config fields at rest
- JWT auth with admin/viewer roles, bcrypt password hashing
- SQLite with WAL mode, parameterized queries, migration system
- APScheduler cleanup jobs for logs, metrics, events

Frontend:
- Server Detail page with 7 tabs (overview, config, players, bans,
  missions, mods, logs)
- Settings page with password change and admin user management
- Create Server wizard (4-step; known bug: silent validation failure)
- New hooks: useServerDetail, useAuth, useGames
- New components: ServerHeader, ConfigEditor, PlayerTable, BanTable,
  MissionList, ModList, LogViewer, PasswordChange, UserManager
- WebSocket onEvent callback for real-time log accumulation
- 120 unit tests passing (Vitest + React Testing Library)

Docs:
- Added .gitignore, CLAUDE.md, README.md
- Updated FRONTEND.md, ARCHITECTURE.md with current implementation state
- Added .env.example for backend configuration

Known issues:
- Create Server form: "Next" buttons don't validate before advancing,
  causing silent submit failure when fields are invalid
- Config sub-tabs need UX redesign for non-technical users
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-17 11:58:34 +07:00
parent 620429c9b8
commit 6511353b55
119 changed files with 13752 additions and 5000 deletions

View File

@@ -0,0 +1,187 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'viewer',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_login TEXT,
CHECK (role IN ('admin', 'viewer'))
);
CREATE TABLE IF NOT EXISTS servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
game_type TEXT NOT NULL DEFAULT 'arma3',
status TEXT NOT NULL DEFAULT 'stopped',
pid INTEGER,
exe_path TEXT NOT NULL,
started_at TEXT,
stopped_at TEXT,
game_port INTEGER NOT NULL,
rcon_port INTEGER,
auto_restart INTEGER NOT NULL DEFAULT 0,
max_restarts INTEGER NOT NULL DEFAULT 3,
restart_window_seconds INTEGER NOT NULL DEFAULT 300,
restart_count INTEGER NOT NULL DEFAULT 0,
last_restart_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
CHECK (status IN ('stopped','starting','running','stopping','crashed','error')),
CHECK (game_port BETWEEN 1024 AND 65535),
CHECK (rcon_port IS NULL OR (rcon_port BETWEEN 1024 AND 65535))
);
CREATE INDEX IF NOT EXISTS idx_servers_status ON servers(status);
CREATE INDEX IF NOT EXISTS idx_servers_game_type ON servers(game_type);
CREATE INDEX IF NOT EXISTS idx_servers_game_port ON servers(game_port);
CREATE TABLE IF NOT EXISTS game_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
game_type TEXT NOT NULL,
section TEXT NOT NULL,
config_json TEXT NOT NULL DEFAULT '{}',
config_version INTEGER NOT NULL DEFAULT 1,
schema_version TEXT NOT NULL DEFAULT '1.0.0',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(server_id, section)
);
CREATE INDEX IF NOT EXISTS idx_game_configs_server ON game_configs(server_id);
CREATE INDEX IF NOT EXISTS idx_game_configs_type_section ON game_configs(game_type, section);
CREATE TABLE IF NOT EXISTS mods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_type TEXT NOT NULL,
name TEXT NOT NULL,
folder_path TEXT NOT NULL,
workshop_id TEXT,
description TEXT,
game_data TEXT DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (game_type, folder_path)
);
CREATE TABLE IF NOT EXISTS server_mods (
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
mod_id INTEGER NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
is_server_mod INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
game_data TEXT DEFAULT '{}',
PRIMARY KEY (server_id, mod_id)
);
CREATE INDEX IF NOT EXISTS idx_server_mods_server ON server_mods(server_id);
CREATE TABLE IF NOT EXISTS missions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
mission_name TEXT NOT NULL,
terrain TEXT,
file_size INTEGER,
game_data TEXT DEFAULT '{}',
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (server_id, filename)
);
CREATE INDEX IF NOT EXISTS idx_missions_server ON missions(server_id);
CREATE TABLE IF NOT EXISTS mission_rotation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
mission_id INTEGER NOT NULL REFERENCES missions(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL DEFAULT 0,
difficulty TEXT,
params_json TEXT NOT NULL DEFAULT '{}',
game_data TEXT DEFAULT '{}',
UNIQUE (server_id, sort_order)
);
CREATE INDEX IF NOT EXISTS idx_mission_rotation_server ON mission_rotation(server_id);
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
slot_id TEXT NOT NULL,
name TEXT NOT NULL,
guid TEXT,
ip TEXT,
ping INTEGER,
game_data TEXT DEFAULT '{}',
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (server_id, slot_id)
);
CREATE INDEX IF NOT EXISTS idx_players_server ON players(server_id);
CREATE TABLE IF NOT EXISTS player_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
guid TEXT,
ip TEXT,
game_data TEXT DEFAULT '{}',
joined_at TEXT NOT NULL,
left_at TEXT NOT NULL DEFAULT (datetime('now')),
session_duration_seconds INTEGER
);
CREATE INDEX IF NOT EXISTS idx_player_history_server ON player_history(server_id);
CREATE INDEX IF NOT EXISTS idx_player_history_guid ON player_history(guid);
CREATE TABLE IF NOT EXISTS bans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
guid TEXT,
name TEXT,
reason TEXT,
banned_by TEXT,
banned_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
game_data TEXT DEFAULT '{}',
CHECK (is_active IN (0, 1))
);
CREATE INDEX IF NOT EXISTS idx_bans_server ON bans(server_id);
CREATE INDEX IF NOT EXISTS idx_bans_guid ON bans(guid);
CREATE INDEX IF NOT EXISTS idx_bans_active ON bans(is_active);
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
timestamp TEXT NOT NULL,
level TEXT NOT NULL DEFAULT 'info',
message TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
CHECK (level IN ('info', 'warning', 'error'))
);
CREATE INDEX IF NOT EXISTS idx_logs_server_ts ON logs(server_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
CREATE INDEX IF NOT EXISTS idx_logs_created ON logs(created_at);
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
cpu_percent REAL,
ram_mb REAL,
player_count INTEGER
);
CREATE INDEX IF NOT EXISTS idx_metrics_server_ts ON metrics(server_id, timestamp);
CREATE TABLE IF NOT EXISTS server_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
actor TEXT,
detail TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_events_server ON server_events(server_id, created_at)

View File