diff --git a/main.go b/main.go index 4e99ab9..145f4f3 100644 --- a/main.go +++ b/main.go @@ -169,7 +169,7 @@ func main() { } func serverUpdateTicker() { - ticker := time.NewTicker(10 * time.Second) + ticker := time.NewTicker(60 * time.Second) for range ticker.C { updateServers() } diff --git a/site/src/components/charts/chart-time-select.tsx b/site/src/components/charts/chart-time-select.tsx new file mode 100644 index 0000000..0cfe125 --- /dev/null +++ b/site/src/components/charts/chart-time-select.tsx @@ -0,0 +1,35 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { $chartTime } from '@/lib/stores' +import { cn } from '@/lib/utils' +import { useStore } from '@nanostores/react' +import { useEffect } from 'react' + +export default function ChartTimeSelect({ className }: { className?: string }) { + const chartTime = useStore($chartTime) + + useEffect(() => { + // todo make sure this doesn't cause multiple fetches on load + return () => $chartTime.set('1h') + }, []) + + return ( + + ) +} diff --git a/site/src/components/charts/cpu-chart.tsx b/site/src/components/charts/cpu-chart.tsx index 6ee3ac3..ca92893 100644 --- a/site/src/components/charts/cpu-chart.tsx +++ b/site/src/components/charts/cpu-chart.tsx @@ -8,9 +8,6 @@ import { } from '@/components/ui/chart' import { formatShortDate, formatShortTime } from '@/lib/utils' import Spinner from '../spinner' -// for (const data of chartData) { -// data.month = formatDateShort(data.month) -// } const chartConfig = { cpu: { diff --git a/site/src/components/charts/radial.tsx b/site/src/components/charts/radial.tsx deleted file mode 100644 index 6852e4a..0000000 --- a/site/src/components/charts/radial.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { TrendingUp } from 'lucide-react' -import { Label, PolarRadiusAxis, RadialBar, RadialBarChart } from 'recharts' - -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card' -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from '@/components/ui/chart' -const chartData = [{ month: 'january', desktop: 1260, mobile: 570 }] - -const chartConfig = { - mobile: { - label: 'Mobile', - color: 'hsl(var(--chart-2))', - }, -} satisfies ChartConfig - -export function RadialChart() { - const totalVisitors = chartData[0].desktop + chartData[0].mobile - - return ( - - - Radial Chart - Stacked - January - June 2024 - - - - - } /> - - - - - - - - -
- Trending up by 5.2% this month -
-
- Showing total visitors for the last 6 months -
-
-
- ) -} diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx index da75dfd..73f491c 100644 --- a/site/src/components/routes/server.tsx +++ b/site/src/components/routes/server.tsx @@ -1,24 +1,13 @@ -import { $systems, pb } from '@/lib/stores' +import { $updatedSystem, $systems, pb } from '@/lib/stores' import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types' -import { Suspense, lazy, useEffect, useState } from 'react' +import { Suspense, lazy, useEffect, useMemo, useState } from 'react' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card' import { useStore } from '@nanostores/react' import Spinner from '../spinner' -// import CpuChart from '../charts/cpu-chart' -// import MemChart from '../charts/mem-chart' -// import DiskChart from '../charts/disk-chart' -// import ContainerCpuChart from '../charts/container-cpu-chart' -// import ContainerMemChart from '../charts/container-mem-chart' -import { CpuIcon, MemoryStickIcon, ServerIcon } from 'lucide-react' -import { RadialChart } from '../charts/radial' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { Separator } from '@/components/ui/separator' +import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react' +import ChartTimeSelect from '../charts/chart-time-select' +import { cn, getPbTimestamp } from '@/lib/utils' +import { Separator } from '../ui/separator' const CpuChart = lazy(() => import('../charts/cpu-chart')) const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart')) @@ -31,15 +20,9 @@ function timestampToBrowserTime(timestamp: string) { return date.toLocaleString() } -// function addColors(objects: Record[]) { -// objects.forEach((obj, index) => { -// const hue = ((index * 360) / objects.length) % 360 // Distribute hues evenly -// obj.fill = `hsl(${hue}, 100%, 50%)` // Set fill to HSL color with full saturation and 50% lightness -// }) -// } - export default function ServerDetail({ name }: { name: string }) { const servers = useStore($systems) + const updatedSystem = useStore($updatedSystem) const [server, setServer] = useState({} as SystemRecord) const [containers, setContainers] = useState([] as ContainerStatsRecord[]) @@ -59,38 +42,60 @@ export default function ServerDetail({ name }: { name: string }) { ) useEffect(() => { - document.title = `${name} / Qoma` - return () => { - setContainerCpuChartData([]) - setCpuChartData([]) - setMemChartData([]) - setDiskChartData([]) + document.title = `${name} / Beszel` + if (server?.id && server.name === name) { + return } - }, [name]) + const matchingServer = servers.find((s) => s.name === name) as SystemRecord + if (matchingServer) { + setServer(matchingServer) + } + }, [name, server]) + + // if visiting directly, make sure server gets set when servers are loaded + // useEffect(() => { + // if (!('id' in server)) { + // const matchingServer = servers.find((s) => s.name === name) as SystemRecord + // if (matchingServer) { + // console.log('setting server') + // setServer(matchingServer) + // } + // } + // }, [servers]) // get stats useEffect(() => { - if (!('name' in server)) { + if (!('id' in server)) { + console.log('no id in server') return + } else { + console.log('id in server') } - pb.collection('system_stats') - .getList(1, 60, { - filter: `system="${server.id}"`, + .getFullList({ + filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`, fields: 'created,stats', sort: '-created', }) .then((records) => { // console.log('sctats', records) - setServerStats(records.items) + setServerStats(records) }) - }, [server, servers]) + }, [server]) + + useEffect(() => { + if (updatedSystem.id === server.id) { + setServer(updatedSystem) + } + }, [updatedSystem]) // get cpu data useEffect(() => { if (!serverStats.length) { return } + + console.log('stats', serverStats) // let maxCpu = 0 const cpuData = [] as { time: string; cpu: number }[] const memData = [] as { time: string; mem: number; memUsed: number; memCache: number }[] @@ -107,26 +112,17 @@ export default function ServerDetail({ name }: { name: string }) { }, [serverStats]) useEffect(() => { - if ($systems.get().length === 0) { - // console.log('skipping') - return - } - // console.log('running') - const matchingServer = servers.find((s) => s.name === name) as SystemRecord - // console.log('found server', matchingServer) - setServer(matchingServer) - pb.collection('container_stats') - .getList(1, 60, { - filter: `system="${matchingServer.id}"`, + .getFullList({ + filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`, fields: 'created,stats', sort: '-created', }) .then((records) => { - // console.log('records', records) - setContainers(records.items) + // console.log('containers', records) + setContainers(records) }) - }, [servers, name]) + }, [server]) // container stats for charts useEffect(() => { @@ -148,98 +144,85 @@ export default function ServerDetail({ name }: { name: string }) { setContainerCpuChartData(containerCpuData.reverse()) setContainerMemChartData(containerMemData.reverse()) }, [containers]) + const uptime = useMemo(() => { + console.log('making uptime') + let uptime = server.info?.u || 0 + if (uptime < 172800) { + return `${Math.floor(uptime / 3600)} hours` + } + return `${Math.floor(server.info?.u / 86400)} days` + }, [server.info?.u]) + + if (!('id' in server)) { + return null + } return ( - <> -
- - - {name} - - -

{server.status}

-

Uptime {(server.info?.u / 86400).toLocaleString()} days

-

- {server.info?.m} ({server.info?.c} cores / {server.info?.t} threads) -

-
-
- - - +
+ +
+

{server.name}

+
+
+ + {server.status === 'up' && ( + + )} + + + {server.status} +
+ +
+ {server.host} +
+ +
+ {uptime} +
+ +
+ + {server.info?.m} ({server.info?.c}c / {server.info.t}t) +
+
+
+
- - - - Total CPU Usage - - - - System-wide CPU utilization of the preceding one minute as a percentage - - - - }> - - - - - {containerCpuChartData.length > 0 && ( - - - - Docker CPU Usage - - {' '} - CPU utilization of docker containers - - - }> - - - - - )} - - - Total Memory Usage - Precise utilization at the recorded time - - - }> - - - - - {containerMemChartData.length > 0 && ( - - - - Docker Memory Usage - - {' '} - Memory usage of docker containers - - - }> - - - - - )} - - - Disk Usage - Precise usage at the recorded time - - - }> - - - - -
+ + + + {containerCpuChartData.length > 0 && ( + + + + )} + + + + + {containerMemChartData.length > 0 && ( + + + + )} + + + {server.name} @@ -251,15 +234,31 @@ export default function ServerDetail({ name }: { name: string }) {
{JSON.stringify(server, null, 2)}
- - {/* - - Containers - - -
{JSON.stringify(containers, null, 2)}
-
-
*/} - +
+ ) +} + +function ChartCard({ + title, + description, + children, +}: { + title: string + description: string + children: React.ReactNode +}) { + return ( + + + + {title} + + + {description} + + + }>{children} + + ) } diff --git a/site/src/components/server-table/systems-table.tsx b/site/src/components/server-table/systems-table.tsx index 149aed9..ad67f9e 100644 --- a/site/src/components/server-table/systems-table.tsx +++ b/site/src/components/server-table/systems-table.tsx @@ -66,7 +66,8 @@ import AlertsButton from '../table-alerts' function CellFormatter(info: CellContext) { const val = info.getValue() as number return ( -
+
+ {val.toFixed(2)}% ) { style={{ transform: `scalex(${val}%)` }} > - {val.toFixed(2)}%
) } diff --git a/site/src/index.css b/site/src/index.css index 922d8ed..984c65f 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -3,7 +3,7 @@ @tailwind utilities; @layer base { :root { - --background: 30 8% 97.45%; + --background: 30 8% 98.5%; --foreground: 30 0% 0%; --card: 30 0% 100%; --card-foreground: 240 6.67% 2.94%; diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts index 59a8cc6..161e5e7 100644 --- a/site/src/lib/stores.ts +++ b/site/src/lib/stores.ts @@ -25,8 +25,14 @@ export const $authenticated = atom(pb.authStore.isValid) /** List of system records */ export const $systems = atom([] as SystemRecord[]) +/** Last updated system record (realtime) */ +export const $updatedSystem = atom({} as SystemRecord) + /** List of alert records */ export const $alerts = atom([] as AlertRecord[]) /** SSH public key */ export const $publicKey = atom('') + +/** Chart time period */ +export const $chartTime = atom('1h') diff --git a/site/src/lib/utils.ts b/site/src/lib/utils.ts index dcd8875..dcf7b88 100644 --- a/site/src/lib/utils.ts +++ b/site/src/lib/utils.ts @@ -96,3 +96,25 @@ export function updateRecordList( } $store.set(newRecords) } + +export function getPbTimestamp(timeString: string) { + const now = new Date() + let timeValue = parseInt(timeString.slice(0, -1)) + let unit = timeString.slice(-1) + + if (unit === 'h') { + now.setUTCHours(now.getUTCHours() - timeValue) + } else { + // d + now.setUTCDate(now.getUTCDate() - timeValue) + } + + const year = now.getUTCFullYear() + const month = String(now.getUTCMonth() + 1).padStart(2, '0') + const day = String(now.getUTCDate()).padStart(2, '0') + const hours = String(now.getUTCHours()).padStart(2, '0') + const minutes = String(now.getUTCMinutes()).padStart(2, '0') + const seconds = String(now.getUTCSeconds()).padStart(2, '0') + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` +} diff --git a/site/src/main.tsx b/site/src/main.tsx index b1de0e2..609e387 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -3,7 +3,15 @@ import React, { Suspense, lazy, useEffect } from 'react' import ReactDOM from 'react-dom/client' import Home from './components/routes/home.tsx' import { ThemeProvider } from './components/theme-provider.tsx' -import { $alerts, $authenticated, $router, $systems, navigate, pb } from './lib/stores.ts' +import { + $alerts, + $authenticated, + $updatedSystem, + $router, + $systems, + navigate, + pb, +} from './lib/stores.ts' import { ModeToggle } from './components/mode-toggle.tsx' import { cn, @@ -53,6 +61,7 @@ const App = () => { // subscribe to real time updates for systems / alerts pb.collection('systems').subscribe('*', (e) => { updateRecordList(e, $systems) + $updatedSystem.set(e.record) }) pb.collection('alerts').subscribe('*', (e) => { updateRecordList(e, $alerts) diff --git a/types.go b/types.go index 8c599b5..0574ae5 100644 --- a/types.go +++ b/types.go @@ -18,14 +18,14 @@ type SystemData struct { } type SystemInfo struct { - Cores int `json:"c"` - Threads int `json:"t"` - CpuModel string `json:"m"` - Os string `json:"o"` - Uptime uint64 `json:"u"` - Cpu float64 `json:"cpu"` - MemPct float64 `json:"mp"` - DiskPct float64 `json:"dp"` + Cores int `json:"c"` + Threads int `json:"t"` + CpuModel string `json:"m"` + // Os string `json:"o"` + Uptime uint64 `json:"u"` + Cpu float64 `json:"cpu"` + MemPct float64 `json:"mp"` + DiskPct float64 `json:"dp"` } type SystemStats struct {