forked from defunkt/toes
real settings
This commit is contained in:
parent
b152e0d3e8
commit
f54cc401dc
|
|
@ -13,8 +13,17 @@ export const shareApp = (name: string) =>
|
||||||
export const unshareApp = (name: string) =>
|
export const unshareApp = (name: string) =>
|
||||||
fetch(`/api/apps/${name}/tunnel`, { method: 'DELETE' })
|
fetch(`/api/apps/${name}/tunnel`, { method: 'DELETE' })
|
||||||
|
|
||||||
|
export const applyUpdate = () =>
|
||||||
|
fetch('/api/system/update', { method: 'POST' }).then(r => r.json())
|
||||||
|
|
||||||
|
export const checkForUpdate = (): Promise<{ available: boolean, current: string, latest: string, commits: string[] }> =>
|
||||||
|
fetch('/api/system/update').then(r => r.json())
|
||||||
|
|
||||||
export const restartApp = (name: string) => fetch(`/api/apps/${name}/restart`, { method: 'POST' })
|
export const restartApp = (name: string) => fetch(`/api/apps/${name}/restart`, { method: 'POST' })
|
||||||
|
|
||||||
|
export const restartServer = () =>
|
||||||
|
fetch('/api/system/restart', { method: 'POST' }).then(r => r.json())
|
||||||
|
|
||||||
export const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' })
|
export const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' })
|
||||||
|
|
||||||
export const stopApp = (name: string) => fetch(`/api/apps/${name}/stop`, { method: 'POST' })
|
export const stopApp = (name: string) => fetch(`/api/apps/${name}/stop`, { method: 'POST' })
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { useEffect, useState } from 'hono/jsx'
|
import { useEffect, useState } from 'hono/jsx'
|
||||||
import { getSystemInfo } from '../api'
|
import { applyUpdate, checkForUpdate, getSystemInfo, restartServer } from '../api'
|
||||||
|
import { setTheme } from '../index'
|
||||||
import { navigate } from '../router'
|
import { navigate } from '../router'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DashboardInstallCmd,
|
DashboardInstallCmd,
|
||||||
|
FormField,
|
||||||
|
FormLabel,
|
||||||
|
FormSelect,
|
||||||
HeaderActions,
|
HeaderActions,
|
||||||
Main,
|
Main,
|
||||||
MainContent,
|
MainContent,
|
||||||
|
|
@ -13,9 +17,28 @@ import {
|
||||||
SectionTitle,
|
SectionTitle,
|
||||||
} from '../styles'
|
} from '../styles'
|
||||||
|
|
||||||
|
type UpdateInfo = { available: boolean, current: string, latest: string, commits: string[] }
|
||||||
|
|
||||||
|
function pollUntilBack(onBack: () => void) {
|
||||||
|
const poll = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/system/info')
|
||||||
|
if (res.ok) {
|
||||||
|
clearInterval(poll)
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
export function SettingsPage({ render }: { render: () => void }) {
|
export function SettingsPage({ render }: { render: () => void }) {
|
||||||
const [version, setVersion] = useState('')
|
const [version, setVersion] = useState('')
|
||||||
const [sha, setSha] = useState('')
|
const [sha, setSha] = useState('')
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
getSystemInfo().then(info => {
|
getSystemInfo().then(info => {
|
||||||
|
|
@ -28,6 +51,57 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
navigate('/')
|
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 handleRestart = () => {
|
||||||
|
if (!confirm('Are you sure you want to restart the server?')) return
|
||||||
|
setRestarting(true)
|
||||||
|
restartServer().catch(() => {})
|
||||||
|
pollUntilBack(() => {
|
||||||
|
setRestarting(false)
|
||||||
|
getSystemInfo().then(info => {
|
||||||
|
setVersion(info.version)
|
||||||
|
setSha(info.sha)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
} catch {}
|
||||||
|
pollUntilBack(() => {
|
||||||
|
setUpdating(false)
|
||||||
|
getSystemInfo().then(info => {
|
||||||
|
setVersion(info.version)
|
||||||
|
setSha(info.sha)
|
||||||
|
})
|
||||||
|
setUpdateInfo(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Main>
|
<Main>
|
||||||
<MainHeader centered>
|
<MainHeader centered>
|
||||||
|
|
@ -37,11 +111,22 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
</HeaderActions>
|
</HeaderActions>
|
||||||
</MainHeader>
|
</MainHeader>
|
||||||
<MainContent centered>
|
<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>
|
<Section>
|
||||||
<SectionTitle>About</SectionTitle>
|
<SectionTitle>About</SectionTitle>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 14 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 14 }}>
|
||||||
<span>Version: {version}</span>
|
<span>Version: {version}</span>
|
||||||
<span>SHA: {sha}</span>
|
<span>SHA: <a href={`https://git.nose.space/defunkt/toes/commit/${sha}`} target="_blank">{sha}</a></span>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
|
|
@ -50,6 +135,41 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
curl -fsSL {location.origin}/install | bash
|
curl -fsSL {location.origin}/install | bash
|
||||||
</DashboardInstallCmd>
|
</DashboardInstallCmd>
|
||||||
</Section>
|
</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>
|
||||||
|
<Button variant="danger" onClick={handleRestart} disabled={restarting}>
|
||||||
|
{restarting ? 'Restarting...' : 'Restart Server'}
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</Main>
|
</Main>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,18 @@ const render = () => {
|
||||||
initUpdate(render)
|
initUpdate(render)
|
||||||
initToolIframes()
|
initToolIframes()
|
||||||
|
|
||||||
// Set theme based on system preference
|
// Set theme based on localStorage preference or system preference
|
||||||
const setTheme = () => {
|
export const setTheme = () => {
|
||||||
|
const stored = localStorage.getItem('theme')
|
||||||
|
if (stored === 'light' || stored === 'dark') {
|
||||||
|
document.documentElement.setAttribute('data-theme', stored)
|
||||||
|
return
|
||||||
|
}
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light')
|
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for system theme changes
|
// Listen for system theme changes (only applies when using system theme)
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
setTheme()
|
setTheme()
|
||||||
render()
|
render()
|
||||||
|
|
|
||||||
|
|
@ -266,4 +266,55 @@ onChange(collectLogs)
|
||||||
// Subscribe to host-level log messages
|
// Subscribe to host-level log messages
|
||||||
onHostLog(pushHostLog)
|
onHostLog(pushHostLog)
|
||||||
|
|
||||||
|
// Restart server (systemd brings it back)
|
||||||
|
router.post('/restart', c => {
|
||||||
|
setTimeout(() => process.exit(0), 100)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
router.get('/update', async c => {
|
||||||
|
const cwd = join(import.meta.dir, '../../..')
|
||||||
|
try {
|
||||||
|
const fetch = Bun.spawnSync(['git', 'fetch', 'origin', 'main'], { cwd })
|
||||||
|
if (fetch.exitCode !== 0) return c.json({ available: false })
|
||||||
|
|
||||||
|
const log = Bun.spawnSync(['git', 'log', 'HEAD..origin/main', '--oneline'], { cwd })
|
||||||
|
const output = log.stdout.toString().trim()
|
||||||
|
const commits = output ? output.split('\n') : []
|
||||||
|
|
||||||
|
const latest = Bun.spawnSync(['git', 'rev-parse', '--short', 'origin/main'], { cwd })
|
||||||
|
.stdout.toString().trim()
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
available: commits.length > 0,
|
||||||
|
current: sha,
|
||||||
|
latest: commits.length > 0 ? latest : sha,
|
||||||
|
commits,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return c.json({ available: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply update and restart
|
||||||
|
router.post('/update', async c => {
|
||||||
|
const cwd = join(import.meta.dir, '../../..')
|
||||||
|
try {
|
||||||
|
const pull = Bun.spawnSync(['git', 'pull', 'origin', 'main'], { cwd })
|
||||||
|
if (pull.exitCode !== 0) return c.json({ ok: false, error: 'git pull failed' }, 500)
|
||||||
|
|
||||||
|
const install = Bun.spawnSync(['bun', 'install'], { cwd })
|
||||||
|
if (install.exitCode !== 0) return c.json({ ok: false, error: 'bun install failed' }, 500)
|
||||||
|
|
||||||
|
const build = Bun.spawnSync(['bun', 'run', 'build'], { cwd })
|
||||||
|
if (build.exitCode !== 0) return c.json({ ok: false, error: 'build failed' }, 500)
|
||||||
|
|
||||||
|
setTimeout(() => process.exit(0), 100)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
} catch {
|
||||||
|
return c.json({ ok: false, error: 'update failed' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user