fix: error handling
This commit is contained in:
@@ -39,6 +39,9 @@ function parseDetails(raw: string): string {
|
||||
if (obj.driver_count !== undefined && obj.race_count !== undefined) {
|
||||
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;
|
||||
} catch {
|
||||
return raw;
|
||||
@@ -50,7 +53,9 @@ const ACTION_BADGE: Record<ActionType, string> = {
|
||||
server_start: 'bg-green-bg text-green',
|
||||
server_stop: 'bg-red-bg text-red',
|
||||
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) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { AssistRules } from '@/lib/schemas/config';
|
||||
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||
|
||||
interface AssistRulesEditorProps {
|
||||
formData: AssistRules;
|
||||
@@ -8,52 +9,16 @@ interface AssistRulesEditorProps {
|
||||
onFormDataChange: (data: AssistRules) => void;
|
||||
}
|
||||
|
||||
const assistFields = [
|
||||
{
|
||||
key: 'stabilityControlLevelMax' as keyof AssistRules,
|
||||
label: 'Stability Control Level Max',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
key: 'disableAutosteer' as keyof AssistRules,
|
||||
label: 'Disable Autosteer',
|
||||
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'
|
||||
}
|
||||
const assistFields: { key: keyof AssistRules; type: 'number' | 'select' }[] = [
|
||||
{ key: 'stabilityControlLevelMax', type: 'number' },
|
||||
{ key: 'disableAutosteer', type: 'select' },
|
||||
{ key: 'disableAutoLights', type: 'select' },
|
||||
{ key: 'disableAutoWiper', type: 'select' },
|
||||
{ key: 'disableAutoEngineStart', type: 'select' },
|
||||
{ key: 'disableAutoPitLimiter', type: 'select' },
|
||||
{ key: 'disableAutoGear', type: 'select' },
|
||||
{ key: 'disableAutoClutch', type: 'select' },
|
||||
{ key: 'disableIdealLine', type: 'select' }
|
||||
];
|
||||
|
||||
export function AssistRulesEditor({ formData, disabled, onFormDataChange }: AssistRulesEditorProps) {
|
||||
@@ -67,9 +32,9 @@ export function AssistRulesEditor({ formData, disabled, onFormDataChange }: Assi
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{assistFields.map(({ key, label, type }) => (
|
||||
{assistFields.map(({ key, type }) => (
|
||||
<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' ? (
|
||||
<input
|
||||
type="number"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { Configuration } from '@/lib/schemas/config';
|
||||
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||
|
||||
interface ConfigurationEditorProps {
|
||||
formData: Configuration;
|
||||
@@ -20,7 +21,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<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
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -31,7 +32,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
||||
</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
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -42,7 +43,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
||||
</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
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -53,7 +54,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
||||
</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
|
||||
disabled={disabled}
|
||||
value={formData.lanDiscovery}
|
||||
@@ -66,7 +67,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
||||
</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
|
||||
disabled={disabled}
|
||||
value={formData.registerToLobby}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { EventConfig, Session } from '@/lib/schemas/config';
|
||||
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||
import { TRACKS } from '@/lib/constants/tracks';
|
||||
|
||||
interface EventConfigEditorProps {
|
||||
@@ -63,7 +64,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<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
|
||||
disabled={disabled}
|
||||
value={formData.track}
|
||||
@@ -79,9 +80,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Pre-Race Waiting Time (seconds)
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.preRaceWaitingTimeSeconds}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -93,9 +92,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Session Over Time (seconds)
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.sessionOverTimeSeconds}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -107,9 +104,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Post Qualify Seconds
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.postQualySeconds}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -121,9 +116,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Post Race Seconds
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.postRaceSeconds}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -142,9 +135,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Ambient Temperature (°C)
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.ambientTemp}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -157,9 +148,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Cloud Level (0.0-1.0)
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.cloudLevel}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -173,7 +162,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</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
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -187,9 +176,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Weather Randomness
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.weatherRandomness}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -202,9 +189,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Simracer Weather Conditions
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.simracerWeatherConditions}</label>
|
||||
<select
|
||||
disabled={disabled}
|
||||
value={formData.simracerWeatherConditions}
|
||||
@@ -217,9 +202,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Fixed Condition Qualification
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.isFixedConditionQualification}</label>
|
||||
<select
|
||||
disabled={disabled}
|
||||
value={formData.isFixedConditionQualification}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { EventRules } from '@/lib/schemas/config';
|
||||
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||
|
||||
interface EventRulesEditorProps {
|
||||
formData: EventRules;
|
||||
@@ -8,63 +9,21 @@ interface EventRulesEditorProps {
|
||||
onFormDataChange: (data: EventRules) => void;
|
||||
}
|
||||
|
||||
const numberFields = [
|
||||
{
|
||||
key: 'pitWindowLengthSec' as keyof EventRules,
|
||||
label: 'Pit Window Length (seconds)',
|
||||
min: -1
|
||||
},
|
||||
{
|
||||
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 numberFields: { key: keyof EventRules; min: number; max?: number }[] = [
|
||||
{ key: 'pitWindowLengthSec', min: -1 },
|
||||
{ key: 'driverStintTimeSec', min: -1 },
|
||||
{ key: 'mandatoryPitstopCount', min: 0, max: 5 },
|
||||
{ key: 'maxTotalDrivingTime', min: -1 },
|
||||
{ key: 'maxDriversCount', min: 1, max: 10 },
|
||||
{ key: 'tyreSetCount', min: 0, max: 50 }
|
||||
];
|
||||
|
||||
const booleanFields = [
|
||||
{
|
||||
key: 'isRefuellingAllowedInRace' as keyof EventRules,
|
||||
label: 'Refuelling Allowed in Race'
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
const booleanFields: { key: keyof EventRules }[] = [
|
||||
{ key: 'isRefuellingAllowedInRace' },
|
||||
{ key: 'isRefuellingTimeFixed' },
|
||||
{ key: 'isMandatoryPitstopRefuellingRequired' },
|
||||
{ key: 'isMandatoryPitstopTyreChangeRequired' },
|
||||
{ key: 'isMandatoryPitstopSwapDriverRequired' }
|
||||
];
|
||||
|
||||
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>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<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
|
||||
disabled={disabled}
|
||||
value={formData.qualifyStandingType}
|
||||
@@ -89,9 +48,9 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
|
||||
<option value={2}>Average Lap</option>
|
||||
</select>
|
||||
</div>
|
||||
{numberFields.map(({ key, label, min, max }) => (
|
||||
{numberFields.map(({ key, min, max }) => (
|
||||
<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
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -111,9 +70,9 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
|
||||
Pitstop & Refuelling Rules
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{booleanFields.map(({ key, label }) => (
|
||||
{booleanFields.map(({ 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
|
||||
disabled={disabled}
|
||||
value={formData[key] ? 'true' : 'false'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { ServerSettings } from '@/lib/schemas/config';
|
||||
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||
|
||||
interface ServerSettingsEditorProps {
|
||||
formData: ServerSettings;
|
||||
@@ -8,86 +9,31 @@ interface ServerSettingsEditorProps {
|
||||
onFormDataChange: (data: ServerSettings) => void;
|
||||
}
|
||||
|
||||
const textFields = [
|
||||
{
|
||||
key: 'serverName' as keyof ServerSettings,
|
||||
label: 'Server Name',
|
||||
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 textFields: { key: keyof ServerSettings; type: 'text' | 'password' }[] = [
|
||||
{ key: 'serverName', type: 'text' },
|
||||
{ key: 'adminPassword', type: 'password' },
|
||||
{ key: 'password', type: 'password' },
|
||||
{ key: 'spectatorPassword', type: 'password' },
|
||||
{ key: 'centralEntryListPath', type: 'text' }
|
||||
];
|
||||
|
||||
const carGroups = ['FreeForAll', 'GT3', 'GT4', 'GT2', 'GTC', 'TCX'];
|
||||
|
||||
const numberFields = [
|
||||
{
|
||||
key: 'trackMedalsRequirement' as keyof ServerSettings,
|
||||
label: 'Track Medals Requirement',
|
||||
min: 0,
|
||||
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 numberFields: { key: keyof ServerSettings; min: number; max: number }[] = [
|
||||
{ key: 'trackMedalsRequirement', min: 0, max: 3 },
|
||||
{ key: 'safetyRatingRequirement', min: -1, max: 99 },
|
||||
{ key: 'racecraftRatingRequirement', min: -1, max: 99 },
|
||||
{ key: 'maxCarSlots', min: 1, max: 30 }
|
||||
];
|
||||
|
||||
const selectFields = [
|
||||
{
|
||||
key: 'dumpLeaderboards' as keyof ServerSettings,
|
||||
label: 'Dump Leaderboards'
|
||||
},
|
||||
{ key: 'isRaceLocked' as keyof ServerSettings, label: 'Race Locked' },
|
||||
{
|
||||
key: 'randomizeTrackWhenEmpty' as keyof ServerSettings,
|
||||
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'
|
||||
}
|
||||
const selectFields: { key: keyof ServerSettings }[] = [
|
||||
{ key: 'dumpLeaderboards' },
|
||||
{ key: 'isRaceLocked' },
|
||||
{ key: 'randomizeTrackWhenEmpty' },
|
||||
{ key: 'allowAutoDQ' },
|
||||
{ key: 'shortFormationLap' },
|
||||
{ key: 'dumpEntryList' },
|
||||
{ key: 'ignorePrematureDisconnects' }
|
||||
];
|
||||
|
||||
export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: ServerSettingsEditorProps) {
|
||||
@@ -102,9 +48,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
||||
Basic Settings
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{textFields.map(({ key, label, type }) => (
|
||||
{textFields.map(({ key, type }) => (
|
||||
<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
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
@@ -116,7 +62,7 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
||||
))}
|
||||
|
||||
<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
|
||||
disabled={disabled}
|
||||
value={formData.carGroup}
|
||||
@@ -138,9 +84,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
||||
Requirements & Limits
|
||||
</h3>
|
||||
<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}>
|
||||
<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
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
@@ -160,9 +106,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
||||
Race Options
|
||||
</h3>
|
||||
<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}>
|
||||
<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
|
||||
disabled={disabled}
|
||||
value={formData[key] as number}
|
||||
@@ -176,9 +122,7 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
||||
))}
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
||||
Formation Lap Type
|
||||
</label>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.formationLapType}</label>
|
||||
<select
|
||||
disabled={disabled}
|
||||
value={formData.formationLapType}
|
||||
|
||||
@@ -224,7 +224,13 @@ export function ServerConfigurationTabs({
|
||||
{isConfigTab && (
|
||||
<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-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>}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { loginUser, getOpenToken } from '@/lib/api/server/auth';
|
||||
import { login, logout } from '@/lib/auth/server';
|
||||
import { formatZodIssues } from '@/lib/utils';
|
||||
import { loginSchema, loginResponseSchema } from '../schemas';
|
||||
|
||||
export type LoginResult = {
|
||||
@@ -19,7 +20,7 @@ export async function loginAction(prevState: LoginResult, formData: FormData) {
|
||||
if (!loginData.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: loginData.error.issues[0]?.message ?? 'Validation failed'
|
||||
message: formatZodIssues(loginData.error.issues)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { requireAuth } from '@/lib/auth/server';
|
||||
import { getServerConfigurations, updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration';
|
||||
import { boolToInt, formatZodIssues } from '@/lib/utils';
|
||||
import {
|
||||
assistRulesSchema,
|
||||
ConfigFile,
|
||||
@@ -34,7 +35,6 @@ export async function getConfigurationsAction(
|
||||
};
|
||||
}
|
||||
}
|
||||
import { boolToInt } from '@/lib/utils';
|
||||
|
||||
export async function updateConfigurationAction(serverId: string, formData: FormData) {
|
||||
try {
|
||||
@@ -87,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.issues[0]?.message ?? 'Validation failed' };
|
||||
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||
}
|
||||
|
||||
await updateServerConfiguration(
|
||||
@@ -135,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.issues[0]?.message ?? 'Validation failed' };
|
||||
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||
}
|
||||
|
||||
await updateServerConfiguration(
|
||||
@@ -182,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.issues[0]?.message ?? 'Validation failed' };
|
||||
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||
}
|
||||
|
||||
await updateServerConfiguration(
|
||||
@@ -231,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.issues[0]?.message ?? 'Validation failed' };
|
||||
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||
}
|
||||
|
||||
await updateServerConfiguration(
|
||||
@@ -262,7 +262,7 @@ export async function bulkUpdateConfigurationsAction(
|
||||
|
||||
const validated = configurationsSchema.partial().safeParse(configs);
|
||||
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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { requireAuth } from '@/lib/auth/server';
|
||||
import { fetchServerAPI } from '@/lib/api/server/base';
|
||||
import { formatZodIssues } from '@/lib/utils';
|
||||
import { leaderboardSchema, type Leaderboard } from '@/lib/schemas/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 validated = leaderboardSchema.safeParse(data);
|
||||
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);
|
||||
revalidatePath(`/dashboard/server/${serverId}`);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { requireAuth } from '@/lib/auth/server';
|
||||
import { createUser, deleteUser } from '@/lib/api/server/membership';
|
||||
import { formatZodIssues } from '@/lib/utils';
|
||||
import { userCreateSchema } from '../schemas';
|
||||
|
||||
export async function createUserAction(formData: FormData) {
|
||||
@@ -15,7 +16,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.issues[0]?.message ?? 'Validation failed' };
|
||||
return { success: false, message: formatZodIssues(data.error.issues) };
|
||||
}
|
||||
|
||||
await createUser(session.token!, data.data);
|
||||
|
||||
@@ -5,16 +5,20 @@ export const actionTypeSchema = z.enum([
|
||||
'server_start',
|
||||
'server_stop',
|
||||
'server_restart',
|
||||
'leaderboard_update'
|
||||
'leaderboard_update',
|
||||
'preset_create',
|
||||
'preset_apply'
|
||||
]);
|
||||
|
||||
export type ActionType = z.infer<typeof actionTypeSchema>;
|
||||
|
||||
// Minimal server shape embedded in activity log responses (avoids circular import with server.ts)
|
||||
const embeddedServerSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}).passthrough();
|
||||
const embeddedServerSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export type EmbeddedServer = z.infer<typeof embeddedServerSchema>;
|
||||
|
||||
@@ -58,5 +62,7 @@ export const ACTION_TYPE_LABELS: Record<ActionType, string> = {
|
||||
server_start: 'Server Start',
|
||||
server_stop: 'Server Stop',
|
||||
server_restart: 'Server Restart',
|
||||
leaderboard_update: 'Leaderboard Update'
|
||||
leaderboard_update: 'Leaderboard Update',
|
||||
preset_create: 'Preset Create',
|
||||
preset_apply: 'Preset Apply'
|
||||
};
|
||||
|
||||
@@ -138,3 +138,76 @@ export const configSchemaMap = {
|
||||
};
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { FIELD_LABELS, SECTION_LABELS } from '@/lib/schemas/config';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function boolToInt(val: boolean) {
|
||||
return val ? 1 : 0;
|
||||
return val ? 1 : 0;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user