fix: error handling

This commit is contained in:
2026-03-26 22:32:45 +01:00
parent e8cb877497
commit 64acf24f27
14 changed files with 204 additions and 245 deletions

View File

@@ -39,6 +39,9 @@ function parseDetails(raw: string): string {
if (obj.driver_count !== undefined && obj.race_count !== undefined) { if (obj.driver_count !== undefined && obj.race_count !== undefined) {
return `${obj.driver_count} drivers, ${obj.race_count} races`; return `${obj.driver_count} drivers, ${obj.race_count} races`;
} }
if (obj.preset_id) {
return `Preset: ${obj.preset_name} - Sections applied: ${obj.sections_applied}`;
}
return raw; return raw;
} catch { } catch {
return raw; return raw;
@@ -50,7 +53,9 @@ const ACTION_BADGE: Record<ActionType, string> = {
server_start: 'bg-green-bg text-green', server_start: 'bg-green-bg text-green',
server_stop: 'bg-red-bg text-red', server_stop: 'bg-red-bg text-red',
server_restart: 'bg-yellow-bg text-yellow', server_restart: 'bg-yellow-bg text-yellow',
leaderboard_update: 'bg-overlay text-muted' leaderboard_update: 'bg-overlay text-muted',
preset_create: 'bg-green-bg text-green',
preset_apply: 'bg-blue-bg text-blue'
}; };
export function ActivityLogTable({ serverId, servers }: ActivityLogTableProps) { export function ActivityLogTable({ serverId, servers }: ActivityLogTableProps) {

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import type { AssistRules } from '@/lib/schemas/config'; import type { AssistRules } from '@/lib/schemas/config';
import { FIELD_LABELS } from '@/lib/schemas/config';
interface AssistRulesEditorProps { interface AssistRulesEditorProps {
formData: AssistRules; formData: AssistRules;
@@ -8,52 +9,16 @@ interface AssistRulesEditorProps {
onFormDataChange: (data: AssistRules) => void; onFormDataChange: (data: AssistRules) => void;
} }
const assistFields = [ const assistFields: { key: keyof AssistRules; type: 'number' | 'select' }[] = [
{ { key: 'stabilityControlLevelMax', type: 'number' },
key: 'stabilityControlLevelMax' as keyof AssistRules, { key: 'disableAutosteer', type: 'select' },
label: 'Stability Control Level Max', { key: 'disableAutoLights', type: 'select' },
type: 'number' { key: 'disableAutoWiper', type: 'select' },
}, { key: 'disableAutoEngineStart', type: 'select' },
{ { key: 'disableAutoPitLimiter', type: 'select' },
key: 'disableAutosteer' as keyof AssistRules, { key: 'disableAutoGear', type: 'select' },
label: 'Disable Autosteer', { key: 'disableAutoClutch', type: 'select' },
type: 'select' { key: 'disableIdealLine', type: 'select' }
},
{
key: 'disableAutoLights' as keyof AssistRules,
label: 'Disable Auto Lights',
type: 'select'
},
{
key: 'disableAutoWiper' as keyof AssistRules,
label: 'Disable Auto Wiper',
type: 'select'
},
{
key: 'disableAutoEngineStart' as keyof AssistRules,
label: 'Disable Auto Engine Start',
type: 'select'
},
{
key: 'disableAutoPitLimiter' as keyof AssistRules,
label: 'Disable Auto Pit Limiter',
type: 'select'
},
{
key: 'disableAutoGear' as keyof AssistRules,
label: 'Disable Auto Gear',
type: 'select'
},
{
key: 'disableAutoClutch' as keyof AssistRules,
label: 'Disable Auto Clutch',
type: 'select'
},
{
key: 'disableIdealLine' as keyof AssistRules,
label: 'Disable Ideal Line',
type: 'select'
}
]; ];
export function AssistRulesEditor({ formData, disabled, onFormDataChange }: AssistRulesEditorProps) { export function AssistRulesEditor({ formData, disabled, onFormDataChange }: AssistRulesEditorProps) {
@@ -67,9 +32,9 @@ export function AssistRulesEditor({ formData, disabled, onFormDataChange }: Assi
return ( return (
<div className="max-w-3xl space-y-6"> <div className="max-w-3xl space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{assistFields.map(({ key, label, type }) => ( {assistFields.map(({ key, type }) => (
<div key={key}> <div key={key}>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
{type === 'number' ? ( {type === 'number' ? (
<input <input
type="number" type="number"

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import type { Configuration } from '@/lib/schemas/config'; import type { Configuration } from '@/lib/schemas/config';
import { FIELD_LABELS } from '@/lib/schemas/config';
interface ConfigurationEditorProps { interface ConfigurationEditorProps {
formData: Configuration; formData: Configuration;
@@ -20,7 +21,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
<div className="max-w-3xl space-y-6"> <div className="max-w-3xl space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">UDP Port</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.udpPort}</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -31,7 +32,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">TCP Port</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.tcpPort}</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -42,7 +43,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">Max Connections</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.maxConnections}</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -53,7 +54,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">LAN Discovery</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.lanDiscovery}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.lanDiscovery} value={formData.lanDiscovery}
@@ -66,7 +67,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">Register To Lobby</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.registerToLobby}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.registerToLobby} value={formData.registerToLobby}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import type { EventConfig, Session } from '@/lib/schemas/config'; import type { EventConfig, Session } from '@/lib/schemas/config';
import { FIELD_LABELS } from '@/lib/schemas/config';
import { TRACKS } from '@/lib/constants/tracks'; import { TRACKS } from '@/lib/constants/tracks';
interface EventConfigEditorProps { interface EventConfigEditorProps {
@@ -63,7 +64,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">Track</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.track}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.track} value={formData.track}
@@ -79,9 +80,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.preRaceWaitingTimeSeconds}</label>
Pre-Race Waiting Time (seconds)
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -93,9 +92,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.sessionOverTimeSeconds}</label>
Session Over Time (seconds)
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -107,9 +104,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.postQualySeconds}</label>
Post Qualify Seconds
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -121,9 +116,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.postRaceSeconds}</label>
Post Race Seconds
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -142,9 +135,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.ambientTemp}</label>
Ambient Temperature (°C)
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -157,9 +148,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.cloudLevel}</label>
Cloud Level (0.0-1.0)
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -173,7 +162,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">Rain (0.0-1.0)</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.rain}</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -187,9 +176,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.weatherRandomness}</label>
Weather Randomness
</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -202,9 +189,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.simracerWeatherConditions}</label>
Simracer Weather Conditions
</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.simracerWeatherConditions} value={formData.simracerWeatherConditions}
@@ -217,9 +202,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.isFixedConditionQualification}</label>
Fixed Condition Qualification
</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.isFixedConditionQualification} value={formData.isFixedConditionQualification}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import type { EventRules } from '@/lib/schemas/config'; import type { EventRules } from '@/lib/schemas/config';
import { FIELD_LABELS } from '@/lib/schemas/config';
interface EventRulesEditorProps { interface EventRulesEditorProps {
formData: EventRules; formData: EventRules;
@@ -8,63 +9,21 @@ interface EventRulesEditorProps {
onFormDataChange: (data: EventRules) => void; onFormDataChange: (data: EventRules) => void;
} }
const numberFields = [ const numberFields: { key: keyof EventRules; min: number; max?: number }[] = [
{ { key: 'pitWindowLengthSec', min: -1 },
key: 'pitWindowLengthSec' as keyof EventRules, { key: 'driverStintTimeSec', min: -1 },
label: 'Pit Window Length (seconds)', { key: 'mandatoryPitstopCount', min: 0, max: 5 },
min: -1 { key: 'maxTotalDrivingTime', min: -1 },
}, { key: 'maxDriversCount', min: 1, max: 10 },
{ { key: 'tyreSetCount', min: 0, max: 50 }
key: 'driverStintTimeSec' as keyof EventRules,
label: 'Driver Stint Time (seconds)',
min: -1
},
{
key: 'mandatoryPitstopCount' as keyof EventRules,
label: 'Mandatory Pitstop Count',
min: 0,
max: 5
},
{
key: 'maxTotalDrivingTime' as keyof EventRules,
label: 'Max Total Driving Time (seconds)',
min: -1
},
{
key: 'maxDriversCount' as keyof EventRules,
label: 'Max Drivers Count',
min: 1,
max: 10
},
{
key: 'tyreSetCount' as keyof EventRules,
label: 'Tyre Set Count',
min: 0,
max: 50
}
]; ];
const booleanFields = [ const booleanFields: { key: keyof EventRules }[] = [
{ { key: 'isRefuellingAllowedInRace' },
key: 'isRefuellingAllowedInRace' as keyof EventRules, { key: 'isRefuellingTimeFixed' },
label: 'Refuelling Allowed in Race' { key: 'isMandatoryPitstopRefuellingRequired' },
}, { key: 'isMandatoryPitstopTyreChangeRequired' },
{ { key: 'isMandatoryPitstopSwapDriverRequired' }
key: 'isRefuellingTimeFixed' as keyof EventRules,
label: 'Refuelling Time Fixed'
},
{
key: 'isMandatoryPitstopRefuellingRequired' as keyof EventRules,
label: 'Mandatory Pitstop Refuelling Required'
},
{
key: 'isMandatoryPitstopTyreChangeRequired' as keyof EventRules,
label: 'Mandatory Pitstop Tyre Change Required'
},
{
key: 'isMandatoryPitstopSwapDriverRequired' as keyof EventRules,
label: 'Mandatory Pitstop Swap Driver Required'
}
]; ];
export function EventRulesEditor({ formData, disabled, onFormDataChange }: EventRulesEditorProps) { export function EventRulesEditor({ formData, disabled, onFormDataChange }: EventRulesEditorProps) {
@@ -78,7 +37,7 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">Race Rules</h3> <h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">Race Rules</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">Qualify Standing Type</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.qualifyStandingType}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.qualifyStandingType} value={formData.qualifyStandingType}
@@ -89,9 +48,9 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
<option value={2}>Average Lap</option> <option value={2}>Average Lap</option>
</select> </select>
</div> </div>
{numberFields.map(({ key, label, min, max }) => ( {numberFields.map(({ key, min, max }) => (
<div key={key}> <div key={key}>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -111,9 +70,9 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
Pitstop & Refuelling Rules Pitstop & Refuelling Rules
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{booleanFields.map(({ key, label }) => ( {booleanFields.map(({ key }) => (
<div key={key}> <div key={key}>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData[key] ? 'true' : 'false'} value={formData[key] ? 'true' : 'false'}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import type { ServerSettings } from '@/lib/schemas/config'; import type { ServerSettings } from '@/lib/schemas/config';
import { FIELD_LABELS } from '@/lib/schemas/config';
interface ServerSettingsEditorProps { interface ServerSettingsEditorProps {
formData: ServerSettings; formData: ServerSettings;
@@ -8,86 +9,31 @@ interface ServerSettingsEditorProps {
onFormDataChange: (data: ServerSettings) => void; onFormDataChange: (data: ServerSettings) => void;
} }
const textFields = [ const textFields: { key: keyof ServerSettings; type: 'text' | 'password' }[] = [
{ { key: 'serverName', type: 'text' },
key: 'serverName' as keyof ServerSettings, { key: 'adminPassword', type: 'password' },
label: 'Server Name', { key: 'password', type: 'password' },
type: 'text' { key: 'spectatorPassword', type: 'password' },
}, { key: 'centralEntryListPath', type: 'text' }
{
key: 'adminPassword' as keyof ServerSettings,
label: 'Admin Password',
type: 'password'
},
{
key: 'password' as keyof ServerSettings,
label: 'Password',
type: 'password'
},
{
key: 'spectatorPassword' as keyof ServerSettings,
label: 'Spectator Password',
type: 'password'
},
{
key: 'centralEntryListPath' as keyof ServerSettings,
label: 'Central Entry List Path',
type: 'text'
}
]; ];
const carGroups = ['FreeForAll', 'GT3', 'GT4', 'GT2', 'GTC', 'TCX']; const carGroups = ['FreeForAll', 'GT3', 'GT4', 'GT2', 'GTC', 'TCX'];
const numberFields = [ const numberFields: { key: keyof ServerSettings; min: number; max: number }[] = [
{ { key: 'trackMedalsRequirement', min: 0, max: 3 },
key: 'trackMedalsRequirement' as keyof ServerSettings, { key: 'safetyRatingRequirement', min: -1, max: 99 },
label: 'Track Medals Requirement', { key: 'racecraftRatingRequirement', min: -1, max: 99 },
min: 0, { key: 'maxCarSlots', min: 1, max: 30 }
max: 3
},
{
key: 'safetyRatingRequirement' as keyof ServerSettings,
label: 'Safety Rating Requirement',
min: -1,
max: 99
},
{
key: 'racecraftRatingRequirement' as keyof ServerSettings,
label: 'Racecraft Rating Requirement',
min: -1,
max: 99
},
{
key: 'maxCarSlots' as keyof ServerSettings,
label: 'Max Car Slots',
min: 1,
max: 30
}
]; ];
const selectFields = [ const selectFields: { key: keyof ServerSettings }[] = [
{ { key: 'dumpLeaderboards' },
key: 'dumpLeaderboards' as keyof ServerSettings, { key: 'isRaceLocked' },
label: 'Dump Leaderboards' { key: 'randomizeTrackWhenEmpty' },
}, { key: 'allowAutoDQ' },
{ key: 'isRaceLocked' as keyof ServerSettings, label: 'Race Locked' }, { key: 'shortFormationLap' },
{ { key: 'dumpEntryList' },
key: 'randomizeTrackWhenEmpty' as keyof ServerSettings, { key: 'ignorePrematureDisconnects' }
label: 'Randomize Track When Empty'
},
{ key: 'allowAutoDQ' as keyof ServerSettings, label: 'Allow Auto DQ' },
{
key: 'shortFormationLap' as keyof ServerSettings,
label: 'Short Formation Lap'
},
{
key: 'dumpEntryList' as keyof ServerSettings,
label: 'Dump Entry List'
},
{
key: 'ignorePrematureDisconnects' as keyof ServerSettings,
label: 'Ignore Premature Disconnects'
}
]; ];
export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: ServerSettingsEditorProps) { export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: ServerSettingsEditorProps) {
@@ -102,9 +48,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
Basic Settings Basic Settings
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{textFields.map(({ key, label, type }) => ( {textFields.map(({ key, type }) => (
<div key={key}> <div key={key}>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
<input <input
type={type} type={type}
disabled={disabled} disabled={disabled}
@@ -116,7 +62,7 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
))} ))}
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary">Car Group</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.carGroup}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.carGroup} value={formData.carGroup}
@@ -138,9 +84,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
Requirements & Limits Requirements & Limits
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{numberFields.map(({ key, label, min, max }) => ( {numberFields.map(({ key, min, max }) => (
<div key={key}> <div key={key}>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
<input <input
type="number" type="number"
disabled={disabled} disabled={disabled}
@@ -160,9 +106,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
Race Options Race Options
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{selectFields.map(({ key, label }) => ( {selectFields.map(({ key }) => (
<div key={key}> <div key={key}>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData[key] as number} value={formData[key] as number}
@@ -176,9 +122,7 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
))} ))}
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-secondary"> <label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.formationLapType}</label>
Formation Lap Type
</label>
<select <select
disabled={disabled} disabled={disabled}
value={formData.formationLapType} value={formData.formationLapType}

View File

@@ -224,7 +224,13 @@ export function ServerConfigurationTabs({
{isConfigTab && ( {isConfigTab && (
<div className="border-border-muted bg-overlay flex items-center justify-between border-b 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"> <div className="flex items-center gap-3">
{saveError && <p className="text-red text-sm">{saveError}</p>} {saveError && (
<ul className="flex flex-col gap-0.5">
{saveError.split('\n').map((line, i) => (
<li key={i} className="text-red text-sm">• {line}</li>
))}
</ul>
)}
{saveSuccess && <p className="text-green text-sm">All configurations saved.</p>} {saveSuccess && <p className="text-green text-sm">All configurations saved.</p>}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">

View File

@@ -3,6 +3,7 @@
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { loginUser, getOpenToken } from '@/lib/api/server/auth'; import { loginUser, getOpenToken } from '@/lib/api/server/auth';
import { login, logout } from '@/lib/auth/server'; import { login, logout } from '@/lib/auth/server';
import { formatZodIssues } from '@/lib/utils';
import { loginSchema, loginResponseSchema } from '../schemas'; import { loginSchema, loginResponseSchema } from '../schemas';
export type LoginResult = { export type LoginResult = {
@@ -19,7 +20,7 @@ export async function loginAction(prevState: LoginResult, formData: FormData) {
if (!loginData.success) { if (!loginData.success) {
return { return {
success: false, success: false,
message: loginData.error.issues[0]?.message ?? 'Validation failed' message: formatZodIssues(loginData.error.issues)
}; };
} }

View File

@@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { requireAuth } from '@/lib/auth/server'; import { requireAuth } from '@/lib/auth/server';
import { getServerConfigurations, updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration'; import { getServerConfigurations, updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration';
import { boolToInt, formatZodIssues } from '@/lib/utils';
import { import {
assistRulesSchema, assistRulesSchema,
ConfigFile, ConfigFile,
@@ -34,7 +35,6 @@ export async function getConfigurationsAction(
}; };
} }
} }
import { boolToInt } from '@/lib/utils';
export async function updateConfigurationAction(serverId: string, formData: FormData) { export async function updateConfigurationAction(serverId: string, formData: FormData) {
try { try {
@@ -87,7 +87,7 @@ export async function updateAssistRulesAction(serverId: string, formData: FormDa
const config = assistRulesSchema.safeParse(rawConfig); const config = assistRulesSchema.safeParse(rawConfig);
if (!config.success) { if (!config.success) {
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(config.error.issues) };
} }
await updateServerConfiguration( await updateServerConfiguration(
@@ -135,7 +135,7 @@ export async function updateServerSettingsAction(serverId: string, formData: For
}; };
const config = serverSettingsSchema.safeParse(rawConfig); const config = serverSettingsSchema.safeParse(rawConfig);
if (!config.success) { if (!config.success) {
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(config.error.issues) };
} }
await updateServerConfiguration( await updateServerConfiguration(
@@ -182,7 +182,7 @@ export async function updateEventConfigAction(serverId: string, formData: FormDa
}; };
const config = eventConfigSchema.safeParse(rawConfig); const config = eventConfigSchema.safeParse(rawConfig);
if (!config.success) { if (!config.success) {
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(config.error.issues) };
} }
await updateServerConfiguration( await updateServerConfiguration(
@@ -231,7 +231,7 @@ export async function updateEventRulesAction(serverId: string, formData: FormDat
const config = eventRulesSchema.safeParse(rawConfig); const config = eventRulesSchema.safeParse(rawConfig);
if (!config.success) { if (!config.success) {
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(config.error.issues) };
} }
await updateServerConfiguration( await updateServerConfiguration(
@@ -262,7 +262,7 @@ export async function bulkUpdateConfigurationsAction(
const validated = configurationsSchema.partial().safeParse(configs); const validated = configurationsSchema.partial().safeParse(configs);
if (!validated.success) { if (!validated.success) {
return { success: false, message: validated.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(validated.error.issues) };
} }
await bulkUpdateServerConfigurations(session.token!, serverId, validated.data, restart); await bulkUpdateServerConfigurations(session.token!, serverId, validated.data, restart);

View File

@@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { requireAuth } from '@/lib/auth/server'; import { requireAuth } from '@/lib/auth/server';
import { fetchServerAPI } from '@/lib/api/server/base'; import { fetchServerAPI } from '@/lib/api/server/base';
import { formatZodIssues } from '@/lib/utils';
import { leaderboardSchema, type Leaderboard } from '@/lib/schemas/leaderboard'; import { leaderboardSchema, type Leaderboard } from '@/lib/schemas/leaderboard';
export async function saveLeaderboardAction(serverId: string, data: Leaderboard) { export async function saveLeaderboardAction(serverId: string, data: Leaderboard) {
@@ -10,7 +11,7 @@ export async function saveLeaderboardAction(serverId: string, data: Leaderboard)
const session = await requireAuth(); const session = await requireAuth();
const validated = leaderboardSchema.safeParse(data); const validated = leaderboardSchema.safeParse(data);
if (!validated.success) { if (!validated.success) {
return { success: false, message: validated.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(validated.error.issues) };
} }
await fetchServerAPI(`/server/${serverId}/leaderboard`, session.token!, 'PUT', validated.data); await fetchServerAPI(`/server/${serverId}/leaderboard`, session.token!, 'PUT', validated.data);
revalidatePath(`/dashboard/server/${serverId}`); revalidatePath(`/dashboard/server/${serverId}`);

View File

@@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { requireAuth } from '@/lib/auth/server'; import { requireAuth } from '@/lib/auth/server';
import { createUser, deleteUser } from '@/lib/api/server/membership'; import { createUser, deleteUser } from '@/lib/api/server/membership';
import { formatZodIssues } from '@/lib/utils';
import { userCreateSchema } from '../schemas'; import { userCreateSchema } from '../schemas';
export async function createUserAction(formData: FormData) { export async function createUserAction(formData: FormData) {
@@ -15,7 +16,7 @@ export async function createUserAction(formData: FormData) {
const rawData = { username, password, role }; const rawData = { username, password, role };
const data = userCreateSchema.safeParse(rawData); const data = userCreateSchema.safeParse(rawData);
if (!data.success) { if (!data.success) {
return { success: false, message: data.error.issues[0]?.message ?? 'Validation failed' }; return { success: false, message: formatZodIssues(data.error.issues) };
} }
await createUser(session.token!, data.data); await createUser(session.token!, data.data);

View File

@@ -5,16 +5,20 @@ export const actionTypeSchema = z.enum([
'server_start', 'server_start',
'server_stop', 'server_stop',
'server_restart', 'server_restart',
'leaderboard_update' 'leaderboard_update',
'preset_create',
'preset_apply'
]); ]);
export type ActionType = z.infer<typeof actionTypeSchema>; export type ActionType = z.infer<typeof actionTypeSchema>;
// Minimal server shape embedded in activity log responses (avoids circular import with server.ts) // Minimal server shape embedded in activity log responses (avoids circular import with server.ts)
const embeddedServerSchema = z.object({ const embeddedServerSchema = z
.object({
id: z.string(), id: z.string(),
name: z.string() name: z.string()
}).passthrough(); })
.passthrough();
export type EmbeddedServer = z.infer<typeof embeddedServerSchema>; export type EmbeddedServer = z.infer<typeof embeddedServerSchema>;
@@ -58,5 +62,7 @@ export const ACTION_TYPE_LABELS: Record<ActionType, string> = {
server_start: 'Server Start', server_start: 'Server Start',
server_stop: 'Server Stop', server_stop: 'Server Stop',
server_restart: 'Server Restart', server_restart: 'Server Restart',
leaderboard_update: 'Leaderboard Update' leaderboard_update: 'Leaderboard Update',
preset_create: 'Preset Create',
preset_apply: 'Preset Apply'
}; };

View File

@@ -138,3 +138,76 @@ export const configSchemaMap = {
}; };
export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings; export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings;
export const SECTION_LABELS: Record<string, string> = {
configuration: 'Configuration',
assistRules: 'Assist Rules',
event: 'Event Config',
eventRules: 'Event Rules',
settings: 'Server Settings'
};
export const FIELD_LABELS: Record<string, string> = {
// configuration.json
udpPort: 'UDP Port',
tcpPort: 'TCP Port',
maxConnections: 'Max Connections',
lanDiscovery: 'LAN Discovery',
registerToLobby: 'Register To Lobby',
configVersion: 'Config Version',
// assistRules.json
stabilityControlLevelMax: 'Stability Control Level Max',
disableAutosteer: 'Disable Autosteer',
disableAutoLights: 'Disable Auto Lights',
disableAutoWiper: 'Disable Auto Wiper',
disableAutoEngineStart: 'Disable Auto Engine Start',
disableAutoPitLimiter: 'Disable Auto Pit Limiter',
disableAutoGear: 'Disable Auto Gear',
disableAutoClutch: 'Disable Auto Clutch',
disableIdealLine: 'Disable Ideal Line',
// event.json
track: 'Track',
preRaceWaitingTimeSeconds: 'Pre-Race Waiting Time (seconds)',
sessionOverTimeSeconds: 'Session Over Time (seconds)',
ambientTemp: 'Ambient Temperature (°C)',
cloudLevel: 'Cloud Level (0.0-1.0)',
rain: 'Rain (0.0-1.0)',
weatherRandomness: 'Weather Randomness',
postQualySeconds: 'Post Qualify Seconds',
postRaceSeconds: 'Post Race Seconds',
simracerWeatherConditions: 'Simracer Weather Conditions',
isFixedConditionQualification: 'Fixed Condition Qualification',
sessions: 'Sessions',
// eventRules.json
qualifyStandingType: 'Qualify Standing Type',
pitWindowLengthSec: 'Pit Window Length (seconds)',
driverStintTimeSec: 'Driver Stint Time (seconds)',
mandatoryPitstopCount: 'Mandatory Pitstop Count',
maxTotalDrivingTime: 'Max Total Driving Time (seconds)',
maxDriversCount: 'Max Drivers Count',
isRefuellingAllowedInRace: 'Refuelling Allowed in Race',
isRefuellingTimeFixed: 'Refuelling Time Fixed',
isMandatoryPitstopRefuellingRequired: 'Mandatory Pitstop Refuelling Required',
isMandatoryPitstopTyreChangeRequired: 'Mandatory Pitstop Tyre Change Required',
isMandatoryPitstopSwapDriverRequired: 'Mandatory Pitstop Swap Driver Required',
tyreSetCount: 'Tyre Set Count',
// settings.json
serverName: 'Server Name',
adminPassword: 'Admin Password',
carGroup: 'Car Group',
trackMedalsRequirement: 'Track Medals Requirement',
safetyRatingRequirement: 'Safety Rating Requirement',
racecraftRatingRequirement: 'Racecraft Rating Requirement',
password: 'Password',
spectatorPassword: 'Spectator Password',
maxCarSlots: 'Max Car Slots',
dumpLeaderboards: 'Dump Leaderboards',
isRaceLocked: 'Race Locked',
randomizeTrackWhenEmpty: 'Randomize Track When Empty',
centralEntryListPath: 'Central Entry List Path',
allowAutoDQ: 'Allow Auto DQ',
shortFormationLap: 'Short Formation Lap',
dumpEntryList: 'Dump Entry List',
formationLapType: 'Formation Lap Type',
ignorePrematureDisconnects: 'Ignore Premature Disconnects'
};

View File

@@ -1,5 +1,6 @@
import { clsx, type ClassValue } from 'clsx'; import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { FIELD_LABELS, SECTION_LABELS } from '@/lib/schemas/config';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -10,5 +11,18 @@ export function boolToInt(val: boolean) {
} }
export function intToBool(val: number) { export function intToBool(val: number) {
return !!val return !!val;
}
export function formatZodIssues(issues: Array<{ path: PropertyKey[]; message: string }>): string {
return issues
.map((issue) => {
const path = issue.path.map(String);
const sectionKey = path.length > 1 ? path[0] : null;
const fieldKey = path.at(-1) ?? '';
const section = sectionKey && SECTION_LABELS[sectionKey] ? `[${SECTION_LABELS[sectionKey]}] ` : '';
const label = FIELD_LABELS[fieldKey] ?? fieldKey;
return `${section}${label}: ${issue.message}`;
})
.join('\n');
} }