Beta cycle bundle over 1.2.4.1

- **VM/LXC modal** — PVE tags (dots on list cards, editable pills in modal with click-to-edit) using NVIDIA-style hash colour and SAPC contrast; Status tab redesign (single card, always-visible subsections, Edit button, autostart toggle, blue subsection icons); Backups and Firewall tabs now fill the full modal height with sticky headers/notes; stopped VMs no longer shift the metrics grid; mount-point card brightness unified across breakpoints.
- **Disks modal** — Overview / SMART / History / Schedule tabs adopt the VM/LXC modal size and the mobile icon-only tab pattern; SMART attributes table drops the 15-row cap and gains a sticky "View full SMART report" footer; Print/Save-as-PDF collapses to two icons in the report; loose i18n and layout follow-ups.
- **NVIDIA driver installer (#298)** — version picker cross-checks kernel + NVIDIA's Production/New Feature/Legacy branch classification (scraped from `nvidia.com/en-us/drivers/unix/`) + the PCI Device IDs of every host GPU, with a release-count heuristic to keep superseded production branches selectable while dropping Vulkan-beta ones; Recommended follows same-branch head when a driver is installed, Production Branch head on a fresh install; Hardware card now shows installed alongside available driver version.
- **Custom notifications (#297)** — `event_type: "custom"` accepts `title`/`message` at the root or nested under `data`; defensive strip of stray `[TITLE]`/`[BODY]` markers echoed by the AI enhancer.
- **App tab** — new "Exclude from the LXC updates counter" toggle; the CT's aggregate updates badge now sums OS packages plus registered apps (respecting the flag); Docs page updated; App suggestion no longer treats bare OS helper slugs (alpine/ubuntu/debian…) as installable apps.
- **i18n and copy** — Monitor UI available in EN / ES / DE / FR / IT / PT / SV / SK (thanks @vaso73) surfaced as the first entry in the What's New modal with a link to the contributor's profile; ES cleanup pass (`Historial`, `Velocidad de rotación`, `Consumo actual`, `Ejecutar`, `Eliminar`, `Activar`, `Ver contenido`, `Repuesto disp.`, `Registrar`, `Ocultar`, `Descartar`); redundant "Tip: search any Linux/Proxmox command" line removed from the terminal command search across all locales.
This commit is contained in:
MacRimi
2026-08-15 17:33:05 +02:00
parent 34b8c47415
commit 0beeb7a68b
26 changed files with 1554 additions and 278 deletions
+99 -36
View File
@@ -250,6 +250,23 @@ export function StorageOverview() {
const [diskObservations, setDiskObservations] = useState<DiskObservation[]>([])
const [loadingObservations, setLoadingObservations] = useState(false)
const [activeModalTab, setActiveModalTab] = useState<"overview" | "smart" | "history" | "schedule">("overview")
// Detect PWA / standalone display so the disk modal gets the same
// adaptive height the VM/LXC modal uses (95/90 vh in standalone,
// 85 vh capped by the visual viewport otherwise). Keeps every
// detail modal at a matching size across the app.
const [isStandalone, setIsStandalone] = useState(false)
useEffect(() => {
const checkStandalone = () => {
const standalone = window.matchMedia("(display-mode: standalone)").matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true
setIsStandalone(standalone)
}
checkStandalone()
const mediaQuery = window.matchMedia("(display-mode: standalone)")
mediaQuery.addEventListener("change", checkStandalone)
return () => mediaQuery.removeEventListener("change", checkStandalone)
}, [])
const [smartJsonData, setSmartJsonData] = useState<{
has_data: boolean
data?: Record<string, unknown>
@@ -1792,7 +1809,13 @@ export function StorageOverview() {
setSmartJsonData(null)
}
}}>
<DialogContent className="max-w-4xl max-h-[80vh] sm:max-h-[85vh] overflow-hidden flex flex-col p-0">
<DialogContent
className={`max-w-4xl flex flex-col p-0 overflow-hidden ${
isStandalone
? "h-[95vh] sm:h-[90vh]"
: "h-[85vh] sm:h-[85vh] max-h-[calc(100dvh-env(safe-area-inset-top)-env(safe-area-inset-bottom)-40px)]"
}`}
>
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle className="flex items-center gap-2">
{selectedDisk?.connection_type === 'usb' ? (
@@ -1816,58 +1839,74 @@ export function StorageOverview() {
</DialogDescription>
</DialogHeader>
{/* Tab Navigation */}
<div className="flex border-b border-border px-6 overflow-x-auto">
{/* Tab Navigation.
Mobile pattern (same as the VM/LXC modal): each tab
shows only its icon; the active tab additionally
reveals its label. That keeps all four tabs on-screen
on narrow viewports without horizontal scroll. */}
<div className="flex border-b border-border px-3 sm:px-6 overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
<button
onClick={() => setActiveModalTab("overview")}
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
activeModalTab === "overview"
? "border-blue-500 text-blue-500"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<Info className="h-4 w-4" />
{t("storage.overview")}
<span className={activeModalTab === "overview" ? "" : "hidden sm:inline"}>
{t("storage.overview")}
</span>
</button>
<button
onClick={() => setActiveModalTab("smart")}
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
activeModalTab === "smart"
? "border-green-500 text-green-500"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<Activity className="h-4 w-4" />
{t("storage.smart")}
<span className={activeModalTab === "smart" ? "" : "hidden sm:inline"}>
{t("storage.smart")}
</span>
</button>
<button
onClick={() => setActiveModalTab("history")}
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
activeModalTab === "history"
? "border-orange-500 text-orange-500"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<Archive className="h-4 w-4" />
{t("storage.history")}
<span className={activeModalTab === "history" ? "" : "hidden sm:inline"}>
{t("storage.history")}
</span>
</button>
<button
onClick={() => setActiveModalTab("schedule")}
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
className={`flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap shrink-0 ${
activeModalTab === "schedule"
? "border-purple-500 text-purple-500"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<Clock className="h-4 w-4" />
{t("storage.schedule")}
<span className={activeModalTab === "schedule" ? "" : "hidden sm:inline"}>
{t("storage.schedule")}
</span>
</button>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
{/* Tab Content — the wrapper is a flex-col so each tab
can either scroll its own content (Overview) or keep
a sticky footer while an inner area grows/scrolls
(SMART, History, Schedule). Removing the wrapper's
own `overflow-y-auto` is what makes that possible. */}
<div className="flex-1 flex flex-col min-h-0 px-6 py-4">
{selectedDisk && activeModalTab === "overview" && (
<div className="space-y-4">
<div className="space-y-4 flex-1 overflow-y-auto min-h-0 pr-1 -mr-1">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">{t("storage.model")}</p>
@@ -3082,10 +3121,12 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri
.top-bar-title { font-weight: 600; }
.top-bar-subtitle { font-size: 11px; color: #94a3b8; }
.top-bar button {
background: #06b6d4; color: #fff; border: none; padding: 10px 20px; border-radius: 6px;
font-size: 14px; font-weight: 600; cursor: pointer;
background: #06b6d4; color: #fff; border: none; padding: 8px 12px; border-radius: 6px;
font-size: 14px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
}
.top-bar button:hover { background: #0891b2; }
.top-bar .btn-group { display: flex; gap: 8px; }
.top-bar button svg { width: 18px; height: 18px; display: block; }
/* Header */
.rpt-header {
@@ -3184,13 +3225,25 @@ function pmxPrint(){
}
</script>
<!-- Top bar (screen only) -->
<!-- Top bar (screen only).
Print / Save as PDF actions replaced by icon-only buttons —
both call the same window.print() dialog (the browser's print
dialog exposes 'Save as PDF' as a destination), so labels
don't need to be translated. aria-label carries the intent
for screen readers. -->
<div class="top-bar no-print">
<div style="display:flex;align-items:center;gap:12px;">
<strong>${t("storage.smartReport.title")}</strong>
<span id="pmx-print-hint" style="font-size:11px;opacity:0.7;">/dev/${disk.name}</span>
</div>
<button onclick="pmxPrint()">${t("storage.smartReport.printPdf")}</button>
<div class="btn-group">
<button onclick="pmxPrint()" title="Print" aria-label="Print">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
</button>
<button onclick="pmxPrint()" title="Save as PDF" aria-label="Save as PDF">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>
</button>
</div>
</div>
<!-- Header -->
@@ -4026,7 +4079,12 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
}
return (
<div className="space-y-6">
<div className="flex flex-col h-full min-h-0">
{/* Scrollable body — Run test controls, progress, last test,
and the SMART Attributes summary. When the attributes list
is long, THIS is what scrolls; the "View full SMART report"
footer stays pinned at the bottom of the tab. */}
<div className="flex-1 overflow-y-auto min-h-0 pr-1 -mr-1 space-y-6">
{/* Quick Actions */}
<div className="space-y-3">
<h4 className="font-semibold flex items-center gap-2">
@@ -4152,8 +4210,8 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-2 text-center">{t("storage.smartTest.worst")}</div>}
<div className="col-span-2 text-center">{t("storage.smartTest.status")}</div>
</div>
<div className="divide-y divide-border max-h-[200px] overflow-y-auto">
{testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
<div className="divide-y divide-border">
{testStatus.smart_data.attributes.map((attr) => (
<div key={attr.id} className={`grid ${(isNvme || testStatus.smart_data?.is_sas) ? 'grid-cols-10' : 'grid-cols-12'} gap-2 p-3 text-sm items-center`}>
{!isNvme && !testStatus.smart_data?.is_sas && <div className="col-span-1 text-muted-foreground">{attr.id}</div>}
<div className={`${(isNvme || testStatus.smart_data?.is_sas) ? 'col-span-5' : 'col-span-5'} truncate`} title={smartAttributeLabel(attr.name)}>{smartAttributeLabel(attr.name)}</div>
@@ -4175,8 +4233,14 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
</div>
)}
{/* View Full Report Button */}
<div className="pt-4 border-t">
</div>
{/* View Full Report Button — sticky footer of the tab.
Sits outside the scrollable body so it's always reachable
without hunting for it at the end of a long attribute
list. The helper subtitle was dropped — the button label
already explains the action, and the extra sentence was
eating vertical space we now give back to attributes. */}
<div className="pt-4 mt-4 border-t shrink-0">
<Button
variant="outline"
className="w-full gap-2 bg-blue-500/10 border-blue-500/30 text-blue-500 hover:bg-blue-500/20 hover:text-blue-400"
@@ -4201,12 +4265,7 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro
<FileText className="h-4 w-4" />
{t("storage.smartTest.viewFullReport")}
</Button>
<p className="text-xs text-muted-foreground text-center mt-2">
{t("storage.smartTest.reportHelp")}
</p>
</div>
</div>
)
}
@@ -4308,7 +4367,7 @@ function HistoryTab({ disk }: { disk: DiskInfo }) {
if (loading) {
return (
<div className="flex flex-col items-center justify-center py-12 gap-3">
<div className="flex flex-col items-center justify-center h-full min-h-0 gap-3">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">{t("storage.historyTab.loading")}</p>
</div>
@@ -4317,7 +4376,7 @@ function HistoryTab({ disk }: { disk: DiskInfo }) {
if (history.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<div className="flex flex-col items-center justify-center h-full min-h-0 text-muted-foreground">
<Archive className="h-12 w-12 mb-3 opacity-30" />
<span className="text-sm">{t("storage.historyTab.empty")}</span>
<span className="text-xs mt-1">{t("storage.historyTab.emptyHint")}</span>
@@ -4326,8 +4385,12 @@ function HistoryTab({ disk }: { disk: DiskInfo }) {
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col h-full min-h-0">
{/* Header stays pinned; the list scrolls; the retention note
sits pinned at the bottom. Same "sticky footer / growing
middle" layout as the other tabs so the modal never wastes
space no matter how many history entries the disk has. */}
<div className="flex items-center justify-between shrink-0 pb-3">
<h4 className="font-semibold flex items-center gap-2">
<Archive className="h-4 w-4" />
{t("storage.historyTab.title")}
@@ -4337,7 +4400,7 @@ function HistoryTab({ disk }: { disk: DiskInfo }) {
</h4>
</div>
<div className="space-y-2">
<div className="space-y-2 flex-1 overflow-y-auto min-h-0 pr-1 -mr-1">
{history.map((entry, i) => {
const isLatest = i === 0
const testDate = new Date(entry.timestamp)
@@ -4399,7 +4462,7 @@ function HistoryTab({ disk }: { disk: DiskInfo }) {
})}
</div>
<p className="text-xs text-muted-foreground text-center pt-2">
<p className="text-xs text-muted-foreground text-center pt-3 mt-2 border-t shrink-0">
{t("storage.historyTab.note")}
</p>
</div>
@@ -4569,7 +4632,7 @@ function ScheduleTab({ disk }: { disk: DiskInfo }) {
if (loading) {
return (
<div className="flex items-center justify-center py-8">
<div className="flex items-center justify-center h-full min-h-0">
<div className="h-6 w-6 rounded-full border-2 border-transparent border-t-purple-400 animate-spin" />
<span className="ml-2 text-muted-foreground">{t("storage.scheduleTab.loading")}</span>
</div>
@@ -4577,7 +4640,7 @@ function ScheduleTab({ disk }: { disk: DiskInfo }) {
}
return (
<div className="space-y-4">
<div className="space-y-4 h-full min-h-0 overflow-y-auto pr-1 -mr-1">
{/* Global Toggle */}
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div>