mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 21:46:18 +01:00
Compare commits
3 Commits
custom-col
...
nvidia-age
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c7f7bd8a | ||
|
|
8af3a0eb5b | ||
|
|
5f7950b474 |
17
.github/workflows/docker-images.yml
vendored
17
.github/workflows/docker-images.yml
vendored
@@ -21,6 +21,14 @@ jobs:
|
|||||||
- image: henrygd/beszel-agent
|
- image: henrygd/beszel-agent
|
||||||
context: ./beszel
|
context: ./beszel
|
||||||
dockerfile: ./beszel/dockerfile_Agent
|
dockerfile: ./beszel/dockerfile_Agent
|
||||||
|
target: default-agent
|
||||||
|
registry: docker.io
|
||||||
|
username_secret: DOCKERHUB_USERNAME
|
||||||
|
password_secret: DOCKERHUB_TOKEN
|
||||||
|
- image: henrygd/beszel-agent-nvidia
|
||||||
|
context: ./beszel
|
||||||
|
dockerfile: ./beszel/dockerfile_Agent
|
||||||
|
target: nvidia-agent
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username_secret: DOCKERHUB_USERNAME
|
username_secret: DOCKERHUB_USERNAME
|
||||||
password_secret: DOCKERHUB_TOKEN
|
password_secret: DOCKERHUB_TOKEN
|
||||||
@@ -33,6 +41,14 @@ jobs:
|
|||||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||||
context: ./beszel
|
context: ./beszel
|
||||||
dockerfile: ./beszel/dockerfile_Agent
|
dockerfile: ./beszel/dockerfile_Agent
|
||||||
|
target: default-agent
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password_secret: GITHUB_TOKEN
|
||||||
|
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||||
|
context: ./beszel
|
||||||
|
dockerfile: ./beszel/dockerfile_Agent
|
||||||
|
target: nvidia-agent
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password_secret: GITHUB_TOKEN
|
password_secret: GITHUB_TOKEN
|
||||||
@@ -91,3 +107,4 @@ jobs:
|
|||||||
push: ${{ github.ref_type == 'tag' }}
|
push: ${{ github.ref_type == 'tag' }}
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|||||||
@@ -14,13 +14,21 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
|
|||||||
|
|
||||||
RUN rm -rf /tmp/*
|
RUN rm -rf /tmp/*
|
||||||
|
|
||||||
# ? -------------------------
|
# --------------------------
|
||||||
FROM scratch
|
# Final image: default scratch-based agent
|
||||||
|
# --------------------------
|
||||||
|
FROM scratch AS default-agent
|
||||||
COPY --from=builder /agent /agent
|
COPY --from=builder /agent /agent
|
||||||
|
|
||||||
# this is so we don't need to create the
|
# this is so we don't need to create the /tmp directory in the scratch container
|
||||||
# /tmp directory in the scratch container
|
|
||||||
COPY --from=builder /tmp /tmp
|
COPY --from=builder /tmp /tmp
|
||||||
|
|
||||||
ENTRYPOINT ["/agent"]
|
ENTRYPOINT ["/agent"]
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Final image: GPU-enabled agent with nvidia-smi
|
||||||
|
# --------------------------
|
||||||
|
FROM nvidia/cuda:12.9.0-base-ubuntu22.04 AS nvidia-agent
|
||||||
|
COPY --from=builder /agent /agent
|
||||||
|
COPY --from=builder /tmp /tmp
|
||||||
|
ENTRYPOINT ["/agent"]
|
||||||
|
|||||||
@@ -14,18 +14,6 @@ type UserManager struct {
|
|||||||
app core.App
|
app core.App
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSettings struct {
|
|
||||||
ChartTime string `json:"chartTime"`
|
|
||||||
NotificationEmails []string `json:"emails"`
|
|
||||||
NotificationWebhooks []string `json:"webhooks"`
|
|
||||||
// UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
|
|
||||||
// UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits
|
|
||||||
// UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits
|
|
||||||
|
|
||||||
// New field for alert history retention (e.g., "1m", "3m", "6m", "1y")
|
|
||||||
AlertHistoryRetention string `json:"alertHistoryRetention,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserManager(app core.App) *UserManager {
|
func NewUserManager(app core.App) *UserManager {
|
||||||
return &UserManager{
|
return &UserManager{
|
||||||
app: app,
|
app: app,
|
||||||
@@ -44,7 +32,10 @@ func (um *UserManager) InitializeUserRole(e *core.RecordEvent) error {
|
|||||||
func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
|
func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
|
||||||
record := e.Record
|
record := e.Record
|
||||||
// intialize settings with defaults (zero values can be ignored)
|
// intialize settings with defaults (zero values can be ignored)
|
||||||
settings := UserSettings{
|
settings := struct {
|
||||||
|
ChartTime string `json:"chartTime"`
|
||||||
|
Emails []string `json:"emails"`
|
||||||
|
}{
|
||||||
ChartTime: "1h",
|
ChartTime: "1h",
|
||||||
}
|
}
|
||||||
record.UnmarshalJSONField("settings", &settings)
|
record.UnmarshalJSONField("settings", &settings)
|
||||||
@@ -59,10 +50,7 @@ func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
|
|||||||
log.Println("failed to get user email", "err", err)
|
log.Println("failed to get user email", "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
settings.NotificationEmails = []string{user.Email}
|
settings.Emails = []string{user.Email}
|
||||||
if len(settings.NotificationWebhooks) == 0 {
|
|
||||||
settings.NotificationWebhooks = []string{""}
|
|
||||||
}
|
|
||||||
record.Set("settings", settings)
|
record.Set("settings", settings)
|
||||||
return e.Next()
|
return e.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,29 +12,17 @@ import languages from "@/lib/languages"
|
|||||||
import { dynamicActivate } from "@/lib/i18n"
|
import { dynamicActivate } from "@/lib/i18n"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
// import { setLang } from "@/lib/i18n"
|
|
||||||
import { Unit } from "@/lib/enums"
|
import { Unit } from "@/lib/enums"
|
||||||
|
|
||||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const { i18n } = useLingui()
|
const { i18n } = useLingui()
|
||||||
|
|
||||||
// Remove all per-metric threshold state and UI
|
|
||||||
// Only keep general yellow/red threshold state and UI
|
|
||||||
const [yellow, setYellow] = useState(userSettings.meterThresholds?.yellow ?? 65)
|
|
||||||
const [red, setRed] = useState(userSettings.meterThresholds?.red ?? 90)
|
|
||||||
|
|
||||||
function handleResetThresholds() {
|
|
||||||
setYellow(65)
|
|
||||||
setRed(90)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const formData = new FormData(e.target as HTMLFormElement)
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
const data = Object.fromEntries(formData) as Partial<UserSettings>
|
const data = Object.fromEntries(formData) as Partial<UserSettings>
|
||||||
data.meterThresholds = { yellow, red }
|
|
||||||
await saveSettings(data)
|
await saveSettings(data)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -114,45 +102,6 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="mb-1 text-lg font-medium">
|
|
||||||
<Trans>Dashboard meter thresholds</Trans>
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
||||||
<Trans>Choose when the dashboard meters changes colors, based on percentage values.</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 items-end">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="yellow-threshold"><Trans>Warning threshold (%)</Trans></Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id="yellow-threshold"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={yellow}
|
|
||||||
onChange={e => setYellow(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="red-threshold"><Trans>Danger threshold (%)</Trans></Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id="red-threshold"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={red}
|
|
||||||
onChange={e => setRed(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button type="button" variant="outline" onClick={handleResetThresholds} disabled={isLoading} className="mt-4">
|
|
||||||
<Trans>Reset to default</Trans>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
{/* Unit preferences section fixed and wrapped in a div */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="mb-1 text-lg font-medium">
|
<h3 className="mb-1 text-lg font-medium">
|
||||||
@@ -232,6 +181,47 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-1 text-lg font-medium">
|
||||||
|
<Trans>Warning thresholds</Trans>
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
<Trans>Set percentage thresholds for meter colors.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 items-end">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="colorWarn">
|
||||||
|
<Trans>Warning (%)</Trans>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="colorWarn"
|
||||||
|
name="colorWarn"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
className="min-w-24"
|
||||||
|
defaultValue={userSettings.colorWarn ?? 65}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="colorCrit">
|
||||||
|
<Trans>Critical (%)</Trans>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="colorCrit"
|
||||||
|
name="colorCrit"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
className="min-w-24"
|
||||||
|
defaultValue={userSettings.colorCrit ?? 90}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
||||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||||
<Trans>Save Settings</Trans>
|
<Trans>Save Settings</Trans>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getPagePath, redirectPage } from "@nanostores/router"
|
|||||||
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
|
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
|
||||||
import { $userSettings, pb } from "@/lib/stores.ts"
|
import { $userSettings, pb } from "@/lib/stores.ts"
|
||||||
import { toast } from "@/components/ui/use-toast.ts"
|
import { toast } from "@/components/ui/use-toast.ts"
|
||||||
import { UserSettings } from "@/types.js"
|
import { UserSettings } from "@/types"
|
||||||
import General from "./general.tsx"
|
import General from "./general.tsx"
|
||||||
import Notifications from "./notifications.tsx"
|
import Notifications from "./notifications.tsx"
|
||||||
import ConfigYaml from "./config-yaml.tsx"
|
import ConfigYaml from "./config-yaml.tsx"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
decimalString,
|
decimalString,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
formatTemperature,
|
formatTemperature,
|
||||||
|
getMeterState,
|
||||||
isReadOnlyUser,
|
isReadOnlyUser,
|
||||||
parseSemVer,
|
parseSemVer,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
@@ -53,6 +54,7 @@ import {
|
|||||||
} from "../ui/alert-dialog"
|
} from "../ui/alert-dialog"
|
||||||
import { buttonVariants } from "../ui/button"
|
import { buttonVariants } from "../ui/button"
|
||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
|
import { MeterState } from "@/lib/enums"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param viewMode - "table" or "grid"
|
* @param viewMode - "table" or "grid"
|
||||||
@@ -98,7 +100,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
accessorFn: ({ info }) => info.cpu,
|
accessorFn: ({ info }) => info.cpu,
|
||||||
id: "cpu",
|
id: "cpu",
|
||||||
name: () => t`CPU`,
|
name: () => t`CPU`,
|
||||||
cell: CellFormatter,
|
cell: TableCellWithMeter,
|
||||||
Icon: CpuIcon,
|
Icon: CpuIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
@@ -107,7 +109,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
accessorFn: ({ info }) => info.mp,
|
accessorFn: ({ info }) => info.mp,
|
||||||
id: "memory",
|
id: "memory",
|
||||||
name: () => t`Memory`,
|
name: () => t`Memory`,
|
||||||
cell: CellFormatter,
|
cell: TableCellWithMeter,
|
||||||
Icon: MemoryStickIcon,
|
Icon: MemoryStickIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
@@ -115,7 +117,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
accessorFn: ({ info }) => info.dp,
|
accessorFn: ({ info }) => info.dp,
|
||||||
id: "disk",
|
id: "disk",
|
||||||
name: () => t`Disk`,
|
name: () => t`Disk`,
|
||||||
cell: CellFormatter,
|
cell: TableCellWithMeter,
|
||||||
Icon: HardDriveIcon,
|
Icon: HardDriveIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
@@ -123,7 +125,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
accessorFn: ({ info }) => info.g,
|
accessorFn: ({ info }) => info.g,
|
||||||
id: "gpu",
|
id: "gpu",
|
||||||
name: () => "GPU",
|
name: () => "GPU",
|
||||||
cell: CellFormatter,
|
cell: TableCellWithMeter,
|
||||||
Icon: GpuIcon,
|
Icon: GpuIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
@@ -157,17 +159,18 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDotColor() {
|
const normalizedLoad = max / (sysInfo.t ?? 1)
|
||||||
const normalized = max / (sysInfo.t ?? 1)
|
const threshold = getMeterState(normalizedLoad * 100)
|
||||||
if (status !== "up") return "bg-primary/30"
|
|
||||||
if (normalized < 0.7) return "bg-green-500"
|
|
||||||
if (normalized < 1) return "bg-yellow-500"
|
|
||||||
return "bg-red-600"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
|
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
|
||||||
<span className={cn("inline-block size-2 rounded-full me-0.5", getDotColor())} />
|
<span
|
||||||
|
className={cn("inline-block size-2 rounded-full me-0.5", {
|
||||||
|
"bg-green-500": threshold === MeterState.Good,
|
||||||
|
"bg-yellow-500": threshold === MeterState.Warn,
|
||||||
|
"bg-red-600": threshold === MeterState.Crit,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
{loadAverages?.map((la, i) => (
|
{loadAverages?.map((la, i) => (
|
||||||
<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>
|
<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>
|
||||||
))}
|
))}
|
||||||
@@ -279,8 +282,10 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|
||||||
|
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
|
||||||
const val = Number(info.getValue()) || 0
|
const val = Number(info.getValue()) || 0
|
||||||
|
const threshold = getMeterState(val)
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
||||||
<span className="min-w-8">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
|
<span className="min-w-8">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
|
||||||
@@ -289,8 +294,8 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 w-full h-full origin-left",
|
"absolute inset-0 w-full h-full origin-left",
|
||||||
(info.row.original.status !== "up" && "bg-primary/30") ||
|
(info.row.original.status !== "up" && "bg-primary/30") ||
|
||||||
(val < 65 && "bg-green-500") ||
|
(threshold === MeterState.Good && "bg-green-500") ||
|
||||||
(val < 90 && "bg-yellow-500") ||
|
(threshold === MeterState.Warn && "bg-yellow-500") ||
|
||||||
"bg-red-600"
|
"bg-red-600"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -51,49 +51,6 @@ import AlertButton from "../alerts/alert-button"
|
|||||||
|
|
||||||
type ViewMode = "table" | "grid"
|
type ViewMode = "table" | "grid"
|
||||||
|
|
||||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|
||||||
const val = (info.getValue() as number) || 0
|
|
||||||
const userSettings = useStore($userSettings)
|
|
||||||
const yellow = userSettings?.meterThresholds?.yellow ?? 65
|
|
||||||
const red = userSettings?.meterThresholds?.red ?? 90
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
|
||||||
<span className="min-w-8">{decimalString(val, 1)}%</span>
|
|
||||||
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 w-full h-full origin-left",
|
|
||||||
(info.row.original.status !== "up" && "bg-primary/30") ||
|
|
||||||
(val < yellow! && "bg-green-500") ||
|
|
||||||
(val < red! && "bg-yellow-500") ||
|
|
||||||
"bg-red-600"
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
transform: `scalex(${val / 100})`,
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
|
|
||||||
const { column } = context
|
|
||||||
// @ts-ignore
|
|
||||||
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-9 px-3 flex"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
{Icon && <Icon className="me-2 size-4" />}
|
|
||||||
{name()}
|
|
||||||
{hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SystemsTable() {
|
export default function SystemsTable() {
|
||||||
const data = useStore($systems)
|
const data = useStore($systems)
|
||||||
const { i18n, t } = useLingui()
|
const { i18n, t } = useLingui()
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ export enum Unit {
|
|||||||
Celsius,
|
Celsius,
|
||||||
Fahrenheit,
|
Fahrenheit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Meter state for color */
|
||||||
|
export enum MeterState {
|
||||||
|
Good,
|
||||||
|
Warn,
|
||||||
|
Crit,
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,17 +24,21 @@ export const $chartTime = atom("1h") as PreinitializedWritableAtom<ChartTimes>
|
|||||||
/** Whether to display average or max chart values */
|
/** Whether to display average or max chart values */
|
||||||
export const $maxValues = atom(false)
|
export const $maxValues = atom(false)
|
||||||
|
|
||||||
|
// export const UserSettingsSchema = v.object({
|
||||||
|
// chartTime: v.picklist(["1h", "12h", "24h", "1w", "30d"]),
|
||||||
|
// emails: v.optional(v.array(v.pipe(v.string(), v.email())), [pb?.authStore?.record?.email ?? ""]),
|
||||||
|
// webhooks: v.optional(v.array(v.string())),
|
||||||
|
// colorWarn: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100))),
|
||||||
|
// colorDanger: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100))),
|
||||||
|
// unitTemp: v.optional(v.enum(Unit)),
|
||||||
|
// unitNet: v.optional(v.enum(Unit)),
|
||||||
|
// unitDisk: v.optional(v.enum(Unit)),
|
||||||
|
// })
|
||||||
|
|
||||||
/** User settings */
|
/** User settings */
|
||||||
export const $userSettings = map<UserSettings>({
|
export const $userSettings = map<UserSettings>({
|
||||||
chartTime: "1h",
|
chartTime: "1h",
|
||||||
emails: [pb.authStore.record?.email || ""],
|
emails: [pb.authStore.record?.email || ""],
|
||||||
meterThresholds: {
|
|
||||||
yellow: 65,
|
|
||||||
red: 90,
|
|
||||||
},
|
|
||||||
// unitTemp: "celsius",
|
|
||||||
// unitNet: "mbps",
|
|
||||||
// unitDisk: "mbps",
|
|
||||||
})
|
})
|
||||||
// update local storage on change
|
// update local storage on change
|
||||||
$userSettings.subscribe((value) => {
|
$userSettings.subscribe((value) => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useEffect, useState } from "react"
|
|||||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||||
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
|
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||||
import { prependBasePath } from "@/components/router"
|
import { prependBasePath } from "@/components/router"
|
||||||
import { Unit } from "./enums"
|
import { MeterState, Unit } from "./enums"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
@@ -507,3 +507,9 @@ export const parseSemVer = (semVer = ""): SemVer => {
|
|||||||
const parts = semVer.split(".").map(Number)
|
const parts = semVer.split(".").map(Number)
|
||||||
return { major: parts?.[0] ?? 0, minor: parts?.[1] ?? 0, patch: parts?.[2] ?? 0 }
|
return { major: parts?.[0] ?? 0, minor: parts?.[1] ?? 0, patch: parts?.[2] ?? 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get meter state from 0-100 value. Used for color coding meters. */
|
||||||
|
export function getMeterState(value: number): MeterState {
|
||||||
|
const { colorWarn = 65, colorCrit = 90 } = $userSettings.get()
|
||||||
|
return value >= colorCrit ? MeterState.Crit : value >= colorWarn ? MeterState.Warn : MeterState.Good
|
||||||
|
}
|
||||||
|
|||||||
7
beszel/site/src/types.d.ts
vendored
7
beszel/site/src/types.d.ts
vendored
@@ -224,17 +224,14 @@ export interface ChartTimeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
// lang?: string
|
|
||||||
chartTime: ChartTimes
|
chartTime: ChartTimes
|
||||||
emails?: string[]
|
emails?: string[]
|
||||||
webhooks?: string[]
|
webhooks?: string[]
|
||||||
meterThresholds?: {
|
|
||||||
yellow?: number
|
|
||||||
red?: number
|
|
||||||
}
|
|
||||||
unitTemp?: Unit
|
unitTemp?: Unit
|
||||||
unitNet?: Unit
|
unitNet?: Unit
|
||||||
unitDisk?: Unit
|
unitDisk?: Unit
|
||||||
|
colorWarn?: number
|
||||||
|
colorCrit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartDataContainer = {
|
type ChartDataContainer = {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
beszel-agent:
|
beszel-agent:
|
||||||
image: 'henrygd/beszel-agent'
|
image: 'henrygd/beszel-agent' #Or henrygd/beszel-agent-nvidia
|
||||||
container_name: 'beszel-agent'
|
container_name: 'beszel-agent'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
# Only when using henrygd/beszel-agent-nvidia
|
||||||
|
# runtime: nvidia
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||||
@@ -11,3 +13,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
PORT: 45876
|
||||||
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
||||||
|
# Only when using henrygd/beszel-agent-nvidia
|
||||||
|
# NVIDIA_VISIBLE_DEVICES: all
|
||||||
|
# NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ services:
|
|||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
|
|
||||||
beszel-agent:
|
beszel-agent:
|
||||||
image: 'henrygd/beszel-agent'
|
image: 'henrygd/beszel-agent' #Add -nvidia for nvidia gpus
|
||||||
container_name: 'beszel-agent'
|
container_name: 'beszel-agent'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
# runtime: nvidia # when using beszel-agent-nvidia
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
PORT: 45876
|
||||||
KEY: '...'
|
KEY: '...'
|
||||||
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats
|
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats
|
||||||
|
# NVIDIA_VISIBLE_DEVICES: all # when using beszel-agent-nvidia
|
||||||
|
# NVIDIA_DRIVER_CAPABILITIES: utility # when using beszel-agent-nvidia
|
||||||
|
|||||||
Reference in New Issue
Block a user