mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
group host changes by function with a truthful before/after
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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" \
|
||||
|
||||
Reference in New Issue
Block a user