Files
Tran G. (Revernomad) Khoa 6511353b55 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
2026-04-17 11:58:34 +07:00

87 lines
2.7 KiB
Python

"""Game-agnostic port availability checking."""
from __future__ import annotations
import logging
import socket
logger = logging.getLogger(__name__)
def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool:
"""Return True if the port is already bound."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
try:
s.bind((host, port))
return False
except OSError:
return True
def check_server_ports_available(
game_port: int,
rcon_port: int | None = None,
host: str = "127.0.0.1",
port_conventions: dict[str, int] | None = None,
) -> list[int]:
"""
Check all ports for a server instance.
If port_conventions is provided (from adapter), checks all derived ports.
Returns list of ports that are already in use (empty = all available).
"""
ports_to_check: set[int] = set()
if port_conventions:
ports_to_check.update(port_conventions.values())
else:
ports_to_check.add(game_port)
if rcon_port is not None:
ports_to_check.add(rcon_port)
return [p for p in sorted(ports_to_check) if is_port_in_use(p, host)]
def check_ports_against_running_servers(
new_server_game_port: int,
new_server_rcon_port: int | None,
exclude_server_id: int | None,
db,
) -> list[int]:
"""
Cross-game port conflict detection.
Checks new server's full port set against all running servers' full port sets.
Returns list of conflicting ports.
"""
from adapters.registry import GameAdapterRegistry
from sqlalchemy import text
rows = db.execute(
text("SELECT id, game_type, game_port, rcon_port FROM servers WHERE status IN ('running','starting')")
).fetchall()
occupied_ports: set[int] = set()
for row in rows:
if exclude_server_id and row[0] == exclude_server_id:
continue
try:
adapter = GameAdapterRegistry.get(row[1])
conventions = adapter.get_process_config().get_port_conventions(row[2])
occupied_ports.update(conventions.values())
except KeyError:
logger.debug("Unknown game type '%s', falling back to game_port only", row[1])
occupied_ports.add(row[2])
if row[3] is not None:
occupied_ports.add(row[3])
# Check new server's ports against occupied set
try:
adapter = GameAdapterRegistry.get("arma3") # temporary — will be passed in
except KeyError:
logger.debug("No 'arma3' adapter for port conventions, using defaults")
new_ports: set[int] = {new_server_game_port}
if new_server_rcon_port:
new_ports.add(new_server_rcon_port)
return sorted(new_ports & occupied_ports)