feat: Complete Issue #11 - Fix backup UI bugs and implement System Settings page
This commit is contained in:
227
frontend/src/pages/SystemSettings.tsx
Normal file
227
frontend/src/pages/SystemSettings.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Card } from '../components/ui/Card'
|
||||
import { Button } from '../components/ui/Button'
|
||||
import { Input } from '../components/ui/Input'
|
||||
import { toast } from '../components/Toast'
|
||||
import { getSettings, updateSetting } from '../api/settings'
|
||||
import client from '../api/client'
|
||||
import { Loader2, Server, RefreshCw, Save, Activity } from 'lucide-react'
|
||||
|
||||
interface HealthResponse {
|
||||
status: string
|
||||
service: string
|
||||
version: string
|
||||
git_commit: string
|
||||
build_time: string
|
||||
}
|
||||
|
||||
interface UpdateInfo {
|
||||
current_version: string
|
||||
latest_version: string
|
||||
update_available: boolean
|
||||
release_url?: string
|
||||
}
|
||||
|
||||
export default function SystemSettings() {
|
||||
const queryClient = useQueryClient()
|
||||
const [caddyEmail, setCaddyEmail] = useState('')
|
||||
const [caddyAdminAPI, setCaddyAdminAPI] = useState('http://localhost:2019')
|
||||
|
||||
// Fetch Settings
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: getSettings,
|
||||
})
|
||||
|
||||
// Update local state when settings load
|
||||
useState(() => {
|
||||
if (settings) {
|
||||
if (settings['caddy.email']) setCaddyEmail(settings['caddy.email'])
|
||||
if (settings['caddy.admin_api']) setCaddyAdminAPI(settings['caddy.admin_api'])
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch Health/System Status
|
||||
const { data: health, isLoading: isLoadingHealth } = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: async (): Promise<HealthResponse> => {
|
||||
const response = await client.get<HealthResponse>('/health')
|
||||
return response.data
|
||||
},
|
||||
})
|
||||
|
||||
// Check for Updates
|
||||
const {
|
||||
data: updateInfo,
|
||||
refetch: checkUpdates,
|
||||
isFetching: isCheckingUpdates,
|
||||
} = useQuery({
|
||||
queryKey: ['updates'],
|
||||
queryFn: async (): Promise<UpdateInfo> => {
|
||||
const response = await client.get<UpdateInfo>('/system/updates')
|
||||
return response.data
|
||||
},
|
||||
enabled: false, // Manual trigger
|
||||
})
|
||||
|
||||
const saveSettingsMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await updateSetting('caddy.email', caddyEmail, 'caddy', 'string')
|
||||
await updateSetting('caddy.admin_api', caddyAdminAPI, 'caddy', 'string')
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['settings'] })
|
||||
toast.success('System settings saved')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(`Failed to save settings: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Server className="w-8 h-8" />
|
||||
System Settings
|
||||
</h1>
|
||||
|
||||
{/* General Configuration */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">General Configuration</h2>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Default Certificate Email"
|
||||
type="email"
|
||||
value={caddyEmail}
|
||||
onChange={(e) => setCaddyEmail(e.target.value)}
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 -mt-2">
|
||||
Email address for Let's Encrypt certificate notifications
|
||||
</p>
|
||||
<Input
|
||||
label="Caddy Admin API Endpoint"
|
||||
type="text"
|
||||
value={caddyAdminAPI}
|
||||
onChange={(e) => setCaddyAdminAPI(e.target.value)}
|
||||
placeholder="http://localhost:2019"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 -mt-2">
|
||||
URL to the Caddy admin API (usually on port 2019)
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => saveSettingsMutation.mutate()}
|
||||
isLoading={saveSettingsMutation.isPending}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* System Status */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Activity className="w-5 h-5" />
|
||||
System Status
|
||||
</h2>
|
||||
{isLoadingHealth ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : health ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Service</p>
|
||||
<p className="text-lg font-medium text-gray-900 dark:text-white">{health.service}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Status</p>
|
||||
<p className="text-lg font-medium text-green-600 dark:text-green-400 capitalize">
|
||||
{health.status}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Version</p>
|
||||
<p className="text-lg font-medium text-gray-900 dark:text-white">{health.version}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Build Time</p>
|
||||
<p className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{health.build_time || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Git Commit</p>
|
||||
<p className="text-sm font-mono text-gray-900 dark:text-white">
|
||||
{health.git_commit || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-red-500">Unable to fetch system status</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Update Check */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Software Updates</h2>
|
||||
<div className="space-y-4">
|
||||
{updateInfo && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Current Version</p>
|
||||
<p className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{updateInfo.current_version}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Latest Version</p>
|
||||
<p className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{updateInfo.latest_version}
|
||||
</p>
|
||||
</div>
|
||||
{updateInfo.update_available && (
|
||||
<div className="md:col-span-2">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p className="text-blue-800 dark:text-blue-300 font-medium">
|
||||
A new version is available!
|
||||
</p>
|
||||
{updateInfo.release_url && (
|
||||
<a
|
||||
href={updateInfo.release_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline text-sm"
|
||||
>
|
||||
View Release Notes
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!updateInfo.update_available && (
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-green-600 dark:text-green-400">
|
||||
✓ You are running the latest version
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => checkUpdates()}
|
||||
isLoading={isCheckingUpdates}
|
||||
variant="secondary"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Check for Updates
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user