From 64acf24f27e147f6f3ccaeb9c7b451ce3b2b5b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Thu, 26 Mar 2026 22:32:45 +0100 Subject: [PATCH] fix: error handling --- .../activityLog/ActivityLogTable.tsx | 7 +- .../configuration/AssistRulesEditor.tsx | 61 ++-------- .../configuration/ConfigurationEditor.tsx | 11 +- .../configuration/EventConfigEditor.tsx | 41 ++----- .../configuration/EventRulesEditor.tsx | 79 +++--------- .../configuration/ServerSettingsEditor.tsx | 112 +++++------------- .../server/ServerConfigurationTabs.tsx | 8 +- src/lib/actions/auth.ts | 3 +- src/lib/actions/configuration.ts | 12 +- src/lib/actions/leaderboard.ts | 3 +- src/lib/actions/membership.ts | 3 +- src/lib/schemas/activityLog.ts | 18 ++- src/lib/schemas/config.ts | 73 ++++++++++++ src/lib/utils.ts | 18 ++- 14 files changed, 204 insertions(+), 245 deletions(-) diff --git a/src/components/activityLog/ActivityLogTable.tsx b/src/components/activityLog/ActivityLogTable.tsx index 2a8eb7b..a896d17 100644 --- a/src/components/activityLog/ActivityLogTable.tsx +++ b/src/components/activityLog/ActivityLogTable.tsx @@ -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 = { 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) { diff --git a/src/components/configuration/AssistRulesEditor.tsx b/src/components/configuration/AssistRulesEditor.tsx index 325d6fb..2784ca6 100755 --- a/src/components/configuration/AssistRulesEditor.tsx +++ b/src/components/configuration/AssistRulesEditor.tsx @@ -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 (
- {assistFields.map(({ key, label, type }) => ( + {assistFields.map(({ key, type }) => (
- + {type === 'number' ? (
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- + 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

Race Rules

- +
- {numberFields.map(({ key, label, min, max }) => ( + {numberFields.map(({ key, min, max }) => (
- +
- {booleanFields.map(({ key, label }) => ( + {booleanFields.map(({ key }) => (
- + - +
- {selectFields.map(({ key, label }) => ( + {selectFields.map(({ key }) => (
- +
- {saveError &&

{saveError}

} + {saveError && ( +
    + {saveError.split('\n').map((line, i) => ( +
  • • {line}
  • + ))} +
+ )} {saveSuccess &&

All configurations saved.

}
diff --git a/src/lib/actions/auth.ts b/src/lib/actions/auth.ts index 09fed28..beb3534 100755 --- a/src/lib/actions/auth.ts +++ b/src/lib/actions/auth.ts @@ -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) }; } diff --git a/src/lib/actions/configuration.ts b/src/lib/actions/configuration.ts index 0237cd8..038c3e8 100755 --- a/src/lib/actions/configuration.ts +++ b/src/lib/actions/configuration.ts @@ -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); diff --git a/src/lib/actions/leaderboard.ts b/src/lib/actions/leaderboard.ts index 1653937..088193c 100755 --- a/src/lib/actions/leaderboard.ts +++ b/src/lib/actions/leaderboard.ts @@ -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}`); diff --git a/src/lib/actions/membership.ts b/src/lib/actions/membership.ts index 8f47d06..f5cf1e2 100755 --- a/src/lib/actions/membership.ts +++ b/src/lib/actions/membership.ts @@ -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); diff --git a/src/lib/schemas/activityLog.ts b/src/lib/schemas/activityLog.ts index 5a79371..a66ad62 100644 --- a/src/lib/schemas/activityLog.ts +++ b/src/lib/schemas/activityLog.ts @@ -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; // 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; @@ -58,5 +62,7 @@ export const ACTION_TYPE_LABELS: Record = { 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' }; diff --git a/src/lib/schemas/config.ts b/src/lib/schemas/config.ts index a3ad30e..f66b3d5 100755 --- a/src/lib/schemas/config.ts +++ b/src/lib/schemas/config.ts @@ -138,3 +138,76 @@ export const configSchemaMap = { }; export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings; + +export const SECTION_LABELS: Record = { + configuration: 'Configuration', + assistRules: 'Assist Rules', + event: 'Event Config', + eventRules: 'Event Rules', + settings: 'Server Settings' +}; + +export const FIELD_LABELS: Record = { + // 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' +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e688a1f..1df7ea2 100755 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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'); }