{ "$schema": "homarr-widget-package-v3", "manifest": { "id": "homarr.reference.beszel", "version": "1.0.0", "sdkVersion": "1", "name": "Custom Beszel systems", "description": "Two attributable Beszel sources, native persisted table layouts, eight history panels and bounded live metrics with partial-failure recovery.", "entrypoints": { "tile": "systems.tsx", "advanced": "systems.tsx", "server": "server.ts" }, "handlers": { "systems": { "kind": "query", "permission": "view" }, "stats": { "kind": "query", "permission": "view" }, "live": { "kind": "subscription", "permission": "view", "timeoutMs": 300000 } } }, "files": { "server.ts": "import type { AttributedSystem, NativeResult, SystemRow, StatsResult, StatsInput, LiveStatsEvent } from \"./beszel-types\";\nimport { callWidgetIntegration, defineWidgetServer, getWidgetServerContext, subscribeWidgetIntegration } from \"@homarr/widget-sdk/server\";\nconst declaredConnections = new Set([\"beszel\", \"secondary\"]);\nconst getConnection = (input: {connection?:string} | undefined) => {\n const connection = input?.connection ?? \"beszel\";\n if (!declaredConnections.has(connection)) throw new Error(\"Unknown Beszel connection\");\n return connection;\n};\nexport default defineWidgetServer({\n systems: async (_input, context) => {\n const {bindingNames} = await getWidgetServerContext(context);\n const names = [...declaredConnections].filter((name) => bindingNames.includes(name));\n if (!names.length) throw new Error(\"Bind a Beszel integration to this widget first\");\n const results = await Promise.allSettled(names.map((connection) => callWidgetIntegration>(context, {connection, capability:\"beszel.systems\"})));\n const data: AttributedSystem[] = [];\n const sources = results.map((result, index) => {\n const connection = names[index]!;\n if (result.status === \"rejected\") return {connection,ok:false,error:result.reason instanceof Error ? result.reason.message : String(result.reason)};\n data.push(...result.value.data.map((system) => ({...system,connection,rowKey:`${connection}:${system.id}`})));\n return {connection,ok:true,timestamp:result.value.timestamp};\n });\n return {data,sources};\n },\n stats: (input: StatsInput, context) => callWidgetIntegration>(context, {connection:getConnection(input),capability:\"beszel.stats\",input}),\n live: (input: StatsInput, context) => subscribeWidgetIntegration(context, {connection:getConnection(input),capability:\"beszel.live\",input}),\n});", "beszel-types.ts": "// Public result fields used from the Homarr Beszel capability adapter.\nexport interface SystemRow {\n id:string; name:string; status:\"up\"|\"down\"|\"paused\"|\"pending\";\n cpu:number; memory:number; disk:number; gpu:number; netBytes:number; uptime:number;\n hostname:string; cpuModel:string; cores:number; osName:string; agentVersion:string;\n}\nexport interface AttributedSystem extends SystemRow {connection:string;rowKey:string}\nexport interface NativeResult {data:T;timestamp?:string}\nexport interface SystemStats {cpu:number;mu:number;mb:number;du:number;dio?:[number,number];dr?:number;dw?:number;b?:[number,number];ns?:number;nr?:number}\nexport interface ContainerStats {n:string;c:number;m:number;b?:[number,number];ns?:number;nr?:number}\nexport interface StatsRecord {id:string;created:string;stats:T}\nexport type SystemRecord = StatsRecord;\nexport type ContainerRecord = StatsRecord;\nexport interface StatsResult {systemStats:SystemRecord[];containerStats:ContainerRecord[]}\nexport type LiveStatsEvent = {type:\"system_stats\";record:SystemRecord}|{type:\"container_stats\";record:ContainerRecord};\nexport interface StatsInput {connection?:string;systemId:string;timePeriod?:string;includeDocker?:boolean}\n\nexport interface SystemsResult {data:AttributedSystem[];sources:{connection:string;ok:boolean;error?:string;timestamp?:string}[]}\n", "systems.tsx": "import type { AttributedSystem, SystemStats, ContainerStats, LiveStatsEvent, SystemsResult, NativeResult, StatsResult } from \"./beszel-types\";\nimport type { DataTableColumn, DataTableSortStatus } from \"@homarr/widget-sdk/ui\";\ntype Layout = {columnOrder:string;columnWidths:string};\ntype Selection = {connection:string;id:string;name:string};\ntype ChartData = Array<{time:string} & Record>;\nimport { useCallback, useMemo } from \"react\";\nimport { Alert, Badge, Button, Group, Loader, ScrollArea, SegmentedControl, SimpleGrid, Stack, Text, TextInput } from \"@mantine/core\";\nimport { LineChart } from \"@mantine/charts\";\nimport { useWidgetHost, useWidgetQuery, useWidgetServices, useWidgetState, useWidgetSubscription, useLocalWidgetState } from \"@homarr/widget-sdk\";\nimport { HomarrDataTable, usePersistedTableLayout } from \"@homarr/widget-sdk/ui\";\n\nconst columnAccessors = [\"name\", \"connection\", \"hostname\", \"cpu\", \"memory\", \"disk\", \"gpu\", \"netBytes\", \"uptime\", \"cpuModel\", \"cores\", \"osName\", \"agentVersion\"];\nconst colors = [\"teal.6\", \"blue.6\", \"grape.6\", \"orange.6\", \"pink.6\", \"cyan.6\"];\nconst mib = 1024 ** 2;\nconst gib = 1024 ** 3;\nconst rate = (bytes: number | undefined, legacy: number | undefined) => bytes ?? (legacy ?? 0) * mib;\nconst order = (records: readonly T[]) => [...records].sort((left, right) => Date.parse(left.created) - Date.parse(right.created));\n\nfunction Chart({ title, data, names, unit, locale }: {title:string;data:ChartData;names:string[];unit:string;locale:string}) {\n const formatter = (value:number) => {\n if (unit === \"%\") return new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value) + \"%\";\n return new Intl.NumberFormat(locale, { notation: \"compact\", maximumFractionDigits: 1 }).format(value) + \" \" + unit;\n };\n return \n {title}\n {data.length === 0 ? Waiting for samples :\n ({ name, color: colors[index % colors.length] }))} curveType=\"linear\" valueFormatter={formatter} withDots={false} />}\n ;\n}\n\nexport default function Systems() {\n const host = useWidgetHost();\n const services = useWidgetServices();\n const systems = useWidgetQuery(\"systems\", {}, { refetchInterval: 10000 });\n const [selected, setSelected] = useWidgetState(\"system\", null);\n const [period, setPeriod] = useWidgetState(\"period\", \"1h\");\n const [search, setSearch] = useWidgetState(\"search\", \"\");\n const [sort, setSort] = useWidgetState>(\"sort\", { columnAccessor: \"name\", direction: \"asc\" });\n const advanced = host.displayMode === \"advanced\";\n const live = useWidgetSubscription(\"live\", { connection: selected?.connection ?? \"beszel\", systemId: selected?.id ?? \"\" }, { enabled: advanced && selected !== null && period === \"live\", maximumEvents: 120 });\n const stats = useWidgetQuery>(\"stats\", { connection: selected?.connection ?? \"beszel\", systemId: selected?.id ?? \"\", timePeriod: period === \"live\" ? \"1h\" : period, includeDocker: true }, { enabled: advanced && selected !== null && period !== \"live\", refetchInterval: 5000 });\n const records = systems.data?.data ?? [];\n const visible = records.filter((system) => `${system.name} ${system.connection} ${system.hostname}`.toLocaleLowerCase().includes(search.toLocaleLowerCase())).toSorted((a, b) => {\n const direction = sort.direction === \"asc\" ? 1 : -1;\n const left = a[sort.columnAccessor as keyof AttributedSystem];\n const right = b[sort.columnAccessor as keyof AttributedSystem];\n if (typeof left === \"number\" && typeof right === \"number\") return (left - right) * direction;\n return String(left ?? \"\").localeCompare(String(right ?? \"\")) * direction;\n });\n const inspect = useCallback((system: AttributedSystem) => {\n setSelected({connection: system.connection, id: system.id, name: system.name});\n if (!advanced) services.openAdvanced();\n }, [advanced, services, setSelected]);\n const [layout, setLayout] = useLocalWidgetState({\n key: host.isPreview ? undefined : `custom-widget:beszel-layout:${host.itemId}:${host.userId ?? \"guest\"}`,\n version: 1, defaultValue: {columnOrder: \"\", columnWidths: \"\"},\n validate: (value): value is Layout => Boolean(value && typeof value === \"object\" && \"columnOrder\" in value && \"columnWidths\" in value && typeof value.columnOrder === \"string\" && typeof value.columnWidths === \"string\"),\n });\n const persistLayout = useCallback((patch: Partial) => setLayout((current) => ({...current, ...patch})), [setLayout]);\n const columns = useMemo[]>(() => [\n {accessor:\"name\", title:\"System\", width:180, sortable:true, render: (system: AttributedSystem) => {system.status}},\n {accessor:\"connection\", title:\"Source\", width:100, sortable:true, hidden:!advanced},\n {accessor:\"hostname\", title:\"Hostname\", width:140, sortable:true, hidden:!advanced},\n ...([\"cpu\",\"memory\",\"disk\",\"gpu\"] as const).map((key) => ({accessor:key, title:key.toUpperCase(), width:100, sortable:true, hidden:!advanced && (key === \"gpu\" || (key === \"disk\" && host.visibleWidth < 420)), render:(system: AttributedSystem) => {system[key].toFixed(1)}%})),\n {accessor:\"netBytes\", title:\"Network\", width:110, sortable:true, hidden:!advanced, render:(system: AttributedSystem) => {(system.netBytes / mib).toFixed(2)} MiB/s},\n {accessor:\"uptime\", title:\"Uptime\", width:100, sortable:true, hidden:!advanced, render:(system: AttributedSystem) => {Math.floor(system.uptime / 3600)} h},\n {accessor:\"cpuModel\", title:\"CPU model\", width:180, hidden:!advanced},\n {accessor:\"cores\", title:\"Cores\", width:70, sortable:true, hidden:!advanced},\n {accessor:\"osName\", title:\"Operating system\", width:140, sortable:true, hidden:!advanced},\n {accessor:\"agentVersion\", title:\"Agent\", width:90, hidden:!advanced},\n ], [advanced, host.visibleWidth, inspect]);\n const {effectiveColumns, storeKey} = usePersistedTableLayout({\n columns, columnAccessors, columnOrder:layout.columnOrder, columnWidths:layout.columnWidths,\n itemId:host.itemId ?? host.previewId, storeKeyPrefix:`custom-beszel-${host.userId ?? \"guest\"}`, onLayoutChange:persistLayout,\n });\n const panels = useMemo(() => {\n const systemRecords = order(period === \"live\" ? live.events.filter((event) => event.type === \"system_stats\").map((event) => event.record) : stats.data?.data?.systemStats ?? []);\n const containerRecords = order(period === \"live\" ? live.events.filter((event) => event.type === \"container_stats\").map((event) => event.record) : stats.data?.data?.containerStats ?? []);\n const formatTime = (created:string) => new Date(created).toLocaleTimeString(host.locale, { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" });\n const mapped = (fn:(stats:SystemStats)=>Record) => systemRecords.map((record) => ({ time: formatTime(record.created), ...fn(record.stats) }));\n const containerNames = [...new Set(containerRecords.flatMap((record) => record.stats.map((container) => container.n)))];\n const containers = (fn:(stats:ContainerStats)=>number) => containerRecords.map((record) => ({ time: formatTime(record.created), ...Object.fromEntries(record.stats.map((container) => [container.n, fn(container)])) }));\n return [\n { title: \"CPU\", data: mapped((s) => ({ CPU: s.cpu })), names: [\"CPU\"], unit: \"%\" },\n { title: \"Memory\", data: mapped((s) => ({ Used: s.mu * gib, Cache: s.mb * gib })), names: [\"Used\", \"Cache\"], unit: \"B\" },\n { title: \"Disk\", data: mapped((s) => ({ Used: s.du * gib })), names: [\"Used\"], unit: \"B\" },\n { title: \"Disk I/O\", data: mapped((s) => ({ Read: rate(s.dio?.[0], s.dr), Write: rate(s.dio?.[1], s.dw) })), names: [\"Read\", \"Write\"], unit: \"B/s\" },\n { title: \"Network\", data: mapped((s) => ({ Sent: rate(s.b?.[0], s.ns), Received: rate(s.b?.[1], s.nr) })), names: [\"Sent\", \"Received\"], unit: \"B/s\" },\n { title: \"Container CPU\", data: containers((c) => c.c), names: containerNames, unit: \"%\" },\n { title: \"Container memory\", data: containers((c) => c.m * mib), names: containerNames, unit: \"B\" },\n { title: \"Container network\", data: containers((c) => rate(c.b?.[0], c.ns) + rate(c.b?.[1], c.nr)), names: containerNames, unit: \"B/s\" },\n ];\n }, [host.locale, live.events, period, stats.data]);\n return \n Beszel systems{systems.isFetching && }\n {systems.error && {systems.error.message}}\n {advanced && setSearch(event.currentTarget.value)} />}\n {(systems.data?.sources ?? []).filter((source) => !source.ok).map((source) => {source.error})}\n inspect(record)} />\n {!systems.isPending && visible.length === 0 && No systems match this view.}\n {!selected && advanced && Select a system to inspect its history.}\n {selected && advanced && \n {selected.name} \u00b7 {selected.connection}\n \n {live.error && period === \"live\" && {live.error.message}}\n {stats.error && period !== \"live\" && {stats.error.message}}\n 700 ? 2 : 1}>{panels.map((panel) => )}\n }\n ;\n}\n" }, "dependencies": {}, "connections": { "beszel": { "label": "Beszel integration", "kind": "integration" }, "secondary": { "kind": "integration", "label": "Secondary Beszel integration", "optional": true } }, "options": {} }