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,146 @@
import { test, expect } from "@playwright/test";
/**
* Full-stack integration tests against the real backend (http://localhost:8000).
* These tests do NOT mock API routes — they exercise the real API + frontend.
*
* Prerequisites:
* - Backend running on port 8000
* - Frontend dev server running on port 5173
* - Admin account with username "admin" / password "admin123"
* - A3Master server record exists
*/
const REAL_API = "http://localhost:8000";
test.describe("Full Stack Integration", () => {
test("should login and see A3Master server on dashboard", async ({ page }) => {
// Navigate to login page
await page.goto("/login");
await expect(page.locator('[data-testid="login-card"]')).toBeVisible();
// Fill in real credentials
await page.locator('[data-testid="login-username"]').fill("admin");
await page.locator('[data-testid="login-password"]').fill("admin123");
await page.locator('[data-testid="login-submit"]').click();
// Should navigate to dashboard
await page.waitForURL("/", { timeout: 10_000 });
// Dashboard should render
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible({
timeout: 10_000,
});
// Should show at least one server (A3Master) — use .first() to avoid strict mode
await expect(page.locator("text=A3Master").first()).toBeVisible({ timeout: 10_000 });
// Should show server count
const serverCount = await page.locator("[data-testid^='server-card-']").count();
expect(serverCount).toBeGreaterThanOrEqual(1);
});
test("should show A3Master server details in card", async ({ page }) => {
// Login via API and set auth state
const loginRes = await page.request.post(`${REAL_API}/api/auth/login`, {
data: { username: "admin", password: "admin123" },
});
expect(loginRes.ok()).toBe(true);
const loginData = await loginRes.json();
const token = loginData.data.access_token;
const user = loginData.data.user;
// Set auth state in localStorage
await page.addInitScript(
({ token, user }) => {
localStorage.setItem("languard_token", token);
localStorage.setItem(
"languard-auth",
JSON.stringify({ state: { token, user }, version: 0 }),
);
},
{ token, user },
);
await page.goto("/");
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible({
timeout: 10_000,
});
// Server card should show the A3Master name
await expect(page.locator("text=A3Master").first()).toBeVisible({ timeout: 10_000 });
// Server card should show arma3 game type
await expect(page.locator("text=arma3").first()).toBeVisible({ timeout: 5_000 });
});
test("should navigate to server detail page", async ({ page }) => {
// Login via API
const loginRes = await page.request.post(`${REAL_API}/api/auth/login`, {
data: { username: "admin", password: "admin123" },
});
const loginData = await loginRes.json();
const token = loginData.data.access_token;
const user = loginData.data.user;
await page.addInitScript(
({ token, user }) => {
localStorage.setItem("languard_token", token);
localStorage.setItem(
"languard-auth",
JSON.stringify({ state: { token, user }, version: 0 }),
);
},
{ token, user },
);
await page.goto("/");
await expect(page.locator('[data-testid="dashboard-content"]')).toBeVisible({
timeout: 10_000,
});
// Click the A3Master server card link in the dashboard content area
const serverLink = page.locator('[data-testid="dashboard-content"] a[href="/servers/1"]');
await serverLink.click();
// Should navigate to server detail
await expect(page).toHaveURL(/\/servers\/1/, { timeout: 5_000 });
});
test("should redirect to login when unauthenticated", async ({ page }) => {
// Navigate to dashboard without auth — should redirect to login
await page.goto("/");
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
await expect(page.locator('[data-testid="login-card"]')).toBeVisible();
});
test("should authenticate and check server API response shape", async ({ page }) => {
// Login via API to verify backend response format
const loginRes = await page.request.post(`${REAL_API}/api/auth/login`, {
data: { username: "admin", password: "admin123" },
});
expect(loginRes.ok()).toBe(true);
const loginData = await loginRes.json();
expect(loginData.success).toBe(true);
expect(loginData.data.access_token).toBeDefined();
expect(loginData.data.user.username).toBe("admin");
expect(loginData.data.user.role).toBe("admin");
// Fetch servers via API
const token = loginData.data.access_token;
const serversRes = await page.request.get(`${REAL_API}/api/servers`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(serversRes.ok()).toBe(true);
const serversData = await serversRes.json();
expect(serversData.success).toBe(true);
expect(Array.isArray(serversData.data)).toBe(true);
expect(serversData.data.length).toBeGreaterThanOrEqual(1);
// Verify A3Master server has expected fields
const a3master = serversData.data.find((s: { name: string }) => s.name === "A3Master");
expect(a3master).toBeDefined();
expect(a3master.game_type).toBe("arma3");
expect(a3master.exe_path).toContain("A3Master");
});
});