feat: add config preset manager

This commit is contained in:
2026-03-26 22:09:01 +01:00
parent 1dad80e557
commit e8cb877497
11 changed files with 587 additions and 29 deletions

View File

@@ -0,0 +1,187 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { listPresetsAction, applyPresetAction } from '@/lib/actions/presets';
import type { ConfigPreset } from '@/lib/schemas/preset';
import { PRESET_SECTION_LABELS, getPresetSections } from '@/lib/schemas/preset';
interface PresetPickerModalProps {
isOpen: boolean;
onClose: () => void;
serverId: string;
onApplied: () => void;
}
export function PresetPickerModal({ isOpen, onClose, serverId, onApplied }: PresetPickerModalProps) {
const [presets, setPresets] = useState<ConfigPreset[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const [restart, setRestart] = useState(false);
const [isApplying, setIsApplying] = useState(false);
const [applyError, setApplyError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const loadPresets = useCallback(async () => {
setIsLoading(true);
setLoadError(null);
const result = await listPresetsAction();
setIsLoading(false);
if (!result.success) {
setLoadError(result.message);
return;
}
setPresets(result.data);
}, []);
useEffect(() => {
if (isOpen) {
setSelected(null);
setApplyError(null);
setSearch('');
loadPresets();
}
}, [isOpen, loadPresets]);
const filtered = presets.filter((p) =>
p.name.toLowerCase().includes(search.toLowerCase()) ||
p.description.toLowerCase().includes(search.toLowerCase())
);
const handleApply = async () => {
if (!selected) return;
setIsApplying(true);
setApplyError(null);
const result = await applyPresetAction(serverId, selected, restart);
setIsApplying(false);
if (!result.success) {
setApplyError(result.message);
return;
}
onApplied();
onClose();
};
const selectedPreset = presets.find((p) => p.id === selected);
return (
<Modal isOpen={isOpen} onClose={onClose} title="Load Config Preset" className="max-w-lg">
<div className="flex flex-col gap-4">
<input
type="text"
placeholder="Search presets..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="form-input text-sm"
/>
{loadError && <p className="text-sm text-red">{loadError}</p>}
<div className="max-h-64 overflow-y-auto rounded-md border border-border">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<LoadingSpinner size="sm" />
</div>
) : filtered.length === 0 ? (
<div className="py-8 text-center text-sm text-muted">
{presets.length === 0 ? 'No presets saved yet.' : 'No presets match your search.'}
</div>
) : (
<ul>
{filtered.map((preset) => {
const sections = getPresetSections(preset);
const isSelected = selected === preset.id;
return (
<li
key={preset.id}
onClick={() => setSelected(preset.id)}
className={`cursor-pointer border-b border-border-muted px-4 py-3 transition-colors last:border-0 ${
isSelected
? 'bg-orange/10'
: 'hover:bg-hover'
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className={`truncate text-sm font-medium ${isSelected ? 'text-orange' : 'text-primary'}`}>
{preset.name}
</p>
{preset.description && (
<p className="mt-0.5 truncate text-xs text-muted">{preset.description}</p>
)}
</div>
<div className="shrink-0 text-right">
{sections.length > 0 ? (
<p className="text-xs text-muted">{sections.length} section{sections.length !== 1 ? 's' : ''}</p>
) : (
<p className="text-xs text-muted/50">Empty</p>
)}
</div>
</div>
</li>
);
})}
</ul>
)}
</div>
{selectedPreset && (
<div className="rounded-md border border-border-muted bg-overlay px-4 py-3">
<p className="mb-2 text-xs font-medium text-secondary">Sections in this preset:</p>
<div className="flex flex-wrap gap-1.5">
{getPresetSections(selectedPreset).map((key) => (
<span
key={key}
className="rounded-full bg-blue-bg px-2 py-0.5 text-xs text-blue"
>
{PRESET_SECTION_LABELS[key] ?? key}
</span>
))}
{getPresetSections(selectedPreset).length === 0 && (
<span className="text-xs text-muted">No sections — nothing will be applied.</span>
)}
</div>
</div>
)}
{applyError && <p className="text-sm text-red">{applyError}</p>}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
/>
Restart after applying
</label>
<div className="flex gap-2">
<Button variant="ghost" size="md" onClick={onClose} disabled={isApplying}>
Cancel
</Button>
<Button
variant="primary"
size="md"
onClick={handleApply}
disabled={!selected || isApplying}
>
{isApplying ? (
<>
<LoadingSpinner size="sm" />
Applying…
</>
) : (
'Apply Preset'
)}
</Button>
</div>
</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,149 @@
'use client';
import { useState } from 'react';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { createPresetAction } from '@/lib/actions/presets';
import type { Configurations } from '@/lib/schemas/config';
import { PRESET_SECTION_LABELS } from '@/lib/schemas/preset';
type SectionKey = keyof Configurations;
const ALL_SECTIONS: SectionKey[] = ['configuration', 'assistRules', 'event', 'eventRules', 'settings'];
interface SavePresetModalProps {
isOpen: boolean;
onClose: () => void;
configurations: Configurations;
}
export function SavePresetModal({ isOpen, onClose, configurations }: SavePresetModalProps) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [selectedSections, setSelectedSections] = useState<Set<SectionKey>>(
new Set(ALL_SECTIONS)
);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);
const toggleSection = (key: SectionKey) =>
setSelectedSections((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
const handleClose = () => {
if (isSaving) return;
setName('');
setDescription('');
setSelectedSections(new Set(ALL_SECTIONS));
setError(null);
setSaved(false);
onClose();
};
const handleSave = async () => {
if (!name.trim() || selectedSections.size === 0) return;
setIsSaving(true);
setError(null);
const result = await createPresetAction({
name: name.trim(),
description: description.trim() || undefined,
configuration: selectedSections.has('configuration') ? (configurations.configuration as Record<string, unknown>) : null,
assistRules: selectedSections.has('assistRules') ? (configurations.assistRules as Record<string, unknown>) : null,
event: selectedSections.has('event') ? (configurations.event as Record<string, unknown>) : null,
eventRules: selectedSections.has('eventRules') ? (configurations.eventRules as Record<string, unknown>) : null,
settings: selectedSections.has('settings') ? (configurations.settings as Record<string, unknown>) : null
});
setIsSaving(false);
if (!result.success) {
setError(result.message);
return;
}
setSaved(true);
setTimeout(() => handleClose(), 1200);
};
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Save as Preset">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-secondary">Name *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Sprint Race — Spa"
className="form-input text-sm"
disabled={isSaving || saved}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-secondary">Description</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description..."
className="form-input text-sm"
disabled={isSaving || saved}
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-xs font-medium text-secondary">Include sections</label>
<div className="flex flex-col gap-1.5">
{ALL_SECTIONS.map((key) => (
<label key={key} className="flex cursor-pointer items-center gap-2.5 text-sm text-secondary">
<input
type="checkbox"
checked={selectedSections.has(key)}
onChange={() => toggleSection(key)}
disabled={isSaving || saved}
className="h-4 w-4 rounded border-border bg-overlay accent-orange"
/>
{PRESET_SECTION_LABELS[key]}
</label>
))}
</div>
</div>
{error && <p className="text-sm text-red">{error}</p>}
{saved && <p className="text-sm text-green">Preset saved!</p>}
<div className="flex justify-end gap-2">
<Button variant="ghost" size="md" onClick={handleClose} disabled={isSaving}>
Cancel
</Button>
<Button
variant="primary"
size="md"
onClick={handleSave}
disabled={!name.trim() || selectedSections.size === 0 || isSaving || saved}
>
{isSaving ? (
<>
<LoadingSpinner size="sm" />
Saving…
</>
) : (
'Save Preset'
)}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -10,10 +10,15 @@ import { StatisticsDashboard } from '@/components/statistics/StatisticsDashboard
import { LeaderboardManager } from '@/components/leaderboard/LeaderboardManager';
import { ActivityLogTable } from '@/components/activityLog/ActivityLogTable';
import { ServerLogViewer } from '@/components/server/ServerLogViewer';
import { PresetPickerModal } from '@/components/server/PresetPickerModal';
import { SavePresetModal } from '@/components/server/SavePresetModal';
import { useState } from 'react';
import { StateHistoryStats } from '@/lib/schemas';
import { Leaderboard } from '@/lib/schemas/leaderboard';
import { bulkUpdateConfigurationsAction } from '@/lib/actions/configuration';
import {
bulkUpdateConfigurationsAction,
getConfigurationsAction
} from '@/lib/actions/configuration';
interface ServerConfigurationTabsProps {
serverId: string;
@@ -56,10 +61,23 @@ export function ServerConfigurationTabs({
const [settingsData, setSettingsData] = useState(configurations.settings);
const [dirtyConfigs, setDirtyConfigs] = useState<Set<keyof Configurations>>(new Set());
const [restart, setRestart] = useState(true);
const [restart, setRestart] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [saveSuccess, setSaveSuccess] = useState(false);
const [isPresetModalOpen, setIsPresetModalOpen] = useState(false);
const [isSavePresetModalOpen, setIsSavePresetModalOpen] = useState(false);
const reloadConfigs = async () => {
const result = await getConfigurationsAction(serverId);
if (!result.success) return;
setConfigData(result.data.configuration);
setAssistData(result.data.assistRules);
setEventData(result.data.event);
setEventRulesData(result.data.eventRules);
setSettingsData(result.data.settings);
setDirtyConfigs(new Set());
};
const markDirty = (key: keyof Configurations) =>
setDirtyConfigs((prev) => new Set(prev).add(key));
@@ -103,7 +121,10 @@ export function ServerConfigurationTabs({
<ConfigurationEditor
formData={configData}
disabled={isSubmitting}
onFormDataChange={(data) => { setConfigData(data); markDirty('configuration'); }}
onFormDataChange={(data) => {
setConfigData(data);
markDirty('configuration');
}}
/>
);
@@ -112,7 +133,10 @@ export function ServerConfigurationTabs({
<AssistRulesEditor
formData={assistData}
disabled={isSubmitting}
onFormDataChange={(data) => { setAssistData(data); markDirty('assistRules'); }}
onFormDataChange={(data) => {
setAssistData(data);
markDirty('assistRules');
}}
/>
);
@@ -121,7 +145,10 @@ export function ServerConfigurationTabs({
<EventConfigEditor
formData={eventData}
disabled={isSubmitting}
onFormDataChange={(data) => { setEventData(data); markDirty('event'); }}
onFormDataChange={(data) => {
setEventData(data);
markDirty('event');
}}
/>
);
@@ -130,7 +157,10 @@ export function ServerConfigurationTabs({
<EventRulesEditor
formData={eventRulesData}
disabled={isSubmitting}
onFormDataChange={(data) => { setEventRulesData(data); markDirty('eventRules'); }}
onFormDataChange={(data) => {
setEventRulesData(data);
markDirty('eventRules');
}}
/>
);
@@ -139,7 +169,10 @@ export function ServerConfigurationTabs({
<ServerSettingsEditor
formData={settingsData}
disabled={isSubmitting}
onFormDataChange={(data) => { setSettingsData(data); markDirty('settings'); }}
onFormDataChange={(data) => {
setSettingsData(data);
markDirty('settings');
}}
/>
);
@@ -165,7 +198,10 @@ export function ServerConfigurationTabs({
return (
<div className="flex flex-1 flex-col overflow-hidden">
<nav className="flex overflow-x-auto border-b border-border-muted px-5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" aria-label="Tabs">
<nav
className="border-border-muted flex overflow-x-auto border-b px-5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
aria-label="Tabs"
>
{tabs.map((tab) => {
const isActive = currentTab === tab.id;
return (
@@ -175,7 +211,7 @@ export function ServerConfigurationTabs({
className={`flex shrink-0 items-center gap-1.5 border-b-2 px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
isActive
? 'border-orange text-primary'
: 'border-transparent text-muted hover:text-primary'
: 'text-muted hover:text-primary border-transparent'
}`}
>
<span className="text-sm">{tab.icon}</span>
@@ -186,29 +222,39 @@ export function ServerConfigurationTabs({
</nav>
{isConfigTab && (
<div className="flex items-center justify-between border-b border-border-muted bg-overlay px-5 py-3">
<div className="border-border-muted bg-overlay flex items-center justify-between border-b px-5 py-3">
<div className="flex items-center gap-3">
{saveError && (
<p className="text-sm text-red">{saveError}</p>
)}
{saveSuccess && (
<p className="text-sm text-green">All configurations saved.</p>
)}
{saveError && <p className="text-red text-sm">{saveError}</p>}
{saveSuccess && <p className="text-green text-sm">All configurations saved.</p>}
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm text-muted">
<label className="text-muted flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
className="border-border bg-overlay accent-green focus:ring-blue h-4 w-4 rounded focus:ring-1"
/>
Restart after saving
</label>
<button
onClick={() => setIsSavePresetModalOpen(true)}
disabled={isSubmitting}
className="border-border text-secondary hover:bg-hover rounded-md border px-4 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40"
>
Save as Preset
</button>
<button
onClick={() => setIsPresetModalOpen(true)}
disabled={isSubmitting}
className="border-border text-secondary hover:bg-hover rounded-md border px-4 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40"
>
Load Preset
</button>
<button
onClick={handleSaveAll}
disabled={isSubmitting || dirtyConfigs.size === 0}
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
className="bg-btn-green border-btn-green hover:bg-btn-green-hover rounded-md border px-5 py-2 text-sm font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save All'}
</button>
@@ -217,6 +263,29 @@ export function ServerConfigurationTabs({
)}
<div className="flex-1 overflow-y-auto p-5">{renderTabContent()}</div>
<SavePresetModal
isOpen={isSavePresetModalOpen}
onClose={() => setIsSavePresetModalOpen(false)}
configurations={{
configuration: configData,
assistRules: assistData,
event: eventData,
eventRules: eventRulesData,
settings: settingsData
}}
/>
<PresetPickerModal
isOpen={isPresetModalOpen}
onClose={() => setIsPresetModalOpen(false)}
serverId={serverId}
onApplied={async () => {
await reloadConfigs();
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
}}
/>
</div>
);
}

View File

@@ -19,7 +19,7 @@ export async function loginAction(prevState: LoginResult, formData: FormData) {
if (!loginData.success) {
return {
success: false,
message: loginData.error.message
message: loginData.error.issues[0]?.message ?? 'Validation failed'
};
}

View File

@@ -2,7 +2,7 @@
import { revalidatePath } from 'next/cache';
import { requireAuth } from '@/lib/auth/server';
import { updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration';
import { getServerConfigurations, updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration';
import {
assistRulesSchema,
ConfigFile,
@@ -19,6 +19,21 @@ import type {
EventRules,
ServerSettings
} from '@/lib/schemas/config';
export async function getConfigurationsAction(
serverId: string
): Promise<{ success: true; data: Configurations } | { success: false; message: string }> {
try {
const session = await requireAuth();
const data = await getServerConfigurations(session.token!, serverId);
return { success: true, data };
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to fetch configurations'
};
}
}
import { boolToInt } from '@/lib/utils';
export async function updateConfigurationAction(serverId: string, formData: FormData) {
@@ -72,7 +87,7 @@ export async function updateAssistRulesAction(serverId: string, formData: FormDa
const config = assistRulesSchema.safeParse(rawConfig);
if (!config.success) {
return { success: false, message: config.error.message };
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
}
await updateServerConfiguration(
@@ -120,7 +135,7 @@ export async function updateServerSettingsAction(serverId: string, formData: For
};
const config = serverSettingsSchema.safeParse(rawConfig);
if (!config.success) {
return { success: false, message: config.error.message };
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
}
await updateServerConfiguration(
@@ -167,7 +182,7 @@ export async function updateEventConfigAction(serverId: string, formData: FormDa
};
const config = eventConfigSchema.safeParse(rawConfig);
if (!config.success) {
return { success: false, message: config.error.message };
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
}
await updateServerConfiguration(
@@ -216,7 +231,7 @@ export async function updateEventRulesAction(serverId: string, formData: FormDat
const config = eventRulesSchema.safeParse(rawConfig);
if (!config.success) {
return { success: false, message: config.error.message };
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
}
await updateServerConfiguration(
@@ -247,7 +262,7 @@ export async function bulkUpdateConfigurationsAction(
const validated = configurationsSchema.partial().safeParse(configs);
if (!validated.success) {
return { success: false, message: validated.error.message };
return { success: false, message: validated.error.issues[0]?.message ?? 'Validation failed' };
}
await bulkUpdateServerConfigurations(session.token!, serverId, validated.data, restart);

View File

@@ -10,7 +10,7 @@ export async function saveLeaderboardAction(serverId: string, data: Leaderboard)
const session = await requireAuth();
const validated = leaderboardSchema.safeParse(data);
if (!validated.success) {
return { success: false, message: validated.error.message };
return { success: false, message: validated.error.issues[0]?.message ?? 'Validation failed' };
}
await fetchServerAPI(`/server/${serverId}/leaderboard`, session.token!, 'PUT', validated.data);
revalidatePath(`/dashboard/server/${serverId}`);

View File

@@ -15,7 +15,7 @@ export async function createUserAction(formData: FormData) {
const rawData = { username, password, role };
const data = userCreateSchema.safeParse(rawData);
if (!data.success) {
return { success: false, message: data.error.message };
return { success: false, message: data.error.issues[0]?.message ?? 'Validation failed' };
}
await createUser(session.token!, data.data);

View File

@@ -0,0 +1,54 @@
'use server';
import { revalidatePath } from 'next/cache';
import { requireAuth } from '@/lib/auth/server';
import { listPresets, createPreset, applyPreset, type ConfigPresetCreateRequest } from '@/lib/api/server/presets';
import type { ConfigPreset } from '@/lib/schemas/preset';
export async function listPresetsAction(
name?: string
): Promise<{ success: true; data: ConfigPreset[] } | { success: false; message: string }> {
try {
const session = await requireAuth();
const data = await listPresets(session.token!, name);
return { success: true, data };
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to fetch presets'
};
}
}
export async function createPresetAction(
data: ConfigPresetCreateRequest
): Promise<{ success: true; data: ConfigPreset } | { success: false; message: string }> {
try {
const session = await requireAuth();
const preset = await createPreset(session.token!, data);
return { success: true, data: preset };
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to save preset'
};
}
}
export async function applyPresetAction(
serverId: string,
presetId: string,
restart = false
): Promise<{ success: true; message: string } | { success: false; message: string }> {
try {
const session = await requireAuth();
await applyPreset(session.token!, serverId, presetId, restart);
revalidatePath(`/dashboard/server/${serverId}`);
return { success: true, message: 'Preset applied successfully' };
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to apply preset'
};
}
}

View File

@@ -30,7 +30,14 @@ export async function fetchServerAPI<T>(
if (response.status == 401) {
redirect('/login?expired=true');
}
throw new Error(`API Error: ${response.statusText} - ${method} - ${BASE_URL}${endpoint}`);
let message = response.statusText;
try {
const body = await response.json();
message = body?.error ?? body?.message ?? message;
} catch {
// non-JSON error body, keep statusText
}
throw new Error(message);
}
if (response.headers.get('Content-Type')?.includes('application/json')) {

View File

@@ -0,0 +1,43 @@
import { fetchServerAPI } from './base';
import { configPresetSchema, type ConfigPreset } from '@/lib/schemas/preset';
import type { ConfigAuditRecord } from './configuration';
const route = '/presets';
export type ConfigPresetCreateRequest = {
name: string;
description?: string;
configuration?: Record<string, unknown> | null;
assistRules?: Record<string, unknown> | null;
event?: Record<string, unknown> | null;
eventRules?: Record<string, unknown> | null;
settings?: Record<string, unknown> | null;
};
export async function createPreset(
token: string,
data: ConfigPresetCreateRequest
): Promise<ConfigPreset> {
const response = await fetchServerAPI<ConfigPreset>(route, token, 'POST', data);
return configPresetSchema.parse(response.data);
}
export async function listPresets(token: string, name?: string): Promise<ConfigPreset[]> {
const query = name ? `?name=${encodeURIComponent(name)}` : '';
const response = await fetchServerAPI<ConfigPreset[]>(`${route}${query}`, token);
return configPresetSchema.array().parse(response.data);
}
export async function applyPreset(
token: string,
serverId: string,
presetId: string,
restart = false
): Promise<ConfigAuditRecord[]> {
const response = await fetchServerAPI<ConfigAuditRecord[]>(
`/server/${serverId}/config/presets/${presetId}/apply?restart=${restart}`,
token,
'POST'
);
return response.data ?? [];
}

34
src/lib/schemas/preset.ts Normal file
View File

@@ -0,0 +1,34 @@
import * as z from 'zod';
export const configPresetSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
configuration: z.string().nullable(),
assistRules: z.string().nullable(),
event: z.string().nullable(),
eventRules: z.string().nullable(),
settings: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string()
});
export type ConfigPreset = z.infer<typeof configPresetSchema>;
export const PRESET_SECTION_LABELS: Record<string, string> = {
configuration: 'Configuration',
assistRules: 'Assist Rules',
event: 'Event Config',
eventRules: 'Event Rules',
settings: 'Server Settings'
};
export function getPresetSections(preset: ConfigPreset): string[] {
const sections: string[] = [];
if (preset.configuration) sections.push('configuration');
if (preset.assistRules) sections.push('assistRules');
if (preset.event) sections.push('event');
if (preset.eventRules) sections.push('eventRules');
if (preset.settings) sections.push('settings');
return sections;
}