group host changes by function with a truthful before/after

This commit is contained in:
MacRimi
2026-09-09 19:26:22 +02:00
parent 36071e6bd3
commit 90c4a720e3
15 changed files with 333 additions and 184 deletions
+212 -140
View File
@@ -58,6 +58,29 @@ interface Summary {
journal_started: number | null
}
// A file that did not exist before was created, not replaced — a sysadmin
// reading this must see "new file", not "file replaced". `capture` carries
// that distinction: "created" for a file born here, "present" for one that
// already had contents we captured before overwriting them.
function operationLabelKey(change: { operation: string; capture: string }): string {
// Two truthful labels for a file: created if it did not exist, modified if
// it did. The diff below shows exactly what changed either way, so there is
// no need to distinguish write/edit/append in the label.
if (["write_file", "edit_file", "append_file"].includes(change.operation)) {
return change.capture === "created" ? "operation.file_created" : "operation.file_modified"
}
return `operation.${change.operation}`
}
// Undoing follows `revert`, not `exactness`: a created file is undone by
// deleting it (there was nothing before), an overwritten one by restoring
// what we captured.
function undoKey(change: { revert: string; exactness: string }): string {
if (change.revert === "remove") return "undo.remove"
if (change.revert === "restore" && change.exactness === "exact") return "undo.restore"
return `exactness.${change.exactness}`
}
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 },
@@ -65,6 +88,144 @@ const CLASS_STYLE: Record<string, { chip: string; Icon: typeof Settings2 }> = {
registration: { chip: "bg-muted text-muted-foreground border-border", Icon: HelpCircle },
}
function ChangeCard({ change, expanded, onToggle, t, when }: {
change: Change; expanded: boolean; onToggle: () => void
t: (k: string, params?: Record<string, string>) => string
when: (n: number) => string
}) {
const style = CLASS_STYLE[change.class] || CLASS_STYLE.registration
const Icon = style.Icon
const installed = String(change.detail?.installed || "")
return (
<Card className="bg-card border-border">
<button
type="button"
onClick={onToggle}
aria-expanded={expanded}
className="w-full text-left p-3 flex flex-wrap items-center gap-2
rounded-lg hover:bg-white/5 transition-colors cursor-pointer"
>
{expanded
? <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
: <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />}
<Badge variant="outline" className={`${style.chip} gap-1.5 shrink-0`}>
<Icon className="h-3.5 w-3.5" />
{t(`audit.changes.${operationLabelKey(change)}`)}
</Badge>
<span className="min-w-0 font-mono text-sm text-foreground break-all">
{change.target}
</span>
{change.diff?.available && (
<Badge variant="outline" className="text-xs tabular-nums shrink-0">
+{change.diff.added} {change.diff.removed}
</Badge>
)}
{change.capture === "unknown" && (
<Badge variant="outline" className="text-xs shrink-0">
{t("audit.changes.capture.unknown")}
</Badge>
)}
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
{when(change.recorded_at)}
</span>
</button>
{expanded && (
<CardContent className="pt-0 pl-10 space-y-3">
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
<span>{t("audit.changes.source")}:{" "}
<span className="font-mono">{change.source || "—"}</span></span>
{change.revert && change.revert !== "none" && (
<span>{t("audit.changes.reversibility")}:{" "}
{t(`audit.changes.${undoKey(change)}`)}</span>
)}
</div>
{(change.operation === "enable_service" || change.operation === "disable_service")
&& Boolean(change.detail?.before_state || change.detail?.after_state) && (
<div className="flex items-center gap-2 text-xs">
<span className="font-mono text-muted-foreground">
{String(change.detail?.before_state || "—").replace(/\s+/g, " ")}
</span>
<span className="text-muted-foreground"></span>
<span className="font-mono text-foreground">
{String(change.detail?.after_state || "—").replace(/\s+/g, " ")}
</span>
</div>
)}
{installed && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
{t("audit.changes.packagesAdded")}
</p>
<div className="flex flex-wrap gap-1.5">
{installed.split(/\s+/).filter(Boolean).map((pkg) => (
<Badge key={pkg} variant="outline" className="font-mono text-xs">
{pkg}
</Badge>
))}
</div>
</div>
)}
{change.class === "execution" && Boolean(change.detail?.command) && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
{t("audit.changes.commandRun")}
</p>
<pre className="text-xs font-mono bg-background border border-border
rounded-md p-2.5 overflow-x-auto">
{String(change.detail.command)}
</pre>
<p className="text-xs text-muted-foreground mt-1">
{t("audit.changes.executionNote")}
</p>
</div>
)}
{change.diff && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
{t("audit.changes.difference")}
</p>
{change.diff.available ? (
<>
<pre className="text-xs font-mono bg-background border border-border
rounded-md p-2.5 overflow-x-auto">
{(change.diff.hunks || []).map((line: string, i: number) => (
<div key={i} className={
line.startsWith("+") ? "text-green-500"
: line.startsWith("-") ? "text-red-400"
: line.startsWith("@@") ? "text-blue-400" : ""
}>{line}</div>
))}
</pre>
{change.diff.truncated && (
<p className="text-xs text-muted-foreground mt-1">
{t("audit.changes.diffTruncated")}
</p>
)}
</>
) : (
<p className="text-xs text-muted-foreground">
{t("audit.changes.diffUnavailable")}
</p>
)}
</div>
)}
{change.capture === "unknown" && (
<p className="text-xs text-muted-foreground">
{t("audit.changes.unknownNote")}
</p>
)}
</CardContent>
)}
</Card>
)
}
export function AuditChanges() {
const t = useT()
const { language } = useI18n()
@@ -100,11 +261,34 @@ export function AuditChanges() {
return next
})
const [openFn, setOpenFn] = useState<Set<string>>(new Set())
const toggleFn = (fn: string) => setOpenFn((prev) => {
const next = new Set(prev)
next.has(fn) ? next.delete(fn) : next.add(fn)
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: [] }
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)
}
return Array.from(byFn.values()).sort((a, b) => b.last - a.last)
}, [visible])
const when = (epoch: number) => new Date(epoch * 1000).toLocaleString(language)
if (loading) {
@@ -155,166 +339,54 @@ export function AuditChanges() {
</CardContent>
</Card>
{summary && summary.functions.length > 0 && (
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base font-semibold text-foreground">
<FileCode className="h-4 w-4" />{t("audit.changes.byFunction")}
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-2">
{summary.functions.map((fn) => (
<button
key={fn.function}
type="button"
onClick={() => setFilter("all")}
className="flex w-full flex-wrap items-center gap-2 rounded-md border
border-border p-2.5 text-left hover:bg-white/5
transition-colors cursor-pointer"
>
<span className="font-mono text-sm text-foreground">{fn.function}</span>
{fn.version && (
<Badge variant="outline" className="text-xs">v{fn.version}</Badge>
)}
<Badge variant="outline" className="text-xs tabular-nums">
{t("audit.changes.count", { count: String(fn.changes) })}
</Badge>
<span className="ml-auto text-xs text-muted-foreground">
{when(fn.last_change)}
</span>
</button>
))}
</CardContent>
</Card>
)}
<div className="space-y-2">
{visible.map((change) => {
const style = CLASS_STYLE[change.class] || CLASS_STYLE.registration
const Icon = style.Icon
const expanded = open.has(change.id)
const installed = String(change.detail?.installed || "")
{groups.map((g) => {
const fnOpen = openFn.has(g.function)
return (
<Card key={change.id} className="bg-card border-border">
<Card key={g.function} className="bg-card border-border">
<button
type="button"
onClick={() => toggle(change.id)}
aria-expanded={expanded}
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"
>
{expanded
{fnOpen
? <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
: <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />}
<Badge variant="outline" className={`${style.chip} gap-1.5 shrink-0`}>
<Icon className="h-3.5 w-3.5" />
{t(`audit.changes.operation.${change.operation}`)}
</Badge>
<span className="min-w-0 font-mono text-sm text-foreground break-all">
{change.target}
<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>
{change.diff?.available && (
<Badge variant="outline" className="text-xs tabular-nums shrink-0">
+{change.diff.added} {change.diff.removed}
</Badge>
)}
{/* Whether the previous state is known is what decides
if undoing this is even discussable. */}
{change.capture === "unknown" && (
<Badge variant="outline" className="text-xs shrink-0">
{t("audit.changes.capture.unknown")}
</Badge>
{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(change.recorded_at)}
{when(g.last)}
</span>
</button>
{expanded && (
<CardContent className="pt-0 pl-10 space-y-3">
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
<span>{t("audit.changes.function")}:{" "}
<span className="font-mono text-foreground">{change.function || "—"}</span>
{change.function_version && ` v${change.function_version}`}
</span>
<span>{t("audit.changes.source")}:{" "}
<span className="font-mono">{change.source || "—"}</span></span>
<span>{t("audit.changes.reversibility")}:{" "}
{t(`audit.changes.exactness.${change.exactness}`)}</span>
</div>
{installed && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
{t("audit.changes.packagesAdded")}
</p>
<div className="flex flex-wrap gap-1.5">
{installed.split(/\s+/).filter(Boolean).map((pkg) => (
<Badge key={pkg} variant="outline" className="font-mono text-xs">
{pkg}
</Badge>
))}
</div>
</div>
)}
{change.class === "execution" && Boolean(change.detail?.command) && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
{t("audit.changes.commandRun")}
</p>
<pre className="text-xs font-mono bg-background border border-border
rounded-md p-2.5 overflow-x-auto">
{String(change.detail.command)}
</pre>
<p className="text-xs text-muted-foreground mt-1">
{t("audit.changes.executionNote")}
</p>
</div>
)}
{change.diff && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
{t("audit.changes.difference")}
</p>
{change.diff.available ? (
<>
<pre className="text-xs font-mono bg-background border border-border
rounded-md p-2.5 overflow-x-auto">
{(change.diff.hunks || []).map((line: string, i: number) => (
<div key={i} className={
line.startsWith("+") ? "text-green-500"
: line.startsWith("-") ? "text-red-400"
: line.startsWith("@@") ? "text-blue-400" : ""
}>{line}</div>
))}
</pre>
{change.diff.truncated && (
<p className="text-xs text-muted-foreground mt-1">
{t("audit.changes.diffTruncated")}
</p>
)}
</>
) : (
<p className="text-xs text-muted-foreground">
{t("audit.changes.diffUnavailable")}
</p>
)}
</div>
)}
{change.capture === "unknown" && (
<p className="text-xs text-muted-foreground">
{t("audit.changes.unknownNote")}
</p>
)}
{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>
)
})}
{visible.length === 0 && summary && summary.total > 0 && (
{groups.length === 0 && summary && summary.total > 0 && (
<p className="text-sm text-muted-foreground px-1">{t("audit.changes.noneInFilter")}</p>
)}
</div>
+10 -2
View File
@@ -5888,8 +5888,8 @@
"diffUnavailable": "Der ersetzte Inhalt ist nicht mehr gespeichert, der Unterschied lässt sich nicht zeigen.",
"packagesAdded": "Hinzugefügte Pakete",
"commandRun": "Ausgeführter Befehl",
"executionNote": "ProxMenux hat dies auf Anforderung ausgeführt; was sich änderte, entschied der Befehl, nicht ProxMenux.",
"unknownNote": "Dies wurde vor dem Journal angewandt, der ersetzte Zustand wurde nie erfasst.",
"executionNote": "ProxMenux hat diesen Befehl ausgeführt; was er geändert hat, bestimmt der Befehl selbst.",
"unknownNote": "Angewendet, bevor das Journal existierte, daher wurde der vorherige Zustand nicht erfasst.",
"noneInFilter": "Keine Änderung dieser Art.",
"class": {
"all": "Alle",
@@ -5900,7 +5900,11 @@
},
"operation": {
"write_file": "Datei ersetzt",
"file_created": "Datei erstellt",
"file_modified": "Datei geändert",
"write_file_created": "Datei erstellt",
"edit_file": "Datei bearbeitet",
"append_file": "An Datei angehängt",
"remove_file": "Datei entfernt",
"install_package": "Installiert",
"enable_service": "Dienst aktiviert",
@@ -5917,6 +5921,10 @@
"exact": "Stellt genau das Vorherige wieder her",
"partial": "Teilweise: Abhängigkeiten können bleiben oder mitgehen",
"none": "Aus dem Journal nicht rückgängig zu machen"
},
"undo": {
"remove": "Löscht die Datei (vorher war keine vorhanden)",
"restore": "Stellt exakt den vorherigen Zustand wieder her"
}
},
"comparison": {
+10 -2
View File
@@ -5954,8 +5954,8 @@
"diffUnavailable": "The content that was replaced is no longer stored, so the difference cannot be shown.",
"packagesAdded": "Packages added",
"commandRun": "Command run",
"executionNote": "ProxMenux ran this on request; what it changed was decided by the command, not by ProxMenux.",
"unknownNote": "This was applied before the journal existed, so what it replaced was never captured.",
"executionNote": "ProxMenux ran this command; what it changed is up to the command itself.",
"unknownNote": "Applied before the journal existed, so the prior state was not captured.",
"noneInFilter": "No change of this kind.",
"class": {
"all": "All",
@@ -5966,7 +5966,11 @@
},
"operation": {
"write_file": "File replaced",
"file_created": "File created",
"file_modified": "File modified",
"write_file_created": "File created",
"edit_file": "File edited",
"append_file": "Appended to file",
"remove_file": "File removed",
"install_package": "Installed",
"enable_service": "Service enabled",
@@ -5983,6 +5987,10 @@
"exact": "Restores exactly what was there",
"partial": "Partial: dependencies may remain or be removed with it",
"none": "Cannot be undone from the journal"
},
"undo": {
"remove": "Deletes the file (there was none before)",
"restore": "Restores exactly what was there"
}
},
"comparison": {
+10 -2
View File
@@ -5888,8 +5888,8 @@
"diffUnavailable": "El contenido sustituido ya no está almacenado, así que no se puede mostrar la diferencia.",
"packagesAdded": "Paquetes añadidos",
"commandRun": "Comando ejecutado",
"executionNote": "ProxMenux lo ejecutó a petición; lo que cambió lo decidió el comando, no ProxMenux.",
"unknownNote": "Esto se aplicó antes de que existiera el registro, así que nunca se capturó a qué sustituyó.",
"executionNote": "ProxMenux ejecutó este comando; lo que cambió lo determina el propio comando.",
"unknownNote": "Se aplicó antes de que existiera el registro, así que no se guardó el estado anterior.",
"noneInFilter": "No hay ningún cambio de este tipo.",
"class": {
"all": "Todos",
@@ -5900,7 +5900,11 @@
},
"operation": {
"write_file": "Fichero reemplazado",
"file_created": "Fichero creado",
"file_modified": "Fichero modificado",
"write_file_created": "Fichero creado",
"edit_file": "Fichero editado",
"append_file": "Se añadió al fichero",
"remove_file": "Fichero eliminado",
"install_package": "Instalado",
"enable_service": "Servicio habilitado",
@@ -5917,6 +5921,10 @@
"exact": "Restaura exactamente lo que había",
"partial": "Parcial: pueden quedar dependencias o irse con ello",
"none": "No se puede deshacer desde el registro"
},
"undo": {
"remove": "Elimina el fichero (no había ninguno antes)",
"restore": "Restaura exactamente lo que había"
}
},
"comparison": {
+10 -2
View File
@@ -5888,8 +5888,8 @@
"diffUnavailable": "Le contenu remplacé n'est plus stocké, la différence ne peut pas être montrée.",
"packagesAdded": "Paquets ajoutés",
"commandRun": "Commande exécutée",
"executionNote": "ProxMenux l'a exécutée à la demande ; ce qui a changé a été décidé par la commande, pas par ProxMenux.",
"unknownNote": "Ceci a été appliqué avant l'existence du journal ; l'état remplacé n'a jamais été capturé.",
"executionNote": "ProxMenux a exécuté cette commande ; ce qu'elle a changé dépend de la commande elle-même.",
"unknownNote": "Appliqué avant l'existence du journal, l'état précédent n'a donc pas été capturé.",
"noneInFilter": "Aucune modification de ce type.",
"class": {
"all": "Toutes",
@@ -5900,7 +5900,11 @@
},
"operation": {
"write_file": "Fichier remplacé",
"file_created": "Fichier créé",
"file_modified": "Fichier modifié",
"write_file_created": "Fichier créé",
"edit_file": "Fichier modifié",
"append_file": "Ajouté au fichier",
"remove_file": "Fichier supprimé",
"install_package": "Installé",
"enable_service": "Service activé",
@@ -5917,6 +5921,10 @@
"exact": "Restaure exactement ce qui était là",
"partial": "Partiel : des dépendances peuvent rester ou partir avec",
"none": "Ne peut pas être annulé depuis le journal"
},
"undo": {
"remove": "Supprime le fichier (il n'y en avait aucun avant)",
"restore": "Restaure exactement ce qui existait"
}
},
"comparison": {
+10 -2
View File
@@ -5888,8 +5888,8 @@
"diffUnavailable": "Il contenuto sostituito non è più archiviato, quindi la differenza non si può mostrare.",
"packagesAdded": "Pacchetti aggiunti",
"commandRun": "Comando eseguito",
"executionNote": "ProxMenux lo ha eseguito su richiesta; ciò che è cambiato lo ha deciso il comando, non ProxMenux.",
"unknownNote": "È stato applicato prima che esistesse il registro, quindi ciò che ha sostituito non è mai stato catturato.",
"executionNote": "ProxMenux ha eseguito questo comando; ciò che ha cambiato dipende dal comando stesso.",
"unknownNote": "Applicato prima che esistesse il registro, quindi lo stato precedente non è stato acquisito.",
"noneInFilter": "Nessuna modifica di questo tipo.",
"class": {
"all": "Tutte",
@@ -5900,7 +5900,11 @@
},
"operation": {
"write_file": "File sostituito",
"file_created": "File creato",
"file_modified": "File modificato",
"write_file_created": "File creato",
"edit_file": "File modificato",
"append_file": "Aggiunto al file",
"remove_file": "File rimosso",
"install_package": "Installato",
"enable_service": "Servizio abilitato",
@@ -5917,6 +5921,10 @@
"exact": "Ripristina esattamente ciò che c'era",
"partial": "Parziale: le dipendenze possono restare o andarsene con esso",
"none": "Non si può annullare dal registro"
},
"undo": {
"remove": "Elimina il file (prima non ce n'era nessuno)",
"restore": "Ripristina esattamente ciò che c'era"
}
},
"comparison": {
+10 -2
View File
@@ -5888,8 +5888,8 @@
"diffUnavailable": "O conteúdo substituído já não está guardado, pelo que a diferença não pode ser mostrada.",
"packagesAdded": "Pacotes adicionados",
"commandRun": "Comando executado",
"executionNote": "O ProxMenux executou-o a pedido; o que mudou foi decidido pelo comando, não pelo ProxMenux.",
"unknownNote": "Isto foi aplicado antes de existir o registo, pelo que nunca se capturou o que substituiu.",
"executionNote": "O ProxMenux executou este comando; o que mudou depende do próprio comando.",
"unknownNote": "Aplicado antes de o registo existir, por isso o estado anterior não foi capturado.",
"noneInFilter": "Não há alterações deste tipo.",
"class": {
"all": "Todas",
@@ -5900,7 +5900,11 @@
},
"operation": {
"write_file": "Ficheiro substituído",
"file_created": "Ficheiro criado",
"file_modified": "Ficheiro modificado",
"write_file_created": "Ficheiro criado",
"edit_file": "Ficheiro editado",
"append_file": "Acrescentado ao ficheiro",
"remove_file": "Ficheiro removido",
"install_package": "Instalado",
"enable_service": "Serviço ativado",
@@ -5917,6 +5921,10 @@
"exact": "Restaura exatamente o que lá estava",
"partial": "Parcial: podem ficar dependências ou sair com ele",
"none": "Não pode ser desfeito a partir do registo"
},
"undo": {
"remove": "Elimina o ficheiro (não existia nenhum antes)",
"restore": "Restaura exatamente o que existia"
}
},
"comparison": {
+10 -2
View File
@@ -5954,8 +5954,8 @@
"diffUnavailable": "Nahradený obsah už nie je uložený, rozdiel sa nedá zobraziť.",
"packagesAdded": "Pridané balíky",
"commandRun": "Spustený príkaz",
"executionNote": "ProxMenux to spustil na požiadanie; čo sa zmenilo, rozhodol príkaz, nie ProxMenux.",
"unknownNote": "Toto bolo použité pred vznikom denníka, takže nahradený stav sa nikdy nezachytil.",
"executionNote": "ProxMenux spustil tento príkaz; čo zmenil, určuje samotný príkaz.",
"unknownNote": "Použité skôr, než existoval denník, takže predchádzajúci stav sa nezachytil.",
"noneInFilter": "Žiadna zmena tohto druhu.",
"class": {
"all": "Všetky",
@@ -5966,7 +5966,11 @@
},
"operation": {
"write_file": "Súbor nahradený",
"file_created": "Súbor vytvorený",
"file_modified": "Súbor zmenený",
"write_file_created": "Súbor vytvorený",
"edit_file": "Súbor upravený",
"append_file": "Pridané do súboru",
"remove_file": "Súbor odstránený",
"install_package": "Nainštalované",
"enable_service": "Služba povolená",
@@ -5983,6 +5987,10 @@
"exact": "Obnoví presne to, čo tam bolo",
"partial": "Čiastočné: závislosti môžu zostať alebo odísť s tým",
"none": "Z denníka sa nedá vrátiť"
},
"undo": {
"remove": "Odstráni súbor (predtým žiadny nebol)",
"restore": "Obnoví presne to, čo tam bolo"
}
},
"comparison": {
+10 -2
View File
@@ -5889,8 +5889,8 @@
"diffUnavailable": "Innehållet som ersattes lagras inte längre, så skillnaden kan inte visas.",
"packagesAdded": "Tillagda paket",
"commandRun": "Kört kommando",
"executionNote": "ProxMenux körde detta på begäran; vad som ändrades avgjordes av kommandot, inte av ProxMenux.",
"unknownNote": "Detta tillämpades innan journalen fanns, så det som ersattes fångades aldrig.",
"executionNote": "ProxMenux körde detta kommando; vad det ändrade avgörs av kommandot självt.",
"unknownNote": "Tillämpades innan loggen fanns, så det tidigare tillståndet fångades inte.",
"noneInFilter": "Ingen ändring av det slaget.",
"class": {
"all": "Alla",
@@ -5901,7 +5901,11 @@
},
"operation": {
"write_file": "Fil ersatt",
"file_created": "Fil skapad",
"file_modified": "Fil ändrad",
"write_file_created": "Fil skapad",
"edit_file": "Fil redigerad",
"append_file": "Lades till i filen",
"remove_file": "Fil borttagen",
"install_package": "Installerat",
"enable_service": "Tjänst aktiverad",
@@ -5918,6 +5922,10 @@
"exact": "Återställer exakt det som fanns",
"partial": "Delvis: beroenden kan bli kvar eller följa med",
"none": "Kan inte ångras från journalen"
},
"undo": {
"remove": "Tar bort filen (det fanns ingen tidigare)",
"restore": "Återställer exakt vad som fanns"
}
},
"comparison": {
+5
View File
@@ -240,6 +240,11 @@ def diff_of(entry: dict[str, Any]) -> Optional[dict[str, Any]]:
if entry.get("class") != CLASS_CONFIGURATION:
return None
before_ref, after_ref = entry.get("before_ref"), entry.get("after_ref")
# A service enable/disable changes state, not file content: it carries
# before_state/after_state, no refs. With nothing to diff, there is no
# difference block to show — the state transition speaks for itself.
if not before_ref and not after_ref:
return None
before = read_object(before_ref) if before_ref else ""
after = read_object(after_ref) if after_ref else ""
if before is None or after is None:
+12
View File
@@ -835,6 +835,18 @@ install_normal_version() {
# Only .sh files need the executable bit. Applying +x recursively would
# also flag README.md, .json, .py etc. as executable for no reason.
find "$BASE_DIR/scripts" -type f -name '*.sh' -exec chmod +x {} +
# Register the base dependencies the installer put in place. They are
# installed in the dependency step, before this clone delivers
# pmx_journal.sh, so the journal cannot capture them as they happen —
# this records them once the engine is available. The recording side of
# pmx_journal.sh is pure bash and needs nothing else installed.
if [ -f "$BASE_DIR/scripts/global/pmx_journal.sh" ]; then
# shellcheck source=/dev/null
source "$BASE_DIR/scripts/global/pmx_journal.sh"
pmx_journal_context "install_proxmenux" "1.0"
pmx_record_applied "dialog, jq, curl, git" "1.0" "install_dependencies"
fi
chmod +x "$BASE_DIR/install_proxmenux.sh"
msg_ok "Necessary files created."
+12
View File
@@ -726,6 +726,18 @@ install_beta() {
# also flag README.md, .json, .py etc. as executable for no reason.
find "$BASE_DIR/scripts" -type f -name '*.sh' -exec chmod +x {} +
# Register the base dependencies the installer put in place. They are
# installed in the dependency step, before this clone delivers
# pmx_journal.sh, so the journal cannot capture them as they happen —
# this records them once the engine is available. The recording side of
# pmx_journal.sh is pure bash and needs nothing else installed.
if [ -f "$BASE_DIR/scripts/global/pmx_journal.sh" ]; then
# shellcheck source=/dev/null
source "$BASE_DIR/scripts/global/pmx_journal.sh"
pmx_journal_context "install_proxmenux" "1.0"
pmx_record_applied "dialog, jq, curl, git" "1.0" "install_dependencies"
fi
if [ -d "./oci" ]; then
mkdir -p "$BASE_DIR/oci"
cp -r "./oci/"* "$BASE_DIR/oci/" 2>/dev/null || true
+4 -4
View File
@@ -158,7 +158,7 @@ _pmx_journal_common() {
# EOF
pmx_write_file() {
local path="$1"
local temp before after existed="false"
local temp before="" after="" existed="false"
temp="$(mktemp)" || { cat > "$path"; return $?; }
cat > "$temp"
@@ -245,7 +245,7 @@ pmx_remove_file() {
# printf 'ulimit -n 1048576\n' | pmx_append_file /root/.profile
pmx_append_file() {
local path="$1"
local temp before after existed="false"
local temp before="" after="" existed="false"
temp="$(mktemp)" || { cat >> "$path"; return $?; }
cat > "$temp"
@@ -342,8 +342,8 @@ pmx_install_pkg() {
_pmx_service_state() {
local unit="$1"
printf '%s/%s' \
"$(systemctl is-enabled "$unit" 2>/dev/null || echo unknown)" \
"$(systemctl is-active "$unit" 2>/dev/null || echo unknown)"
"$(systemctl is-enabled "$unit" 2>/dev/null | head -1 || echo unknown)" \
"$(systemctl is-active "$unit" 2>/dev/null | head -1 || echo unknown)"
}
pmx_enable_service() {
+8 -12
View File
@@ -97,16 +97,6 @@ register_tool() {
local state="$2"
local version="${3:-1.0}"
local source="${4:-${SCRIPT_SOURCE:-unknown}}"
# Same as in the customizable script: the one call every function
# already makes, so an applied tool reaches the journal even where
# the function itself still writes directly.
if declare -F pmx_record_applied >/dev/null 2>&1; then
PMX_JOURNAL_FUNCTION="${FUNCNAME[1]:-$tool}" \
PMX_JOURNAL_VERSION="$version" \
PMX_JOURNAL_SOURCE="$source" \
pmx_record_applied "$tool" "$version" \
"$([[ "$state" == "true" ]] && echo applied || echo removed)"
fi
ensure_tools_json
if [[ "$state" == "true" ]]; then
jq --arg t "$tool" --arg ver "$version" --arg src "$source" \
@@ -1293,8 +1283,13 @@ EOF
[ "$KEEP_MB" -lt 8 ] && KEEP_MB=8
pmx_edit_file /etc/systemd/journald.conf '/^\[Journal\]/,$d' 2>/dev/null || true
pmx_append_file /etc/systemd/journald.conf <<EOF
# Compose the final file and write it once: keep everything above the
# existing [Journal] section, then our block. Editing then appending
# produced two journal entries for one file, each with half the diff;
# one write shows the whole before/after a sysadmin should read.
{
sed '/^\[Journal\]/,$d' /etc/systemd/journald.conf 2>/dev/null
cat <<EOF
[Journal]
Storage=persistent
SplitMode=none
@@ -1315,6 +1310,7 @@ MaxLevelKMsg=warning
MaxLevelConsole=notice
MaxLevelWall=crit
EOF
} | pmx_write_file /etc/systemd/journald.conf
mkdir -p /var/log/pveproxy
@@ -94,18 +94,6 @@ register_tool() {
local state="$2"
local version="${3:-1.0}"
local source="${4:-${SCRIPT_SOURCE:-unknown}}"
# Recorded here rather than in each function: this is the one call the
# whole of post-install already makes, so every applied tool reaches
# the journal even where the function itself still writes directly.
# Such an entry says what was applied and admits it cannot say what
# changed, which is the honest account for anything not yet migrated.
if declare -F pmx_record_applied >/dev/null 2>&1; then
PMX_JOURNAL_FUNCTION="${FUNCNAME[1]:-$tool}" \
PMX_JOURNAL_VERSION="$version" \
PMX_JOURNAL_SOURCE="$source" \
pmx_record_applied "$tool" "$version" \
"$([[ "$state" == "true" ]] && echo applied || echo removed)"
fi
ensure_tools_json
if [[ "$state" == "true" ]]; then
jq --arg t "$tool" --arg ver "$version" --arg src "$source" \