This commit is contained in:
eric
2026-03-25 12:29:03 -05:00
parent fce2c49b62
commit cc2f30b6e2
180 changed files with 20523 additions and 0 deletions

208
app/(tabs)/system.tsx Normal file
View File

@@ -0,0 +1,208 @@
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 },
})