feat: api health check and version display
This commit is contained in:
@@ -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 (
|
||||
<WebSocketProvider openToken={session.openToken!}>
|
||||
|
||||
@@ -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 (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<LoginForm searchParams={searchParams} />
|
||||
<LoginForm searchParams={searchParams} health={health} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} 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">
|
||||
{assistFields.map(({ key, label, type }) => (
|
||||
<div key={key}>
|
||||
@@ -101,7 +73,7 @@ export function AssistRulesEditor({ serverId, formData, restart, onFormDataChang
|
||||
{type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key]}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -110,7 +82,7 @@ export function AssistRulesEditor({ serverId, formData, restart, onFormDataChang
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key]}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -122,28 +94,6 @@ export function AssistRulesEditor({ serverId, formData, restart, onFormDataChang
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-6">
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={restart}
|
||||
onChange={(e) => 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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} 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>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">UDP Port</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.udpPort}
|
||||
onChange={(e) => handleInputChange('udpPort', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -62,7 +34,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">TCP Port</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.tcpPort}
|
||||
onChange={(e) => handleInputChange('tcpPort', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -73,7 +45,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Max Connections</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.maxConnections}
|
||||
onChange={(e) => handleInputChange('maxConnections', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -83,7 +55,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">LAN Discovery</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.lanDiscovery}
|
||||
onChange={(e) => handleInputChange('lanDiscovery', e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -96,7 +68,7 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Register To Lobby</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.registerToLobby}
|
||||
onChange={(e) => handleInputChange('registerToLobby', e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -106,28 +78,6 @@ export function ConfigurationEditor({ serverId, formData, restart, onFormDataCha
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-6">
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={restart}
|
||||
onChange={(e) => 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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
|
||||
<div className="max-w-4xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
|
||||
Basic Event Settings
|
||||
@@ -96,7 +64,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Track</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.track}
|
||||
onChange={(e) => handleInputChange('track', e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -135,7 +103,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.preRaceWaitingTimeSeconds}
|
||||
onChange={(e) => handleInputChange('preRaceWaitingTimeSeconds', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -149,7 +117,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.sessionOverTimeSeconds}
|
||||
onChange={(e) => handleInputChange('sessionOverTimeSeconds', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -163,7 +131,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.postQualySeconds}
|
||||
onChange={(e) => handleInputChange('postQualySeconds', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -177,7 +145,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.postRaceSeconds}
|
||||
onChange={(e) => handleInputChange('postRaceSeconds', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -198,7 +166,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.ambientTemp}
|
||||
onChange={(e) => handleInputChange('ambientTemp', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -213,7 +181,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.cloudLevel}
|
||||
onChange={(e) => handleInputChange('cloudLevel', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -227,7 +195,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Rain (0.0-1.0)</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.rain}
|
||||
onChange={(e) => handleInputChange('rain', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -243,7 +211,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.weatherRandomness}
|
||||
onChange={(e) => handleInputChange('weatherRandomness', e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -257,7 +225,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
Simracer Weather Conditions
|
||||
</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.simracerWeatherConditions}
|
||||
onChange={(e) => handleInputChange('simracerWeatherConditions', e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -272,7 +240,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
Fixed Condition Qualification
|
||||
</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.isFixedConditionQualification}
|
||||
onChange={(e) => handleInputChange('isFixedConditionQualification', e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -290,7 +258,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
<button
|
||||
type="button"
|
||||
onClick={addSession}
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
className="rounded-md border border-border bg-overlay px-3 py-1.5 text-xs font-medium text-secondary hover:bg-border disabled:opacity-40"
|
||||
>
|
||||
+ Add Session
|
||||
@@ -305,7 +273,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSession(index)}
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
className="rounded px-2 py-1 text-xs text-red hover:bg-red-bg disabled:opacity-40"
|
||||
>
|
||||
Remove
|
||||
@@ -318,7 +286,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
Session Type
|
||||
</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={session.sessionType}
|
||||
onChange={(e) => handleSessionChange(index, 'sessionType', e.target.value)}
|
||||
className="form-select w-full text-sm"
|
||||
@@ -337,7 +305,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={session.hourOfDay}
|
||||
onChange={(e) => handleSessionChange(index, 'hourOfDay', e.target.value)}
|
||||
className="form-input w-full text-sm"
|
||||
@@ -352,7 +320,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={session.dayOfWeekend}
|
||||
onChange={(e) => handleSessionChange(index, 'dayOfWeekend', e.target.value)}
|
||||
className="form-input w-full text-sm"
|
||||
@@ -367,7 +335,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={session.timeMultiplier}
|
||||
onChange={(e) => handleSessionChange(index, 'timeMultiplier', e.target.value)}
|
||||
className="form-input w-full text-sm"
|
||||
@@ -382,7 +350,7 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={session.sessionDurationMinutes}
|
||||
onChange={(e) =>
|
||||
handleSessionChange(index, 'sessionDurationMinutes', e.target.value)
|
||||
@@ -396,28 +364,6 @@ export function EventConfigEditor({ serverId, formData, restart, onFormDataChang
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-6">
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={restart}
|
||||
onChange={(e) => 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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
|
||||
<div className="max-w-4xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
<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">
|
||||
@@ -110,7 +82,7 @@ export function EventRulesEditor({ serverId, formData, restart, onFormDataChange
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key] as number}
|
||||
onChange={(e) => handleInputChange(key, parseInt(e.target.value) || 0)}
|
||||
className="form-input w-full"
|
||||
@@ -131,9 +103,9 @@ export function EventRulesEditor({ serverId, formData, restart, onFormDataChange
|
||||
<div key={key}>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key] ? 'true' : 'false'}
|
||||
onChange={(e) => handleInputChange(key, e.target.value === 'true')}
|
||||
onChange={(e) => handleInputChange(key, e.target.value === 'true' ? 1 : 0)}
|
||||
className="form-select w-full"
|
||||
>
|
||||
<option value="false">No</option>
|
||||
@@ -143,28 +115,6 @@ export function EventRulesEditor({ serverId, formData, restart, onFormDataChange
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-6">
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={restart}
|
||||
onChange={(e) => 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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
|
||||
<div className="max-w-4xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
|
||||
Basic Settings
|
||||
@@ -131,7 +103,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
||||
<input
|
||||
type={type}
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key] as string}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="form-input w-full"
|
||||
@@ -142,7 +114,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Car Group</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.carGroup}
|
||||
onChange={(e) => handleInputChange('carGroup', e.target.value)}
|
||||
className="form-select w-full"
|
||||
@@ -167,7 +139,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key] as number}
|
||||
onChange={(e) => handleInputChange(key, parseInt(e.target.value) || 0)}
|
||||
className="form-input w-full"
|
||||
@@ -188,7 +160,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
|
||||
<div key={key}>
|
||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData[key] as number}
|
||||
onChange={(e) => handleInputChange(key, parseInt(e.target.value))}
|
||||
className="form-select w-full"
|
||||
@@ -204,7 +176,7 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
|
||||
Formation Lap Type
|
||||
</label>
|
||||
<select
|
||||
disabled={isSubmitting}
|
||||
disabled={disabled}
|
||||
value={formData.formationLapType}
|
||||
onChange={(e) => handleInputChange('formationLapType', parseInt(e.target.value))}
|
||||
className="form-select w-full"
|
||||
@@ -218,28 +190,6 @@ export function ServerSettingsEditor({ serverId, formData, restart, onFormDataCh
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-6">
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={restart}
|
||||
onChange={(e) => 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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<div className="text-center">
|
||||
<h1 className="text-xl font-bold text-primary">ACC Server Manager</h1>
|
||||
<p className="mt-1 text-sm text-muted">Sign in to manage your servers</p>
|
||||
<div className="mt-2 flex justify-center">
|
||||
{health.healthy ? (
|
||||
<Badge variant="green">API v{health.version}</Badge>
|
||||
) : (
|
||||
<Badge variant="red">API unreachable</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{expired && (
|
||||
<div className="rounded-md border border-yellow/30 bg-yellow-bg px-3 py-2 text-sm text-yellow">
|
||||
|
||||
@@ -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<string | null>(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 (
|
||||
<ConfigurationEditor
|
||||
serverId={serverId}
|
||||
formData={configData}
|
||||
restart={configRestart}
|
||||
disabled={isSubmitting}
|
||||
onFormDataChange={setConfigData}
|
||||
onRestartChange={setConfigRestart}
|
||||
/>
|
||||
);
|
||||
|
||||
case ServerTab.assistRules:
|
||||
return (
|
||||
<AssistRulesEditor
|
||||
serverId={serverId}
|
||||
formData={assistData}
|
||||
restart={assistRestart}
|
||||
disabled={isSubmitting}
|
||||
onFormDataChange={setAssistData}
|
||||
onRestartChange={setAssistRestart}
|
||||
/>
|
||||
);
|
||||
|
||||
case ServerTab.event:
|
||||
return (
|
||||
<EventConfigEditor
|
||||
serverId={serverId}
|
||||
formData={eventData}
|
||||
restart={eventRestart}
|
||||
disabled={isSubmitting}
|
||||
onFormDataChange={setEventData}
|
||||
onRestartChange={setEventRestart}
|
||||
/>
|
||||
);
|
||||
|
||||
case ServerTab.eventRules:
|
||||
return (
|
||||
<EventRulesEditor
|
||||
serverId={serverId}
|
||||
formData={eventRulesData}
|
||||
restart={eventRulesRestart}
|
||||
disabled={isSubmitting}
|
||||
onFormDataChange={setEventRulesData}
|
||||
onRestartChange={setEventRulesRestart}
|
||||
/>
|
||||
);
|
||||
|
||||
case ServerTab.settings:
|
||||
return (
|
||||
<ServerSettingsEditor
|
||||
serverId={serverId}
|
||||
formData={settingsData}
|
||||
restart={settingsRestart}
|
||||
disabled={isSubmitting}
|
||||
onFormDataChange={setSettingsData}
|
||||
onRestartChange={setSettingsRestart}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -147,6 +171,37 @@ export function ServerConfigurationTabs({
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{isConfigTab && (
|
||||
<div className="flex items-center justify-between border-b border-border-muted bg-overlay px-5 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{saveError && (
|
||||
<p className="text-sm text-red">{saveError}</p>
|
||||
)}
|
||||
{saveSuccess && (
|
||||
<p className="text-sm text-green">All configurations saved.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={restart}
|
||||
onChange={(e) => setRestart(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
|
||||
/>
|
||||
Restart after saving
|
||||
</label>
|
||||
<button
|
||||
onClick={handleSaveAll}
|
||||
disabled={isSubmitting}
|
||||
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Save All'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-5">{renderTabContent()}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Configurations> {
|
||||
const response = await fetchServerAPI<Configurations>(`${serverRoute}/${serverId}/config`, token);
|
||||
return configurationsSchema.parse(response.data);
|
||||
const response = await fetchServerAPI<Configurations>(`${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<void> {
|
||||
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<void> {
|
||||
await fetchServerAPI<ConfigAuditRecord[]>(
|
||||
`${serverRoute}/${serverId}/config?override=true&restart=${restart}`,
|
||||
token,
|
||||
'PATCH',
|
||||
configs
|
||||
);
|
||||
}
|
||||
|
||||
17
src/lib/api/server/health.ts
Normal file
17
src/lib/api/server/health.ts
Normal file
@@ -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<HealthStatus> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user