scope the change journal to host changes, in three sections

The change journal exists so a sysadmin sees what ProxMenux changed on the host — its own configuration, packages and services — not how it uses the host or configures a guest. Several scripts recorded operations that are neither: disk passthrough to a VM, container conversions, VM import/export, mounting a share into an LXC. Those are restored to their original, uninstrumented form. Scripts that operate on the host while also installing a package now record only the package: format-disk keeps its exFAT-tools install, the UUP ISO builder its build dependencies, and the share/host scripts their packages, services and /etc/fstab writes, while the mount and unmount operations they used to log are dropped.

The page now reads in three sections: what ProxMenux optimized after install (each function under its menu name), what its other host scripts changed (by script), and what it installed (packages and utilities, each referencing the script that installed it). A file reads as created or modified with its diff, a service shows its state transition, and the undo line appears only when a revert is possible.
This commit is contained in:
MacRimi
2026-09-10 15:20:39 +02:00
parent d99006bb3c
commit 085cbf7e68
28 changed files with 235 additions and 428 deletions
+147 -89
View File
@@ -81,6 +81,67 @@ function undoKey(change: { revert: string; exactness: string }): string {
return `exactness.${change.exactness}`
}
const FN_LABEL: Record<string, string> = {
apply_amd_fixes: "Apply AMD CPU fixes",
apply_network_optimizations: "Apply network optimizations",
apt_upgrade: "Update and upgrade system",
cleanup_duplicate_repos_pve9: "Configure Proxmox APT repositories",
configure_fastfetch: "Install and configure Fastfetch",
configure_figurine: "Install Figurine",
configure_kernel_panic: "Enable restart on kernel panic",
configure_log2ram: "Install and configure Log2RAM",
configure_pigz: "Use pigz for faster gzip compression",
configure_time_sync: "Synchronize time automatically",
customize_bashrc: "Customize bashrc",
disable_rpc: "Disable portmapper/rpcbind",
enable_ha: "Enable High Availability services",
enable_kexec: "Enable fast reboots",
enable_tcp_fast_open: "Enable TCP BBR/Fast Open control",
enable_vfio_iommu: "Enable VFIO IOMMU support",
enable_zfs_autotrim: "Enable ZFS autotrim (SSD/NVMe pools)",
force_apt_ipv4: "Force APT to use IPv4",
increase_system_limits: "Increase various system limits",
install_ceph: "Add latest Ceph support",
install_guest_agent: "Install relevant guest agent",
install_log2ram: "Install and configure Log2RAM",
install_log2ram_auto: "Install and configure Log2RAM",
install_openvswitch: "Install Open vSwitch",
install_ovh_rtm: "Install OVH Real Time Monitoring",
install_system_utils: "Install common system utilities",
install_zfs_auto_snapshot: "Install ZFS auto-snapshot",
optimize_journald: "Optimize journald",
optimize_logrotate: "Optimize logrotate",
optimize_memory_settings: "Optimize Memory",
optimize_vzdump: "Increase vzdump backup speed",
optimize_zfs_arc: "Optimize ZFS ARC size",
remove_subscription_banner: "Remove subscription banner",
setup_motd: "Set up custom MOTD banner",
setup_persistent_network: "Interface Names (persistent)",
setup_proxmox_repositories: "Configure Proxmox APT repositories",
skip_apt_languages: "Skip downloading additional languages",
update_pve8: "Update and upgrade system",
update_pve9: "Update and upgrade system",
update_pve_appliance_manager: "Update Proxmox VE Appliance Manager",
}
// Post-install functions run from the auto/customizable scripts; everything
// else is a general host script (nvidia/tpu installers, PVE update, vfio…).
const POST_INSTALL_SOURCES = new Set(["auto", "customizable"])
// Which of the three sections a change belongs to: installations are their
// own block, post-install optimizations another, general scripts the rest.
function blockOf(c: { class: string; source: string }): "installs" | "postInstall" | "scripts" {
if (c.class === "installation") return "installs"
if (POST_INSTALL_SOURCES.has(c.source)) return "postInstall"
return "scripts"
}
// A post-install function shows its menu name; anything else shows the script
// that made the change.
function groupLabel(fn: string, source: string): string {
return FN_LABEL[fn] || source || fn || "—"
}
const CLASS_STYLE: Record<string, { chip: string; Icon: typeof Settings2 }> = {
configuration: { chip: "bg-blue-500/10 text-blue-400 border-blue-400/20", Icon: Settings2 },
installation: { chip: "bg-green-500/10 text-green-500 border-green-500/20", Icon: Package },
@@ -226,6 +287,62 @@ function ChangeCard({ change, expanded, onToggle, t, when }: {
)
}
function GroupSection({ title, groups, openFn, toggleFn, open, toggle, t, when }: {
title: string
groups: { key: string; label: string; version: string; last: number; items: Change[] }[]
openFn: Set<string>; toggleFn: (k: string) => void
open: Set<number>; toggle: (id: number) => void
t: (k: string, params?: Record<string, string>) => string
when: (n: number) => string
}) {
if (groups.length === 0) return null
return (
<div className="space-y-2">
<h3 className="text-sm font-semibold text-foreground px-1">{title}</h3>
{groups.map((g) => {
const fnOpen = openFn.has(g.key)
return (
<Card key={g.key} className="bg-card border-border">
<button
type="button"
onClick={() => toggleFn(g.key)}
aria-expanded={fnOpen}
className="w-full text-left p-3 flex flex-wrap items-center gap-2
rounded-lg hover:bg-white/5 transition-colors cursor-pointer"
>
{fnOpen
? <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
: <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />}
<FileCode className="h-4 w-4 shrink-0 text-blue-400" />
<span className="min-w-0 text-sm font-medium text-foreground break-words">
{g.label}
</span>
{g.version && (
<Badge variant="outline" className="text-xs shrink-0">v{g.version}</Badge>
)}
<Badge variant="outline" className="text-xs tabular-nums shrink-0">
{t("audit.changes.count", { count: String(g.items.length) })}
</Badge>
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
{when(g.last)}
</span>
</button>
{fnOpen && (
<CardContent className="pt-0 pl-10 pr-3 space-y-2">
{g.items.map((change) => (
<ChangeCard key={change.id} change={change}
expanded={open.has(change.id)} onToggle={() => toggle(change.id)}
t={t} when={when} />
))}
</CardContent>
)}
</Card>
)
})}
</div>
)
}
export function AuditChanges() {
const t = useT()
const { language } = useI18n()
@@ -234,7 +351,6 @@ export function AuditChanges() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [open, setOpen] = useState<Set<number>>(new Set())
const [filter, setFilter] = useState<string>("all")
const load = useCallback(async () => {
try {
@@ -268,26 +384,27 @@ export function AuditChanges() {
return next
})
const visible = useMemo(
() => changes.filter((c) => filter === "all" || c.class === filter),
[changes, filter],
)
// A sysadmin asks "what did each ProxMenux function do to my host?" — so
// the changes are grouped under the function that made them. Each group is
// one card; opening it reveals that function's individual file changes.
const groups = useMemo(() => {
const byFn = new Map<string, { function: string; version: string; last: number; items: Change[] }>()
for (const c of visible) {
const key = c.function || "—"
const g = byFn.get(key) || { function: key, version: c.function_version || "", last: 0, items: [] }
// A sysadmin reads this in three sections: what ProxMenux optimized
// (post-install), what its other scripts changed, and what it installed.
// Within each, changes are grouped under a card that opens to reveal them.
type Group = { key: string; label: string; version: string; last: number; items: Change[] }
const blocks = useMemo(() => {
const mk = () => new Map<string, Group>()
const post = mk(), scripts = mk(), installs = mk()
const pick = (b: string) => b === "installs" ? installs : b === "postInstall" ? post : scripts
for (const c of changes) {
const target = pick(blockOf(c))
const key = c.function || c.source || "—"
const label = groupLabel(c.function, c.source)
const g = target.get(key) || { key, label, version: c.function_version || "", last: 0, items: [] }
g.items.push(c)
if (c.recorded_at > g.last) g.last = c.recorded_at
if (c.function_version) g.version = c.function_version
byFn.set(key, g)
target.set(key, g)
}
return Array.from(byFn.values()).sort((a, b) => b.last - a.last)
}, [visible])
const sort = (m: Map<string, Group>) => Array.from(m.values()).sort((a, b) => b.last - a.last)
return { post: sort(post), scripts: sort(scripts), installs: sort(installs) }
}, [changes])
const when = (epoch: number) => new Date(epoch * 1000).toLocaleString(language)
@@ -315,81 +432,22 @@ export function AuditChanges() {
{t("audit.changes.since", { date: when(summary.journal_started) })}
</p>
)}
<div className="flex flex-wrap items-center gap-2">
{(["all", "configuration", "installation", "execution", "registration"] as const)
.filter((key) => key === "all" || summary?.by_class?.[key])
.map((key) => (
<button
key={key}
type="button"
onClick={() => setFilter(key)}
className={`px-3 py-1 rounded-md text-sm transition-colors ${
filter === key
? "bg-blue-500 text-white"
: "text-muted-foreground hover:text-foreground hover:bg-background/60"
}`}
>
{t(`audit.changes.class.${key}`)}
{key !== "all" && summary?.by_class?.[key] !== undefined && (
<span className="ml-1.5 tabular-nums">{summary.by_class[key]}</span>
)}
</button>
))}
</div>
</CardContent>
</Card>
<div className="space-y-2">
{groups.map((g) => {
const fnOpen = openFn.has(g.function)
return (
<Card key={g.function} className="bg-card border-border">
<button
type="button"
onClick={() => toggleFn(g.function)}
aria-expanded={fnOpen}
className="w-full text-left p-3 flex flex-wrap items-center gap-2
rounded-lg hover:bg-white/5 transition-colors cursor-pointer"
>
{fnOpen
? <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
: <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />}
<FileCode className="h-4 w-4 shrink-0 text-blue-400" />
<span className="min-w-0 font-mono text-sm font-medium text-foreground break-all">
{g.function}
</span>
{g.version && (
<Badge variant="outline" className="text-xs shrink-0">v{g.version}</Badge>
)}
<Badge variant="outline" className="text-xs tabular-nums shrink-0">
{t("audit.changes.count", { count: String(g.items.length) })}
</Badge>
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
{when(g.last)}
</span>
</button>
{fnOpen && (
<CardContent className="pt-0 pl-10 pr-3 space-y-2">
{g.items.map((change) => (
<ChangeCard
key={change.id}
change={change}
expanded={open.has(change.id)}
onToggle={() => toggle(change.id)}
t={t}
when={when}
/>
))}
</CardContent>
)}
</Card>
)
})}
{groups.length === 0 && summary && summary.total > 0 && (
<p className="text-sm text-muted-foreground px-1">{t("audit.changes.noneInFilter")}</p>
)}
</div>
<GroupSection title={t("audit.changes.section.postInstall")}
groups={blocks.post} openFn={openFn} toggleFn={toggleFn}
open={open} toggle={toggle} t={t} when={when} />
<GroupSection title={t("audit.changes.section.scripts")}
groups={blocks.scripts} openFn={openFn} toggleFn={toggleFn}
open={open} toggle={toggle} t={t} when={when} />
<GroupSection title={t("audit.changes.section.installs")}
groups={blocks.installs} openFn={openFn} toggleFn={toggleFn}
open={open} toggle={toggle} t={t} when={when} />
{summary && summary.total > 0
&& blocks.post.length + blocks.scripts.length + blocks.installs.length === 0 && (
<p className="text-sm text-muted-foreground px-1">{t("audit.changes.empty")}</p>
)}
</div>
)
}
+6 -1
View File
@@ -5875,7 +5875,7 @@
"changes": {
"loading": "Änderungsjournal wird gelesen…",
"failed": "Das Änderungsjournal war nicht lesbar",
"intro": "Was ProxMenux auf diesem Host geändert hat und was vor jeder Änderung vorhanden war. Gezeigt wird die Differenz, nicht das Skript, das sie angewandt hat.",
"intro": "Was ProxMenux auf diesem Host geändert hat und was vor jeder Änderung vorhanden war. Gezeigt wird die Differenz.",
"empty": "Auf diesem Host wurde noch nichts aufgezeichnet.",
"since": "Aufzeichnung seit {date}. Früher Angewandtes erscheint als angewandt, ohne den ersetzten Zustand.",
"byFunction": "Nach Funktion",
@@ -5925,6 +5925,11 @@
"undo": {
"remove": "Löscht die Datei (vorher war keine vorhanden)",
"restore": "Stellt exakt den vorherigen Zustand wieder her"
},
"section": {
"postInstall": "Post-Install-Optimierungen",
"scripts": "ProxMenux-Skripte",
"installs": "Installierte Pakete und Werkzeuge"
}
},
"comparison": {
+6 -1
View File
@@ -5941,7 +5941,7 @@
"changes": {
"loading": "Reading the change journal…",
"failed": "The change journal could not be read",
"intro": "What ProxMenux changed on this host and what was there before each change. The difference is shown, not the script that applied it.",
"intro": "What ProxMenux changed on this host and what was there before each change. The difference is shown.",
"empty": "Nothing has been recorded on this host yet.",
"since": "Recording since {date}. Anything applied before that appears as applied, without the state it replaced.",
"byFunction": "By function",
@@ -5991,6 +5991,11 @@
"undo": {
"remove": "Deletes the file (there was none before)",
"restore": "Restores exactly what was there"
},
"section": {
"postInstall": "Post-install optimizations",
"scripts": "ProxMenux scripts",
"installs": "Installed packages and utilities"
}
},
"comparison": {
+7 -2
View File
@@ -4306,7 +4306,7 @@
},
"profile": {
"custom": "Personalizado",
"customDescription": "Elija exactamente qué rutas deben incluirse.",
"customDescription": "Elija qué rutas deben incluirse.",
"default": "Por defecto",
"defaultDescription": "Configuración de host recomendada y datos de ProxMenux.",
"defaultDescriptionShort": "Configuración de host recomendada.",
@@ -5875,7 +5875,7 @@
"changes": {
"loading": "Leyendo el registro de cambios…",
"failed": "No se ha podido leer el registro de cambios",
"intro": "Qué ha cambiado ProxMenux en este host y qué había antes de cada cambio. Se muestra la diferencia, no el script que la aplicó.",
"intro": "Qué ha cambiado ProxMenux en este host y qué había antes de cada cambio. Se muestra la diferencia.",
"empty": "Todavía no se ha registrado nada en este host.",
"since": "Registrando desde el {date}. Lo aplicado antes figura como aplicado, sin el estado al que sustituyó.",
"byFunction": "Por función",
@@ -5925,6 +5925,11 @@
"undo": {
"remove": "Elimina el fichero (no había ninguno antes)",
"restore": "Restaura exactamente lo que había"
},
"section": {
"postInstall": "Optimizaciones de post-install",
"scripts": "Scripts de ProxMenux",
"installs": "Paquetes y utilidades instalados"
}
},
"comparison": {
+6 -1
View File
@@ -5875,7 +5875,7 @@
"changes": {
"loading": "Lecture du journal des modifications…",
"failed": "Le journal des modifications n'a pas pu être lu",
"intro": "Ce que ProxMenux a changé sur cet hôte et ce qui existait avant chaque changement. C'est la différence qui est montrée, non le script qui l'a appliquée.",
"intro": "Ce que ProxMenux a changé sur cet hôte et ce qui existait avant chaque changement. C'est la différence qui est montrée.",
"empty": "Rien n'a encore été enregistré sur cet hôte.",
"since": "Enregistrement depuis le {date}. Ce qui a été appliqué avant figure comme appliqué, sans l'état remplacé.",
"byFunction": "Par fonction",
@@ -5925,6 +5925,11 @@
"undo": {
"remove": "Supprime le fichier (il n'y en avait aucun avant)",
"restore": "Restaure exactement ce qui existait"
},
"section": {
"postInstall": "Optimisations post-installation",
"scripts": "Scripts ProxMenux",
"installs": "Paquets et utilitaires installés"
}
},
"comparison": {
+6 -1
View File
@@ -5875,7 +5875,7 @@
"changes": {
"loading": "Lettura del registro delle modifiche…",
"failed": "Non è stato possibile leggere il registro delle modifiche",
"intro": "Cosa ha cambiato ProxMenux su questo host e cosa c'era prima di ogni modifica. Viene mostrata la differenza, non lo script che l'ha applicata.",
"intro": "Cosa ha cambiato ProxMenux su questo host e cosa c'era prima di ogni modifica. Viene mostrata la differenza.",
"empty": "Su questo host non è ancora stato registrato nulla.",
"since": "Registrazione dal {date}. Ciò che è stato applicato prima risulta applicato, senza lo stato sostituito.",
"byFunction": "Per funzione",
@@ -5925,6 +5925,11 @@
"undo": {
"remove": "Elimina il file (prima non ce n'era nessuno)",
"restore": "Ripristina esattamente ciò che c'era"
},
"section": {
"postInstall": "Ottimizzazioni post-installazione",
"scripts": "Script di ProxMenux",
"installs": "Pacchetti e utilità installati"
}
},
"comparison": {
+6 -1
View File
@@ -5875,7 +5875,7 @@
"changes": {
"loading": "A ler o registo de alterações…",
"failed": "Não foi possível ler o registo de alterações",
"intro": "O que o ProxMenux alterou neste host e o que existia antes de cada alteração. Mostra-se a diferença, não o script que a aplicou.",
"intro": "O que o ProxMenux alterou neste host e o que existia antes de cada alteração. Mostra-se a diferença.",
"empty": "Ainda não foi registado nada neste anfitrião.",
"since": "A registar desde {date}. O que foi aplicado antes figura como aplicado, sem o estado que substituiu.",
"byFunction": "Por função",
@@ -5925,6 +5925,11 @@
"undo": {
"remove": "Elimina o ficheiro (não existia nenhum antes)",
"restore": "Restaura exatamente o que existia"
},
"section": {
"postInstall": "Otimizações pós-instalação",
"scripts": "Scripts do ProxMenux",
"installs": "Pacotes e utilitários instalados"
}
},
"comparison": {
+6 -1
View File
@@ -5941,7 +5941,7 @@
"changes": {
"loading": "Načítava sa denník zmien…",
"failed": "Denník zmien sa nepodarilo prečítať",
"intro": "Čo ProxMenux na tomto hostiteľovi zmenil a čo tam bolo pred každou zmenou. Zobrazuje sa rozdiel, nie skript, ktorý ho aplikoval.",
"intro": "Čo ProxMenux na tomto hostiteľovi zmenil a čo tam bolo pred každou zmenou. Zobrazuje sa rozdiel.",
"empty": "Na tomto hostiteľovi zatiaľ nebolo nič zaznamenané.",
"since": "Zaznamenáva sa od {date}. Čo bolo použité skôr, figuruje ako použité, bez stavu, ktorý nahradilo.",
"byFunction": "Podľa funkcie",
@@ -5991,6 +5991,11 @@
"undo": {
"remove": "Odstráni súbor (predtým žiadny nebol)",
"restore": "Obnoví presne to, čo tam bolo"
},
"section": {
"postInstall": "Optimalizácie po inštalácii",
"scripts": "Skripty ProxMenux",
"installs": "Nainštalované balíky a nástroje"
}
},
"comparison": {
+6 -1
View File
@@ -5876,7 +5876,7 @@
"changes": {
"loading": "Läser ändringsjournalen…",
"failed": "Ändringsjournalen kunde inte läsas",
"intro": "Vad ProxMenux ändrat på den här värden och vad som fanns före varje ändring. Skillnaden visas, inte skriptet som tillämpade den.",
"intro": "Vad ProxMenux ändrat på den här värden och vad som fanns före varje ändring. Skillnaden visas.",
"empty": "Inget har ännu registrerats på den här värden.",
"since": "Registrerar sedan {date}. Det som tillämpades dessförinnan står som tillämpat, utan det tillstånd det ersatte.",
"byFunction": "Per funktion",
@@ -5926,6 +5926,11 @@
"undo": {
"remove": "Tar bort filen (det fanns ingen tidigare)",
"restore": "Återställer exakt vad som fanns"
},
"section": {
"postInstall": "Optimeringar efter installation",
"scripts": "ProxMenux-skript",
"installs": "Installerade paket och verktyg"
}
},
"comparison": {