update 1.2.2.2 beta

This commit is contained in:
MacRimi
2026-06-24 23:40:22 +02:00
parent 87a29f324b
commit 202068124b
9 changed files with 148 additions and 112 deletions

View File

@@ -7,6 +7,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { Cpu, HardDrive, Thermometer, Zap, Loader2, CpuIcon, Cpu as Gpu, Network, MemoryStick, PowerIcon, FanIcon, Battery, Usb, BrainCircuit, AlertCircle } from "lucide-react"
import { Download } from "lucide-react"
import { Button } from "@/components/ui/button"
import { getDiskType } from "../lib/disk-type"
import useSWR from "swr"
import { useState, useEffect } from "react"
import {
@@ -2460,25 +2461,10 @@ return (
)
.map((device, index) => {
const getDiskTypeBadge = (diskName: string, rotationRate: number | string | undefined) => {
let diskType = "HDD"
// Check if it's NVMe
if (diskName.startsWith("nvme")) {
diskType = "NVMe"
}
// Check rotation rate for SSD vs HDD
else if (rotationRate !== undefined && rotationRate !== null) {
// Handle both number and string formats
const rateNum = typeof rotationRate === "string" ? Number.parseInt(rotationRate) : rotationRate
if (rateNum === 0 || isNaN(rateNum)) {
diskType = "SSD"
}
}
// If rotation_rate is "Solid State Device" string
else if (typeof rotationRate === "string" && rotationRate.includes("Solid State")) {
diskType = "SSD"
}
// Classifier lives in lib/disk-type.ts — same rules
// the Storage page uses, so a drive can't show up as
// HDD here while showing as SSD there.
const diskType = getDiskType(diskName, rotationRate)
const badgeStyles: Record<string, { className: string; label: string }> = {
NVMe: {
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
@@ -2600,38 +2586,22 @@ return (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Type</span>
{(() => {
const getDiskTypeBadge = (diskName: string, rotationRate: number | string | undefined) => {
let diskType = "HDD"
if (diskName.startsWith("nvme")) {
diskType = "NVMe"
} else if (rotationRate !== undefined && rotationRate !== null) {
const rateNum = typeof rotationRate === "string" ? Number.parseInt(rotationRate) : rotationRate
if (rateNum === 0 || isNaN(rateNum)) {
diskType = "SSD"
}
} else if (typeof rotationRate === "string" && rotationRate.includes("Solid State")) {
diskType = "SSD"
}
const badgeStyles: Record<string, { className: string; label: string }> = {
NVMe: {
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
label: "NVMe SSD",
},
SSD: {
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
label: "SSD",
},
HDD: {
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
label: "HDD",
},
}
return badgeStyles[diskType]
const diskType = getDiskType(selectedDisk.name, selectedDisk.rotation_rate)
const badgeStyles: Record<string, { className: string; label: string }> = {
NVMe: {
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
label: "NVMe SSD",
},
SSD: {
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
label: "SSD",
},
HDD: {
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
label: "HDD",
},
}
const diskBadge = getDiskTypeBadge(selectedDisk.name, selectedDisk.rotation_rate)
const diskBadge = badgeStyles[diskType]
return <Badge className={diskBadge.className}>{diskBadge.label}</Badge>
})()}
</div>

View File

@@ -48,7 +48,7 @@ import {
import { ScriptTerminalModal } from "./script-terminal-modal"
import { fetchApi, getApiUrl } from "../lib/api-config"
import { fetchTerminalTicket } from "../lib/terminal-ws"
import { formatStorage } from "../lib/utils"
import { formatStorage, formatBytes } from "../lib/utils"
// ── Shape contracts with the backend (flask_server.py: api_host_backups_*) ──
@@ -316,18 +316,6 @@ const methodBadgeCls = (m: string | undefined): string => {
}
}
// formatStorage() in lib/utils.ts expects GIGABYTES as input — it's
// the storage layer's native unit. For raw log file sizes coming from
// os.path.getsize() we need a byte-aware formatter; passing N bytes to
// formatStorage() rendered "442 GB" for a 442-byte log. Tiny inline.
const formatBytes = (n: number): string => {
if (!Number.isFinite(n) || n < 0) return "—"
if (n < 1024) return `${n} B`
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`
}
const formatRunAt = (iso: string | null) => {
if (!iso) return null
try {
@@ -509,6 +497,26 @@ export function HostBackup() {
<Lock className="h-3.5 w-3.5" />
</Badge>
)}
{/* Surface the profile mode at row level so the
operator can tell at a glance whether the
job ships the default path set or a custom
list — without having to open the detail
modal. Same wording the modal uses. */}
<Badge
variant="outline"
className={`text-[10px] uppercase tracking-wide ${
j.profile_mode === "custom"
? "border-cyan-500/40 text-cyan-400 bg-cyan-500/5"
: "border-border text-muted-foreground bg-background/40"
}`}
title={
j.profile_mode === "custom"
? "Custom path list — only the paths the operator picked"
: "Default path list — ProxMenux's recommended host config set"
}
>
{j.profile_mode === "custom" ? "custom" : "default"}
</Badge>
{!j.enabled && !j.manual && (
<Badge variant="outline" className="text-[10px] text-amber-500 border-amber-500/40 bg-amber-500/5">
disabled
@@ -656,7 +664,7 @@ export function HostBackup() {
</CardHeader>
<CardContent>
<p className="text-[11px] text-muted-foreground mb-3">
All backups visible from this host local <code className="font-mono">.tar.zst</code> files (PVE default dump dir, configured local target, USB mountpoints, scheduled jobs' destinations) and PBS snapshots from every configured datastore. Click an entry to inspect, restore or download it — downloads of PBS snapshots are extracted on-demand only when you request them.
All backups visible from this host local <code className="font-mono">.tar.zst</code> files (PVE default dump dir, configured local target, USB mountpoints, scheduled jobs' destinations) and PBS backups from every configured datastore. Click an entry to inspect, restore or download it — downloads of PBS backups are extracted on-demand only when you request them.
</p>
{remoteArchivesResp?.errors && remoteArchivesResp.errors.length > 0 && (
<div className="text-[11px] text-amber-500 mb-3 px-3 py-2 rounded-md border border-amber-500/30 bg-amber-500/5 space-y-1">
@@ -677,7 +685,7 @@ export function HostBackup() {
</div>
) : unifiedArchives.length === 0 ? (
<div className="text-sm text-muted-foreground py-4">
No backups found yet. Use <span className="font-medium">Run manual backup</span> above, configure a scheduled job, or check that the configured PBS / Borg destinations have snapshots.
No backups found yet. Use <span className="font-medium">Run manual backup</span> above, configure a scheduled job, or check that the configured PBS / Borg destinations have backups.
</div>
) : (
<div className="space-y-2">
@@ -7205,14 +7213,15 @@ function RollbackPlanView({ plan }: { plan: RollbackPlan }) {
{children}
</code>
)
// Row uses flex-wrap + min-w-0 + gap so labels and pills flow to
// a new line on narrow screens instead of pushing the card wider.
// Each pill is a direct flex child of the row (no inner wrapper)
// so flex-wrap can break a long list of IDs mid-row on mobile.
// The previous nested `<div className="flex flex-wrap">` wouldn't
// wrap because its width was capped by the parent's flex layout,
// and the whole block would overflow the modal instead.
const Row = ({ label, items }: { label: string; items: (string | number)[] }) => (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1.5 min-w-0">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1.5">
<span className="text-muted-foreground shrink-0">{label}</span>
<div className="flex flex-wrap gap-1.5 min-w-0">
{items.map((id) => <Pill key={id}>{id}</Pill>)}
</div>
{items.map((id) => <Pill key={id}>{id}</Pill>)}
</div>
)

View File

@@ -8,8 +8,10 @@ import { Progress } from "@/components/ui/progress"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { fetchApi } from "../lib/api-config"
import { formatStorage as sharedFormatStorage } from "../lib/utils"
import { DiskTemperatureDetailModal } from "./disk-temperature-detail-modal"
import { DiskTemperatureCard } from "./disk-temperature-card"
import { getDiskType as resolveDiskType } from "../lib/disk-type"
import {
useDiskTempThresholds,
loadDiskTempThresholds,
@@ -146,17 +148,9 @@ interface RemoteMountsData {
error?: string
}
const formatStorage = (sizeInGB: number): string => {
if (sizeInGB < 1) {
// Less than 1 GB, show in MB
return `${(sizeInGB * 1024).toFixed(1)} MB`
} else if (sizeInGB > 999) {
return `${(sizeInGB / 1024).toFixed(2)} TB`
} else {
// Between 1 and 999 GB, show in GB
return `${sizeInGB.toFixed(2)} GB`
}
}
// Re-exported under the local name so the rest of this large file
// stays untouched. Single source of truth lives in lib/utils.ts.
const formatStorage = sharedFormatStorage
// Translate the short ATA/SCSI error codes that appear inside `{ ... }`
// in a raw kernel observation (e.g. `error: { IDNF }`) into a one-line
@@ -624,21 +618,12 @@ export function StorageOverview() {
return `${rpm.toLocaleString()} RPM`
}
const getDiskType = (diskName: string, rotationRate: number | undefined): string => {
if (diskName.startsWith("nvme")) {
return "NVMe"
}
// rotation_rate = -1 means HDD but RPM is unknown (detected via kernel rotational flag)
// rotation_rate = 0 or undefined means SSD
// rotation_rate > 0 means HDD with known RPM
if (rotationRate === -1) {
return "HDD"
}
if (!rotationRate || rotationRate === 0) {
return "SSD"
}
return "HDD"
}
// Thin wrapper over the shared classifier so the rest of the file
// doesn't need to be touched. The actual rules live in
// lib/disk-type.ts (single source of truth across Storage page,
// Hardware page, and any future consumer).
const getDiskType = (diskName: string, rotationRate: number | undefined): string =>
resolveDiskType(diskName, rotationRate)
const getDiskTypeBadge = (diskName: string, rotationRate: number | undefined) => {
const diskType = getDiskType(diskName, rotationRate)