Files
languard-servers-manager/frontend/src/hooks/useServerDetail.ts
Tran G. (Revernomad) Khoa 64b35a7aaf feat: basic/advanced config split with profile section gate
- FieldSchema gains optional `advanced` boolean flag
- ConfigEditor reads schema at top level and passes sectionSchema as prop
- ConfigSectionForm filters out advanced fields by default; "Show advanced"
  toggle reveals them without entering edit mode
- Profile (Difficulty) section shows an inline banner when
  forced_difficulty is not "Custom", guiding users to the right setting
- All 173 frontend tests pass; tsc clean
2026-04-20 10:49:08 +07:00

457 lines
12 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "@/lib/api";
// ── Types ──────────────────────────────────────────────────────────────
export interface EnrichedServer {
id: number;
name: string;
description: string | null;
game_type: string;
status: "stopped" | "running" | "starting" | "restarting" | "crashed";
pid: number | null;
exe_path: string;
game_port: number;
rcon_port: number | null;
auto_restart: boolean;
max_restarts: number;
restart_count: number;
last_restart_at: string | null;
started_at: string | null;
stopped_at: string | null;
created_at: string;
updated_at: string;
cpu_percent: number | null;
ram_mb: number | null;
player_count: number;
}
export interface ConfigSection {
[key: string]: unknown;
_meta: {
config_version: number;
schema_version: number;
};
}
export interface ConfigMap {
[sectionName: string]: ConfigSection;
}
export interface ConfigPreview {
[filename: string]: string;
}
export interface Player {
id: number;
server_id: number;
slot_id: number;
name: string;
guid: string;
ip: string;
ping: number;
game_data: string | null;
joined_at: string;
updated_at: string;
}
export interface PlayersResponse {
server_id: number;
player_count: number;
players: Player[];
}
export interface PlayerHistoryEntry {
id: number;
server_id: number;
name: string;
guid: string;
ip: string;
game_data: string | null;
joined_at: string;
left_at: string | null;
session_duration_seconds: number | null;
}
export interface PlayerHistoryResponse {
total: number;
items: PlayerHistoryEntry[];
}
export interface Ban {
id: number;
server_id: number;
guid: string;
name: string;
reason: string;
banned_by: string;
banned_at: string;
expires_at: string | null;
is_active: boolean;
game_data: string | null;
}
export interface CreateBanRequest {
player_uid: string;
ban_type?: "GUID" | "IP";
reason?: string;
duration_minutes?: number;
}
export interface Mission {
name: string;
filename: string;
size_bytes: number;
terrain: string;
}
export type MissionParamValue = number | string | boolean;
export interface MissionRotationEntry {
name: string;
difficulty: string;
params: Record<string, MissionParamValue>;
}
export interface MissionsResponse {
server_id: number;
missions: Mission[];
total: number;
}
export interface Mod {
name: string;
path: string;
size_bytes: number;
enabled: boolean;
is_server_mod: boolean;
display_name: string | null;
workshop_id: string | null;
}
export interface EnabledModEntry {
name: string;
is_server_mod: boolean;
}
export interface FieldSchema {
widget: "text" | "number" | "password" | "textarea" | "select" | "toggle" | "tag-list" | "hidden" | "key-value";
label?: string;
placeholder?: string;
min?: number;
max?: number;
options?: string[];
advanced?: boolean;
}
export interface ConfigSchema {
[section: string]: { [field: string]: FieldSchema };
}
export interface ModsResponse {
server_id: number;
mods: Mod[];
enabled_count: number;
}
// ── Query Hooks ────────────────────────────────────────────────────────
export function useServerConfigSchema(serverId: number) {
return useQuery({
queryKey: ["servers", serverId, "config", "schema"],
queryFn: async () => {
const res = await apiClient.get<{ success: boolean; data: ConfigSchema }>(
`/api/servers/${serverId}/config/schema`,
);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerConfig(serverId: number) {
return useQuery({
queryKey: ["servers", serverId, "config"],
queryFn: async () => {
const res = await apiClient.get<{ success: boolean; data: ConfigMap }>(
`/api/servers/${serverId}/config`,
);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerConfigSection(serverId: number, section: string) {
return useQuery({
queryKey: ["servers", serverId, "config", section],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: ConfigSection;
}>(`/api/servers/${serverId}/config/${section}`);
return res.data.data;
},
enabled: serverId > 0 && !!section,
});
}
export function useServerConfigPreview(serverId: number) {
return useQuery({
queryKey: ["servers", serverId, "config", "preview"],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: ConfigPreview;
}>(`/api/servers/${serverId}/config/preview`);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerPlayers(serverId: number) {
return useQuery({
queryKey: ["players", serverId],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: PlayersResponse;
}>(`/api/servers/${serverId}/players`);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerPlayerHistory(
serverId: number,
opts?: { limit?: number; offset?: number; search?: string },
) {
return useQuery({
queryKey: ["players", serverId, "history", opts],
queryFn: async () => {
const params = new URLSearchParams();
if (opts?.limit) params.set("limit", String(opts.limit));
if (opts?.offset) params.set("offset", String(opts.offset));
if (opts?.search) params.set("search", opts.search);
const qs = params.toString();
const res = await apiClient.get<{
success: boolean;
data: PlayerHistoryResponse;
}>(`/api/servers/${serverId}/players/history${qs ? `?${qs}` : ""}`);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerBans(serverId: number) {
return useQuery({
queryKey: ["bans", serverId],
queryFn: async () => {
const res = await apiClient.get<{ success: boolean; data: Ban[] }>(
`/api/servers/${serverId}/bans`,
);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerMissions(serverId: number) {
return useQuery({
queryKey: ["missions", serverId],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: MissionsResponse;
}>(`/api/servers/${serverId}/missions`);
return res.data.data;
},
enabled: serverId > 0,
});
}
export function useServerMods(serverId: number) {
return useQuery({
queryKey: ["mods", serverId],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: ModsResponse;
}>(`/api/servers/${serverId}/mods`);
return res.data.data;
},
enabled: serverId > 0,
});
}
// ── Mutation Hooks ─────────────────────────────────────────────────────
export function useUpdateConfigSection(serverId: number, section: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Record<string, unknown>) =>
apiClient.put(`/api/servers/${serverId}/config/${section}`, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["servers", serverId, "config", section],
});
queryClient.invalidateQueries({
queryKey: ["servers", serverId, "config"],
});
},
});
}
export function useCreateBan(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateBanRequest) =>
apiClient.post(`/api/servers/${serverId}/bans`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["bans", serverId] });
},
});
}
export function useRevokeBan(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (banId: number) =>
apiClient.delete(`/api/servers/${serverId}/bans/${banId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["bans", serverId] });
},
});
}
export function useServerMissionRotation(serverId: number) {
return useQuery({
queryKey: ["missions", serverId, "rotation"],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: { missions: MissionRotationEntry[] };
}>(`/api/servers/${serverId}/missions/rotation`);
return res.data.data.missions;
},
enabled: serverId > 0,
});
}
export function useUpdateMissionRotation(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { missions: MissionRotationEntry[]; config_version: number }) =>
apiClient.put(`/api/servers/${serverId}/missions/rotation`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["missions", serverId, "rotation"] });
queryClient.invalidateQueries({ queryKey: ["servers", serverId, "config", "server"] });
},
});
}
export function useUploadMission(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (files: File[]) => {
for (const file of files) {
const formData = new FormData();
formData.append("file", file);
await apiClient.post(`/api/servers/${serverId}/missions`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["missions", serverId] });
},
});
}
export function useDeleteMission(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (filename: string) =>
apiClient.delete(
`/api/servers/${serverId}/missions/${encodeURIComponent(filename)}`,
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["missions", serverId] });
},
});
}
export function useSetEnabledMods(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (mods: EnabledModEntry[]) =>
apiClient.put(`/api/servers/${serverId}/mods/enabled`, { mods }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["mods", serverId] });
},
});
}
export function useSendCommand(serverId: number) {
return useMutation({
mutationFn: (command: string) =>
apiClient.post(`/api/servers/${serverId}/rcon/command`, { command }),
});
}
export function useKickPlayer(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ slotId, reason }: { slotId: number; reason: string }) =>
apiClient.post(`/api/servers/${serverId}/players/${slotId}/kick`, { reason }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["players", serverId] }),
});
}
export function useBanPlayer(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ slotId, reason, durationMinutes }: { slotId: number; reason: string; durationMinutes?: number }) =>
apiClient.post(`/api/servers/${serverId}/players/${slotId}/ban`, {
reason,
duration_minutes: durationMinutes ?? null,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["players", serverId] });
queryClient.invalidateQueries({ queryKey: ["bans", serverId] });
},
});
}
export interface LogFile {
filename: string;
size_bytes: number;
modified_at: number;
}
export function useServerLogFiles(serverId: number) {
return useQuery({
queryKey: ["servers", serverId, "logfiles"],
queryFn: async () => {
const res = await apiClient.get<{ success: boolean; data: LogFile[] }>(
`/api/servers/${serverId}/logfiles`,
);
return res.data.data;
},
enabled: serverId > 0,
refetchInterval: 30_000,
});
}
export function useDeleteLogFile(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (filename: string) =>
apiClient.delete(`/api/servers/${serverId}/logfiles/${encodeURIComponent(filename)}`),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["servers", serverId, "logfiles"] }),
});
}