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

123 lines
4.1 KiB
Python

"""
BaseServerThread — base class for all per-server background threads.
Rules every subclass MUST follow:
- Call super().__init__(server_id, name) in __init__
- Implement _run_loop() — called repeatedly until _stop_event is set
- Do NOT override run() directly
- Use self._db for all database operations — it is a thread-local connection
- Call self._close_db() in your finally block if you open additional connections
- Exceptions raised from _run_loop() are caught, logged, and the loop continues
unless the exception is a fatal error — set self._fatal_error = True to stop
"""
from __future__ import annotations
import logging
import threading
from abc import ABC, abstractmethod
from database import get_thread_db
logger = logging.getLogger(__name__)
_EXCEPTION_BACKOFF_BASE = 2.0
_EXCEPTION_BACKOFF_MAX = 60.0
_EXCEPTION_BACKOFF_MULTIPLIER = 2.0
class BaseServerThread(ABC, threading.Thread):
"""
Abstract base for all per-server background threads.
Subclasses implement _run_loop(). This base class handles:
- Stop event signaling
- Thread-local DB connection lifecycle
- Exception backoff to prevent tight crash loops
- Structured logging with server_id context
"""
def __init__(self, server_id: int, name: str) -> None:
super().__init__(name=f"{name}-server-{server_id}", daemon=True)
self.server_id = server_id
self._stop_event = threading.Event()
self._fatal_error = False
self._db = None
self._exception_count = 0
# ── Public API ──
def stop(self) -> None:
"""Signal the thread to stop. Does not block."""
self._stop_event.set()
def stop_and_join(self, timeout: float = 5.0) -> None:
"""Signal stop and wait for the thread to exit."""
self._stop_event.set()
self.join(timeout=timeout)
@property
def is_stopping(self) -> bool:
return self._stop_event.is_set()
# ── Thread entry point ──
def run(self) -> None:
logger.info("[%s] Starting", self.name)
backoff = _EXCEPTION_BACKOFF_BASE
try:
self._db = get_thread_db()
self._on_start()
while not self._stop_event.is_set() and not self._fatal_error:
try:
self._run_loop()
backoff = _EXCEPTION_BACKOFF_BASE
self._exception_count = 0
except Exception as exc:
self._exception_count += 1
logger.error(
"[%s] Unhandled exception in _run_loop (count=%d): %s",
self.name, self._exception_count, exc, exc_info=True,
)
if self._fatal_error:
break
self._stop_event.wait(timeout=backoff)
backoff = min(backoff * _EXCEPTION_BACKOFF_MULTIPLIER, _EXCEPTION_BACKOFF_MAX)
except Exception as exc:
logger.critical("[%s] Fatal error in thread setup: %s", self.name, exc, exc_info=True)
finally:
self._on_stop()
self._close_db()
logger.info("[%s] Stopped", self.name)
# ── Hooks for subclasses ──
def _on_start(self) -> None:
"""Called once before the loop starts. Override for setup."""
def _on_stop(self) -> None:
"""Called once after the loop ends. Override for cleanup."""
@abstractmethod
def _run_loop(self) -> None:
"""
Implement the thread's work here.
Called repeatedly until stop() is called or _fatal_error is set.
Should block for a short period (sleep or wait) to avoid busy-looping.
"""
# ── Internal helpers ──
def _close_db(self) -> None:
if self._db is not None:
try:
self._db.close()
except Exception as exc:
logger.debug("[%s] Error closing DB connection: %s", self.name, exc)
self._db = None
def _sleep(self, seconds: float) -> None:
"""Interruptible sleep — wakes up early if stop() is called."""
self._stop_event.wait(timeout=seconds)