214 lines
6.7 KiB
TypeScript
214 lines
6.7 KiB
TypeScript
import { useEffect, useState } from 'hono/jsx'
|
|
import { applyUpdate, checkForUpdate, getSystemInfo, restartServer } from '../api'
|
|
import { setTheme } from '../index'
|
|
import { navigate } from '../router'
|
|
import {
|
|
Button,
|
|
DashboardInstallCmd,
|
|
FormField,
|
|
FormLabel,
|
|
FormSelect,
|
|
HeaderActions,
|
|
Main,
|
|
MainContent,
|
|
MainHeader,
|
|
MainTitle,
|
|
Section,
|
|
SectionTitle,
|
|
} from '../styles'
|
|
|
|
type UpdateInfo = { available: boolean, current: string, latest: string, commits: string[] }
|
|
|
|
function formatUptime(ms: number): string {
|
|
const seconds = Math.floor(ms / 1000)
|
|
const days = Math.floor(seconds / 86400)
|
|
const hours = Math.floor((seconds % 86400) / 3600)
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
const secs = seconds % 60
|
|
const parts: string[] = []
|
|
if (days > 0) parts.push(`${days}d`)
|
|
if (hours > 0) parts.push(`${hours}h`)
|
|
if (minutes > 0) parts.push(`${minutes}m`)
|
|
parts.push(`${secs}s`)
|
|
return parts.join(' ')
|
|
}
|
|
|
|
function pollUntilBack(onBack: () => void, onTimeout?: () => void) {
|
|
let elapsed = 0
|
|
const poll = setInterval(async () => {
|
|
elapsed += 2000
|
|
if (elapsed > 60000) {
|
|
clearInterval(poll)
|
|
onTimeout?.()
|
|
return
|
|
}
|
|
try {
|
|
const res = await fetch('/api/system/info')
|
|
if (res.ok) {
|
|
clearInterval(poll)
|
|
onBack()
|
|
}
|
|
} catch {}
|
|
}, 2000)
|
|
}
|
|
|
|
export function SettingsPage({ render }: { render: () => void }) {
|
|
const [version, setVersion] = useState('')
|
|
const [sha, setSha] = useState('')
|
|
const [uptime, setUptime] = useState(0)
|
|
const [themeChoice, setThemeChoice] = useState(localStorage.getItem('theme') || 'system')
|
|
const [restarting, setRestarting] = useState(false)
|
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
|
const [checking, setChecking] = useState(false)
|
|
const [updating, setUpdating] = useState(false)
|
|
|
|
useEffect(() => {
|
|
getSystemInfo().then(info => {
|
|
setVersion(info.version)
|
|
setSha(info.sha)
|
|
setUptime(info.uptime)
|
|
})
|
|
}, [])
|
|
|
|
// Tick uptime every second
|
|
useEffect(() => {
|
|
const interval = setInterval(() => setUptime(u => u + 1000), 1000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
const goBack = () => {
|
|
navigate('/')
|
|
}
|
|
|
|
const handleThemeChange = (e: Event) => {
|
|
const value = (e.target as HTMLSelectElement).value
|
|
setThemeChoice(value)
|
|
if (value === 'system') {
|
|
localStorage.removeItem('theme')
|
|
} else {
|
|
localStorage.setItem('theme', value)
|
|
}
|
|
setTheme()
|
|
}
|
|
|
|
const refreshSystemInfo = () => {
|
|
getSystemInfo().then(info => {
|
|
setVersion(info.version)
|
|
setSha(info.sha)
|
|
setUptime(info.uptime)
|
|
})
|
|
}
|
|
|
|
const handleRestart = () => {
|
|
if (!confirm('Are you sure you want to restart the server?')) return
|
|
setRestarting(true)
|
|
restartServer().catch(() => {})
|
|
pollUntilBack(
|
|
() => { setRestarting(false); refreshSystemInfo() },
|
|
() => { setRestarting(false) },
|
|
)
|
|
}
|
|
|
|
const handleCheckUpdate = async () => {
|
|
setChecking(true)
|
|
try {
|
|
const info = await checkForUpdate()
|
|
setUpdateInfo(info)
|
|
} catch {
|
|
setUpdateInfo(null)
|
|
}
|
|
setChecking(false)
|
|
}
|
|
|
|
const handleApplyUpdate = async () => {
|
|
if (!confirm('This will update and restart the server. Continue?')) return
|
|
setUpdating(true)
|
|
try {
|
|
await applyUpdate()
|
|
pollUntilBack(
|
|
() => { setUpdating(false); refreshSystemInfo(); setUpdateInfo(null) },
|
|
() => { setUpdating(false) },
|
|
)
|
|
} catch {
|
|
setUpdating(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Main>
|
|
<MainHeader centered>
|
|
<MainTitle>Settings</MainTitle>
|
|
<HeaderActions>
|
|
<Button onClick={goBack}>Back</Button>
|
|
</HeaderActions>
|
|
</MainHeader>
|
|
<MainContent centered>
|
|
<Section>
|
|
<SectionTitle>Theme</SectionTitle>
|
|
<FormField>
|
|
<FormLabel>Appearance</FormLabel>
|
|
<FormSelect onChange={handleThemeChange}>
|
|
<option value="system" selected={themeChoice === 'system'}>System</option>
|
|
<option value="light" selected={themeChoice === 'light'}>Light</option>
|
|
<option value="dark" selected={themeChoice === 'dark'}>Dark</option>
|
|
</FormSelect>
|
|
</FormField>
|
|
</Section>
|
|
<Section>
|
|
<SectionTitle>About</SectionTitle>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 14 }}>
|
|
<span>Version: {version}</span>
|
|
<span>SHA: <a href={`https://git.nose.space/defunkt/toes/commit/${sha}`} target="_blank">{sha}</a></span>
|
|
</div>
|
|
</Section>
|
|
<Section>
|
|
<SectionTitle>Install CLI</SectionTitle>
|
|
<DashboardInstallCmd>
|
|
curl -fsSL {location.origin}/install | bash
|
|
</DashboardInstallCmd>
|
|
</Section>
|
|
<Section>
|
|
<SectionTitle>Update</SectionTitle>
|
|
{updating ? (
|
|
<div style={{ fontSize: 14 }}>Updating... server will restart shortly.</div>
|
|
) : updateInfo?.available ? (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
<div style={{ fontSize: 14 }}>
|
|
{updateInfo.commits.length} update{updateInfo.commits.length !== 1 ? 's' : ''} available ({updateInfo.current} → {updateInfo.latest})
|
|
</div>
|
|
<div style={{ fontSize: 13, color: 'var(--colors-textMuted)', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
{updateInfo.commits.map(c => <div>{c}</div>)}
|
|
</div>
|
|
<div>
|
|
<Button variant="primary" onClick={handleApplyUpdate}>Update & Restart</Button>
|
|
</div>
|
|
</div>
|
|
) : updateInfo ? (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<span style={{ fontSize: 14 }}>Up to date</span>
|
|
<Button onClick={handleCheckUpdate} disabled={checking}>
|
|
{checking ? 'Checking...' : 'Check Again'}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<Button onClick={handleCheckUpdate} disabled={checking}>
|
|
{checking ? 'Checking...' : 'Check for Updates'}
|
|
</Button>
|
|
)}
|
|
</Section>
|
|
<Section>
|
|
<SectionTitle>Server</SectionTitle>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 14 }}>
|
|
<span>Uptime: {formatUptime(uptime)}</span>
|
|
</div>
|
|
<div style={{ marginTop: 12 }}>
|
|
<Button variant="danger" onClick={handleRestart} disabled={restarting}>
|
|
{restarting ? 'Restarting...' : 'Restart Server'}
|
|
</Button>
|
|
</div>
|
|
</Section>
|
|
</MainContent>
|
|
</Main>
|
|
)
|
|
}
|