Files
2026-03-25 12:29:03 -05:00

209 lines
7.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import { View, Text, ScrollView, StyleSheet, RefreshControl } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useBind } from '@/context/BindContext'
import { apiFetch } from '@/lib/api'
import type { SystemMetricsPayload } from '@/lib/pcTypes'
import { theme } from '@/constants/theme'
function fmtBytes(n: number | undefined): string {
if (n == null || Number.isNaN(n)) return '—'
if (n < 1024) return `${n} B`
if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`
if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(1)} MB`
return `${(n / 1024 ** 3).toFixed(2)} GB`
}
export default function SystemScreen() {
const { bind } = useBind()
const [data, setData] = useState<SystemMetricsPayload | null>(null)
const [err, setErr] = useState<string | null>(null)
const [refreshing, setRefreshing] = useState(false)
const load = useCallback(async () => {
if (!bind) {
setData(null)
setErr(null)
return
}
try {
const res = await apiFetch(bind, '/system_metrics')
const json = (await res.json()) as SystemMetricsPayload
if (!res.ok) {
setErr(json.error || `HTTP ${res.status}`)
return
}
setErr(null)
setData(json)
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
}
}, [bind])
useEffect(() => {
void load()
}, [load])
useEffect(() => {
if (!bind) return
const id = setInterval(() => void load(), 2000)
return () => clearInterval(id)
}, [bind, load])
const onRefresh = useCallback(async () => {
setRefreshing(true)
await load()
setRefreshing(false)
}, [load])
const m = data?.memory
const net = data?.network
return (
<SafeAreaView style={styles.safe} edges={['bottom']}>
<ScrollView
contentContainerStyle={styles.scroll}
refreshControl={
bind ? (
<RefreshControl refreshing={refreshing} onRefresh={() => void onRefresh()} tintColor={theme.accent} />
) : undefined
}
>
<Text style={styles.h1}></Text>
<Text style={styles.lead}> Windows </Text>
{!bind && (
<View style={styles.banner}>
<Text style={styles.bannerText}> PC 2 </Text>
</View>
)}
<View style={styles.card}>
<Text style={styles.cardTitle}></Text>
<Text style={styles.desc}>
ffmpeg
</Text>
{err && bind ? (
<Text style={styles.err}>{err}</Text>
) : null}
<View style={styles.metricsGrid}>
<View style={styles.block}>
<Text style={styles.blockTitle}>CPU</Text>
<Text style={styles.big}>{data?.cpu_percent != null ? `${data.cpu_percent.toFixed(1)}%` : '—'}</Text>
<Text style={styles.sub}> {data?.cpu_count_logical ?? '—'} </Text>
</View>
<View style={styles.block}>
<Text style={styles.blockTitle}></Text>
<Text style={styles.big}>{m?.percent != null ? `${m.percent.toFixed(1)}%` : '—'}</Text>
<Text style={styles.sub}>
{fmtBytes(m?.used)} / {fmtBytes(m?.total)} {fmtBytes(m?.available)}
</Text>
</View>
<View style={styles.block}>
<Text style={styles.blockTitle}></Text>
<Text style={styles.rate}>
{net?.up_mbps != null ? `${net.up_mbps.toFixed(2)} Mbps` : '—'}
</Text>
<Text style={styles.rate}>
{net?.down_mbps != null ? `${net.down_mbps.toFixed(2)} Mbps` : '—'}
</Text>
<Text style={styles.sub}></Text>
</View>
</View>
{data?.swap && data.swap.total != null && data.swap.total > 0 ? (
<Text style={styles.swap}>
{data.swap.percent != null ? `${data.swap.percent.toFixed(0)}%` : '—'} ·{' '}
{fmtBytes(data.swap.used)} / {fmtBytes(data.swap.total)}
</Text>
) : null}
{data?.disks && data.disks.length > 0 ? (
<View style={styles.diskSec}>
<Text style={styles.diskTitle}></Text>
{data.disks.slice(0, 8).map((d, i) => (
<Text key={i} style={styles.diskLine}>
<Text style={styles.diskMp}>{d.mountpoint}</Text> ·{' '}
{d.percent != null ? `${d.percent.toFixed(0)}%` : '—'} · {fmtBytes(d.free)}
</Text>
))}
</View>
) : null}
{data?.ffmpeg_processes && data.ffmpeg_processes.length > 0 ? (
<View style={styles.ffSec}>
<Text style={styles.diskTitle}>ffmpeg </Text>
{data.ffmpeg_processes.map((p, i) => (
<Text key={i} style={styles.ffLine}>
PID {p.pid} · {p.name ?? '—'} · {fmtBytes(p.rss)}
</Text>
))}
</View>
) : null}
</View>
<View style={styles.cardMuted}>
<Text style={styles.cardTitle}></Text>
<Text style={styles.muted}> Electron </Text>
</View>
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: theme.bg },
scroll: { padding: 16, paddingBottom: 40 },
h1: { fontSize: 20, fontWeight: '700', color: theme.text, marginBottom: 6 },
lead: { fontSize: 14, color: theme.muted, marginBottom: 14, lineHeight: 22 },
banner: {
padding: 12,
borderRadius: 10,
backgroundColor: 'rgba(124,92,255,0.08)',
borderWidth: 1,
borderColor: 'rgba(124,92,255,0.22)',
marginBottom: 14,
},
bannerText: { fontSize: 13, color: theme.text, lineHeight: 20 },
card: {
padding: 14,
borderRadius: 12,
backgroundColor: theme.bgCard,
borderWidth: 1,
borderColor: 'rgba(124,92,255,0.22)',
marginBottom: 12,
},
cardMuted: {
padding: 14,
borderRadius: 12,
backgroundColor: theme.bgElevated,
borderWidth: 1,
borderColor: theme.border,
},
cardTitle: { fontSize: 16, fontWeight: '600', color: theme.text, marginBottom: 8 },
desc: { fontSize: 13, color: theme.muted, marginBottom: 12, lineHeight: 20 },
err: { color: theme.danger, marginBottom: 10, fontSize: 13 },
metricsGrid: { gap: 12 },
block: {
padding: 12,
borderRadius: 10,
backgroundColor: theme.bgElevated,
borderWidth: 1,
borderColor: theme.border,
},
blockTitle: { fontSize: 11, fontWeight: '700', color: theme.muted, marginBottom: 6 },
big: { fontSize: 22, fontWeight: '700', color: theme.text },
sub: { fontSize: 11, color: theme.muted, marginTop: 4, lineHeight: 16 },
rate: { fontSize: 15, fontWeight: '600', color: theme.accent, fontFamily: 'monospace' },
swap: { fontSize: 12, color: theme.muted, marginTop: 10, marginBottom: 8 },
diskSec: { marginTop: 8 },
diskTitle: { fontSize: 13, fontWeight: '600', color: theme.text, marginBottom: 8 },
diskLine: { fontSize: 12, color: theme.muted, marginBottom: 6 },
diskMp: { fontFamily: 'monospace', color: theme.text },
ffSec: { marginTop: 12 },
ffLine: { fontSize: 12, color: theme.muted, marginBottom: 4, fontFamily: 'monospace' },
muted: { fontSize: 13, color: theme.muted, lineHeight: 20 },
})