Compare commits
2 Commits
00c37bd9e8
...
2937fb2372
| Author | SHA1 | Date | |
|---|---|---|---|
| 2937fb2372 | |||
| c66a40df96 |
|
|
@ -4,7 +4,7 @@ export const getLogDates = (name: string): Promise<string[]> =>
|
||||||
export const getLogsForDate = (name: string, date: string): Promise<string[]> =>
|
export const getLogsForDate = (name: string, date: string): Promise<string[]> =>
|
||||||
fetch(`/api/apps/${name}/logs?date=${date}`).then(r => r.json())
|
fetch(`/api/apps/${name}/logs?date=${date}`).then(r => r.json())
|
||||||
|
|
||||||
export const getSystemInfo = (): Promise<{ version: string, sha: string }> =>
|
export const getSystemInfo = (): Promise<{ version: string, sha: string, uptime: number }> =>
|
||||||
fetch('/api/system/info').then(r => r.json())
|
fetch('/api/system/info').then(r => r.json())
|
||||||
|
|
||||||
export const shareApp = (name: string) =>
|
export const shareApp = (name: string) =>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,20 @@ import {
|
||||||
|
|
||||||
type UpdateInfo = { available: boolean, current: string, latest: string, commits: string[] }
|
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) {
|
function pollUntilBack(onBack: () => void, onTimeout?: () => void) {
|
||||||
let elapsed = 0
|
let elapsed = 0
|
||||||
const poll = setInterval(async () => {
|
const poll = setInterval(async () => {
|
||||||
|
|
@ -41,6 +55,7 @@ function pollUntilBack(onBack: () => void, onTimeout?: () => void) {
|
||||||
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 [uptime, setUptime] = useState(0)
|
||||||
const [themeChoice, setThemeChoice] = useState(localStorage.getItem('theme') || 'system')
|
const [themeChoice, setThemeChoice] = useState(localStorage.getItem('theme') || 'system')
|
||||||
const [restarting, setRestarting] = useState(false)
|
const [restarting, setRestarting] = useState(false)
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
||||||
|
|
@ -51,9 +66,16 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
getSystemInfo().then(info => {
|
getSystemInfo().then(info => {
|
||||||
setVersion(info.version)
|
setVersion(info.version)
|
||||||
setSha(info.sha)
|
setSha(info.sha)
|
||||||
|
setUptime(info.uptime)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Tick uptime every second
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => setUptime(u => u + 1000), 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
navigate('/')
|
navigate('/')
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +95,7 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
getSystemInfo().then(info => {
|
getSystemInfo().then(info => {
|
||||||
setVersion(info.version)
|
setVersion(info.version)
|
||||||
setSha(info.sha)
|
setSha(info.sha)
|
||||||
|
setUptime(info.uptime)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,9 +198,14 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<SectionTitle>Server</SectionTitle>
|
<SectionTitle>Server</SectionTitle>
|
||||||
<Button variant="danger" onClick={handleRestart} disabled={restarting}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontSize: 14 }}>
|
||||||
{restarting ? 'Restarting...' : 'Restart Server'}
|
<span>Uptime: {formatUptime(uptime)}</span>
|
||||||
</Button>
|
</div>
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<Button variant="danger" onClick={handleRestart} disabled={restarting}>
|
||||||
|
{restarting ? 'Restarting...' : 'Restart Server'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</Main>
|
</Main>
|
||||||
|
|
|
||||||
|
|
@ -203,12 +203,13 @@ router.sse('/metrics/stream', (send) => {
|
||||||
|
|
||||||
// System info
|
// System info
|
||||||
const projectRoot = join(import.meta.dir, '../../..')
|
const projectRoot = join(import.meta.dir, '../../..')
|
||||||
|
const startedAt = Date.now()
|
||||||
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'))
|
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'))
|
||||||
const sha = Bun.spawnSync(['git', 'rev-parse', '--short', 'HEAD'], { cwd: projectRoot }).stdout.toString().trim() || 'unknown'
|
const sha = Bun.spawnSync(['git', 'rev-parse', '--short', 'HEAD'], { cwd: projectRoot }).stdout.toString().trim() || 'unknown'
|
||||||
let isUpdating = false
|
let isUpdating = false
|
||||||
|
|
||||||
router.get('/info', c => {
|
router.get('/info', c => {
|
||||||
return c.json({ version: pkg.version, sha })
|
return c.json({ version: pkg.version, sha, uptime: Date.now() - startedAt })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get recent unified logs
|
// Get recent unified logs
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ import { LOCAL_HOST } from '%config'
|
||||||
import { networkInterfaces } from 'os'
|
import { networkInterfaces } from 'os'
|
||||||
import { hostLog } from './tui'
|
import { hostLog } from './tui'
|
||||||
|
|
||||||
|
const MAX_REPUBLISH_DELAY = 30_000
|
||||||
|
const REPUBLISH_BASE_DELAY = 1_000
|
||||||
|
|
||||||
|
const _killed = new Set<string>()
|
||||||
const _publishers = new Map<string, Subprocess>()
|
const _publishers = new Map<string, Subprocess>()
|
||||||
|
const _republishAttempts = new Map<string, number>()
|
||||||
|
|
||||||
const isEnabled = process.env.NODE_ENV === 'production' && process.platform === 'linux'
|
const isEnabled = process.env.NODE_ENV === 'production' && process.platform === 'linux'
|
||||||
|
|
||||||
|
|
@ -51,10 +56,12 @@ export function publishApp(name: string) {
|
||||||
})
|
})
|
||||||
|
|
||||||
_publishers.set(name, proc)
|
_publishers.set(name, proc)
|
||||||
|
_republishAttempts.delete(name)
|
||||||
hostLog(`mDNS: published ${host} -> ${ip}`)
|
hostLog(`mDNS: published ${host} -> ${ip}`)
|
||||||
|
|
||||||
proc.exited.then(() => {
|
proc.exited.then(() => {
|
||||||
_publishers.delete(name)
|
_publishers.delete(name)
|
||||||
|
if (!_killed.delete(name)) republish(name)
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
hostLog(`mDNS: failed to publish ${host}`)
|
hostLog(`mDNS: failed to publish ${host}`)
|
||||||
|
|
@ -64,9 +71,11 @@ export function publishApp(name: string) {
|
||||||
export function unpublishApp(name: string) {
|
export function unpublishApp(name: string) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
|
|
||||||
|
_republishAttempts.delete(name)
|
||||||
const proc = _publishers.get(name)
|
const proc = _publishers.get(name)
|
||||||
if (!proc) return
|
if (!proc) return
|
||||||
|
|
||||||
|
_killed.add(name)
|
||||||
proc.kill()
|
proc.kill()
|
||||||
_publishers.delete(name)
|
_publishers.delete(name)
|
||||||
hostLog(`mDNS: unpublished ${toSubdomain(name)}.${LOCAL_HOST}`)
|
hostLog(`mDNS: unpublished ${toSubdomain(name)}.${LOCAL_HOST}`)
|
||||||
|
|
@ -75,9 +84,20 @@ export function unpublishApp(name: string) {
|
||||||
export function unpublishAll() {
|
export function unpublishAll() {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
|
|
||||||
|
_republishAttempts.clear()
|
||||||
for (const [name, proc] of _publishers) {
|
for (const [name, proc] of _publishers) {
|
||||||
|
_killed.add(name)
|
||||||
proc.kill()
|
proc.kill()
|
||||||
hostLog(`mDNS: unpublished ${toSubdomain(name)}.${LOCAL_HOST}`)
|
hostLog(`mDNS: unpublished ${toSubdomain(name)}.${LOCAL_HOST}`)
|
||||||
}
|
}
|
||||||
_publishers.clear()
|
_publishers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function republish(name: string) {
|
||||||
|
const attempts = _republishAttempts.get(name) ?? 0
|
||||||
|
const delay = Math.min(REPUBLISH_BASE_DELAY * 2 ** attempts, MAX_REPUBLISH_DELAY)
|
||||||
|
_republishAttempts.set(name, attempts + 1)
|
||||||
|
|
||||||
|
hostLog(`mDNS: ${toSubdomain(name)}.${LOCAL_HOST} exited unexpectedly, retrying in ${delay}ms`)
|
||||||
|
setTimeout(() => publishApp(name), delay)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user