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,61 @@
from core.dal.base_repository import BaseRepository
class LogRepository(BaseRepository):
def insert(self, server_id: int, entry: dict) -> None:
"""entry = {timestamp, level, message}"""
self._execute(
"""
INSERT INTO logs (server_id, timestamp, level, message)
VALUES (:sid, :ts, :level, :msg)
""",
{
"sid": server_id,
"ts": entry.get("timestamp", ""),
"level": entry.get("level", "info"),
"msg": entry.get("message", ""),
},
)
def query(
self,
server_id: int,
limit: int = 200,
offset: int = 0,
level: str | None = None,
since: str | None = None,
search: str | None = None,
) -> tuple[int, list[dict]]:
conditions = ["server_id = :sid"]
params: dict = {"sid": server_id, "limit": limit, "offset": offset}
if level:
conditions.append("level = :level")
params["level"] = level
if since:
conditions.append("timestamp >= :since")
params["since"] = since
if search:
conditions.append("message LIKE :search")
params["search"] = f"%{search}%"
where = " AND ".join(conditions)
total_row = self._fetchone(f"SELECT COUNT(*) as cnt FROM logs WHERE {where}", params)
total = total_row["cnt"] if total_row else 0
rows = self._fetchall(
f"SELECT * FROM logs WHERE {where} ORDER BY timestamp DESC LIMIT :limit OFFSET :offset",
params,
)
return total, rows
def clear(self, server_id: int) -> int:
result = self._execute(
"DELETE FROM logs WHERE server_id = :sid", {"sid": server_id}
)
return result.rowcount
def cleanup_old(self, retention_days: int) -> None:
self._execute(
"DELETE FROM logs WHERE created_at < datetime('now', :delta)",
{"delta": f"-{retention_days} days"},
)