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:
@@ -20,8 +20,12 @@ apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem("languard_token");
|
||||
window.location.href = "/login";
|
||||
const url = error.config?.url ?? "";
|
||||
// Don't redirect on auth endpoints — let the calling component handle the error
|
||||
if (!url.startsWith("/api/auth/")) {
|
||||
localStorage.removeItem("languard_token");
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
|
||||
54
frontend/src/lib/logger.ts
Normal file
54
frontend/src/lib/logger.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Lightweight logger for the frontend.
|
||||
*
|
||||
* In development: logs to console with level-appropriate methods.
|
||||
* In production: can be swapped for an error reporting service (Sentry, etc.)
|
||||
* by replacing the transport in logger.ts.
|
||||
*/
|
||||
|
||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
const LOG_LEVELS: Record<LogLevel, number> = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
};
|
||||
|
||||
const currentLevel: LogLevel =
|
||||
(import.meta.env.VITE_LOG_LEVEL as LogLevel) ??
|
||||
(import.meta.env.DEV ? "debug" : "warn");
|
||||
|
||||
function shouldLog(level: LogLevel): boolean {
|
||||
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
||||
}
|
||||
|
||||
function formatMessage(level: LogLevel, context: string, message: string): string {
|
||||
return `[${level.toUpperCase()}] [${context}] ${message}`;
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
debug(context: string, message: string, ...args: unknown[]) {
|
||||
if (shouldLog("debug")) {
|
||||
console.debug(formatMessage("debug", context, message), ...args);
|
||||
}
|
||||
},
|
||||
|
||||
info(context: string, message: string, ...args: unknown[]) {
|
||||
if (shouldLog("info")) {
|
||||
console.info(formatMessage("info", context, message), ...args);
|
||||
}
|
||||
},
|
||||
|
||||
warn(context: string, message: string, ...args: unknown[]) {
|
||||
if (shouldLog("warn")) {
|
||||
console.warn(formatMessage("warn", context, message), ...args);
|
||||
}
|
||||
},
|
||||
|
||||
error(context: string, message: string, ...args: unknown[]) {
|
||||
if (shouldLog("error")) {
|
||||
console.error(formatMessage("error", context, message), ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user