"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" import { ChevronDown, ChevronRight, FileCode, HelpCircle, Loader2, Package, Play, Settings2, } from "lucide-react" import { fetchApi } from "../lib/api-config" import { useT, useI18n } from "../lib/i18n/provider" /** * What ProxMenux changed on this host. * * The complaint this answers is not that the tool changes things: it is * that afterwards nobody can say what it changed. Showing the script * does not answer it either — a function of four hundred lines may alter * two values, and the reader cannot tell which two. So what is shown * here is the difference and nothing else. * * Two distinctions are kept in front of the reader, because both bear on * what they can do about what they are looking at: whether ProxMenux * authored the change or merely ran something the user asked for, and * how much of the previous state is actually known. */ interface Change { id: number recorded_at: number class: string operation: string source: string function: string function_version: string target: string before_ref: string after_ref: string capture: string revert: string exactness: string result: string recoverable: boolean detail: Record diff?: { available: boolean; reason?: string added?: number; removed?: number; truncated?: boolean; hunks?: string[] } | null } interface Summary { total: number by_class: Record functions: Array<{ function: string; source: string; version: string changes: number; last_change: number; first_change: number }> journal_started: number | null } const CLASS_STYLE: Record = { 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 }, execution: { chip: "bg-muted text-muted-foreground border-border", Icon: Play }, registration: { chip: "bg-muted text-muted-foreground border-border", Icon: HelpCircle }, } export function AuditChanges() { const t = useT() const { language } = useI18n() const [changes, setChanges] = useState([]) const [summary, setSummary] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [open, setOpen] = useState>(new Set()) const [filter, setFilter] = useState("all") const load = useCallback(async () => { try { const res: any = await fetchApi("/api/audit/changes?limit=500") if (res?.success) { setChanges(res.changes || []) setSummary(res.summary || null) setError(null) } else { setError(res?.message || t("audit.changes.failed")) } } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setLoading(false) } }, [t]) useEffect(() => { load() }, [load]) const toggle = (id: number) => setOpen((prev) => { const next = new Set(prev) next.has(id) ? next.delete(id) : next.add(id) return next }) const visible = useMemo( () => changes.filter((c) => filter === "all" || c.class === filter), [changes, filter], ) const when = (epoch: number) => new Date(epoch * 1000).toLocaleString(language) if (loading) { return (
{t("audit.changes.loading")}
) } if (error) return

{error}

return (

{t("audit.changes.intro")}

{/* A host with nothing recorded should say why, rather than looking like a host nothing has touched. */} {summary && summary.total === 0 && (

{t("audit.changes.empty")}

)} {summary && summary.journal_started && (

{t("audit.changes.since", { date: when(summary.journal_started) })}

)}
{(["all", "configuration", "installation", "execution", "registration"] as const) .filter((key) => key === "all" || summary?.by_class?.[key]) .map((key) => ( ))}
{summary && summary.functions.length > 0 && ( {t("audit.changes.byFunction")} {summary.functions.map((fn) => ( ))} )}
{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 || "") return ( {expanded && (
{t("audit.changes.function")}:{" "} {change.function || "—"} {change.function_version && ` v${change.function_version}`} {t("audit.changes.source")}:{" "} {change.source || "—"} {t("audit.changes.reversibility")}:{" "} {t(`audit.changes.exactness.${change.exactness}`)}
{installed && (

{t("audit.changes.packagesAdded")}

{installed.split(/\s+/).filter(Boolean).map((pkg) => ( {pkg} ))}
)} {change.class === "execution" && Boolean(change.detail?.command) && (

{t("audit.changes.commandRun")}

                        {String(change.detail.command)}
                      

{t("audit.changes.executionNote")}

)} {change.diff && (

{t("audit.changes.difference")}

{change.diff.available ? ( <>
                            {(change.diff.hunks || []).map((line: string, i: number) => (
                              
{line}
))}
{change.diff.truncated && (

{t("audit.changes.diffTruncated")}

)} ) : (

{t("audit.changes.diffUnavailable")}

)}
)} {change.capture === "unknown" && (

{t("audit.changes.unknownNote")}

)}
)}
) })} {visible.length === 0 && summary && summary.total > 0 && (

{t("audit.changes.noneInFilter")}

)}
) }