From 4cc005d309adc0f01119c14a7583a8373d1c9581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sun, 22 Mar 2026 12:34:33 +0100 Subject: [PATCH] feat: api health check and version display --- src/app/dashboard/layout.tsx | 8 ++ src/app/login/page.tsx | 7 +- .../configuration/AssistRulesEditor.tsx | 62 +---------- .../configuration/ConfigurationEditor.tsx | 68 ++---------- .../configuration/EventConfigEditor.tsx | 98 ++++------------- .../configuration/EventRulesEditor.tsx | 66 ++--------- .../configuration/ServerSettingsEditor.tsx | 68 ++---------- src/components/login/LoginForm.tsx | 13 ++- .../server/ServerConfigurationTabs.tsx | 103 ++++++++++++++---- src/lib/actions/configuration.ts | 29 ++++- src/lib/api/server/configuration.ts | 39 ++++++- src/lib/api/server/health.ts | 17 +++ 12 files changed, 236 insertions(+), 342 deletions(-) create mode 100644 src/lib/api/server/health.ts diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index f6f9af2..abc05b1 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -5,9 +5,17 @@ import { ServerCreationPopupContainer } from '@/components/server/ServerCreation import { requireAuth } from '@/lib/auth/server'; import { AppShell } from '@/components/ui/AppShell'; import { hasPermission } from '@/lib/schemas'; +import { checkHealth } from '@/lib/api/server/health'; +import { redirect } from 'next/navigation'; export default async function DashboardLayout({ children }: Readonly<{ children: React.ReactNode }>) { const session = await requireAuth(); + + const health = await checkHealth(); + if (!health.healthy) { + redirect('/login'); + } + const showUsers = hasPermission(session.user!, 'membership.view'); return ( diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 2a1d25a..07a8751 100755 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,15 +1,18 @@ import { Suspense } from 'react'; import LoginForm from '@/components/login/LoginForm'; +import { checkHealth } from '@/lib/api/server/health'; export const dynamic = 'force-dynamic'; -export default function LoginPage({ +export default async function LoginPage({ searchParams }: { searchParams: Promise<{ expired: boolean | undefined }>; }) { + const health = await checkHealth(); + return ( Loading...}> - + ); } diff --git a/src/components/configuration/AssistRulesEditor.tsx b/src/components/configuration/AssistRulesEditor.tsx index a8359a9..325d6fb 100755 --- a/src/components/configuration/AssistRulesEditor.tsx +++ b/src/components/configuration/AssistRulesEditor.tsx @@ -1,15 +1,11 @@ 'use client'; -import { useState } from 'react'; import type { AssistRules } from '@/lib/schemas/config'; -import { updateAssistRulesAction } from '@/lib/actions/configuration'; interface AssistRulesEditorProps { - serverId: string; formData: AssistRules; - restart: boolean; + disabled?: boolean; onFormDataChange: (data: AssistRules) => void; - onRestartChange: (restart: boolean) => void; } const assistFields = [ @@ -60,31 +56,7 @@ const assistFields = [ } ]; -export function AssistRulesEditor({ serverId, formData, restart, onFormDataChange, onRestartChange }: AssistRulesEditorProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - const formDataObj = new FormData(); - Object.entries(formData).forEach(([key, value]) => { - formDataObj.append(key, value.toString()); - }); - if (restart) { - formDataObj.append('restart', 'on'); - } - - try { - const result = await updateAssistRulesAction(serverId, formDataObj); - if (!result.success) { - console.error('Failed to update assist rules:', result.message); - } - } finally { - setIsSubmitting(false); - } - }; - +export function AssistRulesEditor({ formData, disabled, onFormDataChange }: AssistRulesEditorProps) { const handleInputChange = (key: keyof AssistRules, value: string | number) => { onFormDataChange({ ...formData, @@ -93,7 +65,7 @@ export function AssistRulesEditor({ serverId, formData, restart, onFormDataChang }; return ( -
+
{assistFields.map(({ key, label, type }) => (
@@ -101,7 +73,7 @@ export function AssistRulesEditor({ serverId, formData, restart, onFormDataChang {type === 'number' ? ( handleInputChange(key, e.target.value)} className="form-input w-full" @@ -110,7 +82,7 @@ export function AssistRulesEditor({ serverId, formData, restart, onFormDataChang /> ) : ( onRestartChange(e.target.checked)} - className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue" - /> - Restart server after saving - -
- -
- -
- +
); } diff --git a/src/components/configuration/ConfigurationEditor.tsx b/src/components/configuration/ConfigurationEditor.tsx index 7138184..27b7cf5 100755 --- a/src/components/configuration/ConfigurationEditor.tsx +++ b/src/components/configuration/ConfigurationEditor.tsx @@ -1,42 +1,14 @@ 'use client'; -import { useState } from 'react'; import type { Configuration } from '@/lib/schemas/config'; -import { updateConfigurationAction } from '@/lib/actions/configuration'; interface ConfigurationEditorProps { - serverId: string; formData: Configuration; - restart: boolean; + disabled?: boolean; onFormDataChange: (data: Configuration) => void; - onRestartChange: (restart: boolean) => void; } -export function ConfigurationEditor({ serverId, formData, restart, onFormDataChange, onRestartChange }: ConfigurationEditorProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - const formDataObj = new FormData(); - Object.entries(formData).forEach(([key, value]) => { - formDataObj.append(key, value.toString()); - }); - if (restart) { - formDataObj.append('restart', 'on'); - } - - try { - const result = await updateConfigurationAction(serverId, formDataObj); - if (!result.success) { - console.error('Failed to update configuration:', result.message); - } - } finally { - setIsSubmitting(false); - } - }; - +export function ConfigurationEditor({ formData, disabled, onFormDataChange }: ConfigurationEditorProps) { const handleInputChange = (key: keyof Configuration, value: string | number) => { onFormDataChange({ ...formData, @@ -45,13 +17,13 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha }; return ( -
+
handleInputChange('udpPort', e.target.value)} className="form-input w-full" @@ -62,7 +34,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha handleInputChange('tcpPort', e.target.value)} className="form-input w-full" @@ -73,7 +45,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha handleInputChange('maxConnections', e.target.value)} className="form-input w-full" @@ -83,7 +55,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
handleInputChange('registerToLobby', e.target.value)} className="form-select w-full" @@ -106,28 +78,6 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
- -
- -
- -
- -
- +
); } diff --git a/src/components/configuration/EventConfigEditor.tsx b/src/components/configuration/EventConfigEditor.tsx index 6c02ae0..5d9d28c 100755 --- a/src/components/configuration/EventConfigEditor.tsx +++ b/src/components/configuration/EventConfigEditor.tsx @@ -1,15 +1,11 @@ 'use client'; -import { useState } from 'react'; import type { EventConfig, Session } from '@/lib/schemas/config'; -import { updateEventConfigAction } from '@/lib/actions/configuration'; interface EventConfigEditorProps { - serverId: string; formData: EventConfig; - restart: boolean; + disabled?: boolean; onFormDataChange: (data: EventConfig) => void; - onRestartChange: (restart: boolean) => void; } const sessionTypes = [ @@ -18,35 +14,7 @@ const sessionTypes = [ { value: 'R', label: 'Race' } ]; -export function EventConfigEditor({ serverId, formData, restart, onFormDataChange, onRestartChange }: EventConfigEditorProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - const formDataObj = new FormData(); - Object.entries(formData).forEach(([key, value]) => { - if (key === 'sessions') { - formDataObj.append(key, JSON.stringify(value)); - } else { - formDataObj.append(key, value.toString()); - } - }); - if (restart) { - formDataObj.append('restart', 'on'); - } - - try { - const result = await updateEventConfigAction(serverId, formDataObj); - if (!result.success) { - console.error('Failed to update event config:', result.message); - } - } finally { - setIsSubmitting(false); - } - }; - +export function EventConfigEditor({ formData, disabled, onFormDataChange }: EventConfigEditorProps) { const handleInputChange = (key: keyof EventConfig, value: string | number) => { if (key === 'sessions') return; @@ -87,7 +55,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang }; return ( -
+

Basic Event Settings @@ -96,7 +64,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
handleInputChange('preRaceWaitingTimeSeconds', e.target.value)} className="form-input w-full" @@ -149,7 +117,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('sessionOverTimeSeconds', e.target.value)} className="form-input w-full" @@ -163,7 +131,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('postQualySeconds', e.target.value)} className="form-input w-full" @@ -177,7 +145,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('postRaceSeconds', e.target.value)} className="form-input w-full" @@ -198,7 +166,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('ambientTemp', e.target.value)} className="form-input w-full" @@ -213,7 +181,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('cloudLevel', e.target.value)} className="form-input w-full" @@ -227,7 +195,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('rain', e.target.value)} className="form-input w-full" @@ -243,7 +211,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang handleInputChange('weatherRandomness', e.target.value)} className="form-input w-full" @@ -257,7 +225,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang Simracer Weather Conditions handleInputChange('isFixedConditionQualification', e.target.value)} className="form-select w-full" @@ -290,7 +258,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang

- -
- -
- -
- -
- +
); } diff --git a/src/components/configuration/EventRulesEditor.tsx b/src/components/configuration/EventRulesEditor.tsx index 4ead6f9..c00f41b 100755 --- a/src/components/configuration/EventRulesEditor.tsx +++ b/src/components/configuration/EventRulesEditor.tsx @@ -1,15 +1,11 @@ 'use client'; -import { useState } from 'react'; import type { EventRules } from '@/lib/schemas/config'; -import { updateEventRulesAction } from '@/lib/actions/configuration'; interface EventRulesEditorProps { - serverId: string; formData: EventRules; - restart: boolean; + disabled?: boolean; onFormDataChange: (data: EventRules) => void; - onRestartChange: (restart: boolean) => void; } const numberFields = [ @@ -71,37 +67,13 @@ const booleanFields = [ } ]; -export function EventRulesEditor({ serverId, formData, restart, onFormDataChange, onRestartChange }: EventRulesEditorProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - const formDataObj = new FormData(); - Object.entries(formData).forEach(([key, value]) => { - formDataObj.append(key, value.toString()); - }); - if (restart) { - formDataObj.append('restart', 'on'); - } - - try { - const result = await updateEventRulesAction(serverId, formDataObj); - if (!result.success) { - console.error('Failed to update event rules:', result.message); - } - } finally { - setIsSubmitting(false); - } - }; - - const handleInputChange = (key: keyof EventRules, value: string | number | boolean) => { +export function EventRulesEditor({ formData, disabled, onFormDataChange }: EventRulesEditorProps) { + const handleInputChange = (key: keyof EventRules, value: number) => { onFormDataChange({ ...formData, [key]: value }); }; return ( -
+

Race Rules

@@ -110,7 +82,7 @@ export function EventRulesEditor({ serverId, formData, restart, onFormDataChange handleInputChange(key, parseInt(e.target.value) || 0)} className="form-input w-full" @@ -131,9 +103,9 @@ export function EventRulesEditor({ serverId, formData, restart, onFormDataChange
onRestartChange(e.target.checked)} - className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue" - /> - Restart server after saving - -
- -
- -
- +
); } diff --git a/src/components/configuration/ServerSettingsEditor.tsx b/src/components/configuration/ServerSettingsEditor.tsx index df2607c..e2a74d2 100755 --- a/src/components/configuration/ServerSettingsEditor.tsx +++ b/src/components/configuration/ServerSettingsEditor.tsx @@ -1,15 +1,11 @@ 'use client'; -import { useState } from 'react'; import type { ServerSettings } from '@/lib/schemas/config'; -import { updateServerSettingsAction } from '@/lib/actions/configuration'; interface ServerSettingsEditorProps { - serverId: string; formData: ServerSettings; - restart: boolean; + disabled?: boolean; onFormDataChange: (data: ServerSettings) => void; - onRestartChange: (restart: boolean) => void; } const textFields = [ @@ -90,37 +86,13 @@ const selectFields = [ } ]; -export function ServerSettingsEditor({ serverId, formData, restart, onFormDataChange, onRestartChange }: ServerSettingsEditorProps) { - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSubmitting(true); - - const formDataObj = new FormData(); - Object.entries(formData).forEach(([key, value]) => { - formDataObj.append(key, value?.toString() ?? ''); - }); - if (restart) { - formDataObj.append('restart', 'on'); - } - - try { - const result = await updateServerSettingsAction(serverId, formDataObj); - if (!result.success) { - console.error('Failed to update server settings:', result.message); - } - } finally { - setIsSubmitting(false); - } - }; - +export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: ServerSettingsEditorProps) { const handleInputChange = (key: keyof ServerSettings, value: string | number) => { onFormDataChange({ ...formData, [key]: value }); }; return ( -
+

Basic Settings @@ -131,7 +103,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh handleInputChange(key, e.target.value)} className="form-input w-full" @@ -142,7 +114,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
handleInputChange(key, parseInt(e.target.value) || 0)} className="form-input w-full" @@ -188,7 +160,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
handleInputChange('formationLapType', parseInt(e.target.value))} className="form-select w-full" @@ -218,28 +190,6 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh

- -
- -
- -
- -
- +
); } diff --git a/src/components/login/LoginForm.tsx b/src/components/login/LoginForm.tsx index b8434e5..0742835 100755 --- a/src/components/login/LoginForm.tsx +++ b/src/components/login/LoginForm.tsx @@ -2,6 +2,8 @@ import { clearExpiredSessionAction, loginAction, LoginResult } from '@/lib/actions/auth'; import { use, useActionState, useEffect } from 'react'; +import type { HealthStatus } from '@/lib/api/server/health'; +import { Badge } from '@/components/ui/Badge'; const initialState: LoginResult = { message: '', @@ -9,9 +11,11 @@ const initialState: LoginResult = { }; export default function LoginForm({ - searchParams + searchParams, + health }: { searchParams: Promise<{ expired: boolean | undefined }>; + health: HealthStatus; }) { const params = use(searchParams); const expired = params.expired; @@ -28,6 +32,13 @@ export default function LoginForm({

ACC Server Manager

Sign in to manage your servers

+
+ {health.healthy ? ( + API v{health.version} + ) : ( + API unreachable + )} +
{expired && (
diff --git a/src/components/server/ServerConfigurationTabs.tsx b/src/components/server/ServerConfigurationTabs.tsx index ffb0367..338a8dc 100755 --- a/src/components/server/ServerConfigurationTabs.tsx +++ b/src/components/server/ServerConfigurationTabs.tsx @@ -11,6 +11,7 @@ import { LeaderboardManager } from '@/components/leaderboard/LeaderboardManager' import { useState } from 'react'; import { StateHistoryStats } from '@/lib/schemas'; import { Leaderboard } from '@/lib/schemas/leaderboard'; +import { bulkUpdateConfigurationsAction } from '@/lib/actions/configuration'; interface ServerConfigurationTabsProps { serverId: string; @@ -28,6 +29,14 @@ const tabs = [ { id: ServerTab.leaderboard, name: 'Leaderboard', icon: '🏆' } ]; +const configTabs = new Set([ + ServerTab.configuration, + ServerTab.assistRules, + ServerTab.event, + ServerTab.eventRules, + ServerTab.settings +]); + export function ServerConfigurationTabs({ serverId, configurations, @@ -37,19 +46,44 @@ export function ServerConfigurationTabs({ const [currentTab, setCurrentTab] = useState(ServerTab.statistics); const [configData, setConfigData] = useState(configurations.configuration); - const [configRestart, setConfigRestart] = useState(true); - const [assistData, setAssistData] = useState(configurations.assistRules); - const [assistRestart, setAssistRestart] = useState(true); - const [eventData, setEventData] = useState(configurations.event); - const [eventRestart, setEventRestart] = useState(true); - const [eventRulesData, setEventRulesData] = useState(configurations.eventRules); - const [eventRulesRestart, setEventRulesRestart] = useState(true); - const [settingsData, setSettingsData] = useState(configurations.settings); - const [settingsRestart, setSettingsRestart] = useState(true); + + const [restart, setRestart] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [saveError, setSaveError] = useState(null); + const [saveSuccess, setSaveSuccess] = useState(false); + + const handleSaveAll = async () => { + setIsSubmitting(true); + setSaveError(null); + setSaveSuccess(false); + + const result = await bulkUpdateConfigurationsAction( + serverId, + { + configuration: configData, + assistRules: assistData, + event: eventData, + eventRules: eventRulesData, + settings: settingsData + }, + restart + ); + + if (result.success) { + setSaveSuccess(true); + setTimeout(() => setSaveSuccess(false), 3000); + } else { + setSaveError(result.message); + } + + setIsSubmitting(false); + }; + + const isConfigTab = configTabs.has(currentTab); const renderTabContent = () => { switch (currentTab) { @@ -59,55 +93,45 @@ export function ServerConfigurationTabs({ case ServerTab.configuration: return ( ); case ServerTab.assistRules: return ( ); case ServerTab.event: return ( ); case ServerTab.eventRules: return ( ); case ServerTab.settings: return ( ); @@ -147,6 +171,37 @@ export function ServerConfigurationTabs({ })} + {isConfigTab && ( +
+
+ {saveError && ( +

{saveError}

+ )} + {saveSuccess && ( +

All configurations saved.

+ )} +
+
+ + +
+
+ )} +
{renderTabContent()}
); diff --git a/src/lib/actions/configuration.ts b/src/lib/actions/configuration.ts index 3fd55a7..2ce6929 100755 --- a/src/lib/actions/configuration.ts +++ b/src/lib/actions/configuration.ts @@ -2,10 +2,11 @@ import { revalidatePath } from 'next/cache'; import { requireAuth } from '@/lib/auth/server'; -import { updateServerConfiguration } from '@/lib/api/server/configuration'; +import { updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration'; import { assistRulesSchema, ConfigFile, + configurationsSchema, eventConfigSchema, eventRulesSchema, serverSettingsSchema @@ -13,6 +14,7 @@ import { import type { Configuration, AssistRules, + Configurations, EventConfig, EventRules, ServerSettings @@ -232,3 +234,28 @@ export async function updateEventRulesAction(serverId: string, formData: FormDat }; } } + +export async function bulkUpdateConfigurationsAction( + serverId: string, + configs: Configurations, + restart: boolean +) { + try { + const session = await requireAuth(); + + const validated = configurationsSchema.safeParse(configs); + if (!validated.success) { + return { success: false, message: validated.error.message }; + } + + await bulkUpdateServerConfigurations(session.token!, serverId, validated.data, restart); + revalidatePath(`/dashboard/server/${serverId}`); + + return { success: true, message: 'All configurations updated successfully' }; + } catch (error) { + return { + success: false, + message: error instanceof Error ? error.message : 'Failed to update configurations' + }; + } +} diff --git a/src/lib/api/server/configuration.ts b/src/lib/api/server/configuration.ts index c21b396..d9a763e 100755 --- a/src/lib/api/server/configuration.ts +++ b/src/lib/api/server/configuration.ts @@ -8,14 +8,23 @@ import { } from '@/lib/schemas/config'; import * as z from 'zod'; +export type ConfigAuditRecord = { + id: string; + serverId: string; + configFile: string; + oldConfig: string; + newConfig: string; + changedAt: string; +}; + const serverRoute = '/server'; export async function getServerConfigurations( token: string, serverId: string ): Promise { - const response = await fetchServerAPI(`${serverRoute}/${serverId}/config`, token); - return configurationsSchema.parse(response.data); + const response = await fetchServerAPI(`${serverRoute}/${serverId}/config`, token); + return configurationsSchema.parse(response.data); } export function validateConfig( @@ -45,8 +54,26 @@ export async function updateServerConfiguration( config: Config, restart = false ): Promise { - await fetchServerAPI(`${serverRoute}/${serverId}/config/${configType}?override=true`, token, 'PUT', { - ...validateConfig(configType, config), - restart - }); + await fetchServerAPI( + `${serverRoute}/${serverId}/config/${configType}?override=true&restart=${restart}`, + token, + 'PUT', + { + ...validateConfig(configType, config) + } + ); +} + +export async function bulkUpdateServerConfigurations( + token: string, + serverId: string, + configs: Configurations, + restart = false +): Promise { + await fetchServerAPI( + `${serverRoute}/${serverId}/config?override=true&restart=${restart}`, + token, + 'PATCH', + configs + ); } diff --git a/src/lib/api/server/health.ts b/src/lib/api/server/health.ts new file mode 100644 index 0000000..7210b19 --- /dev/null +++ b/src/lib/api/server/health.ts @@ -0,0 +1,17 @@ +const BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080'; + +export type HealthStatus = { healthy: true; version: string } | { healthy: false; version: null }; + +export async function checkHealth(): Promise { + try { + const response = await fetch(`${BASE_URL}/system/health`, { + method: 'GET', + next: { revalidate: 0 } + }); + if (!response.ok) return { healthy: false, version: null }; + const version = await response.text(); + return { healthy: true, version: version.trim() || 'unknown' }; + } catch { + return { healthy: false, version: null }; + } +}