import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { copyToClipboard } from "@/lib/clipboard"; import { usePageVisibleInterval } from "@/hooks/use-page-visible-interval"; import { Input } from "@/components/input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/tooltip"; import { ArrowLeft, Check, Copy, Download, Eye, Loader2, Save, ScrollText, Search, X, } from "lucide-react"; import { toast } from "sonner"; import { getSessionLogs, getSessionLogContent, getSessionLogBlob, getSessionRecordingRetention, setSessionRecordingRetention, deleteSessionLog, type SessionLogRecord, } from "@/api/session-log-api"; import { SessionRecordingPlayer } from "@/features/session-recording/SessionRecordingPlayer"; function formatDuration(seconds: number | null): string { if (seconds == null) return "--"; if (seconds < 60) return `${seconds}s`; const m = Math.floor(seconds / 60); const s = seconds % 60; return s > 0 ? `${m}m ${s}s` : `${m}m`; } function formatBytes(bytes: number | null): string { if (bytes == null || bytes === 0) return "--"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function formatDate(iso: string): string { const d = new Date(iso); return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } function buildFilename(log: SessionLogRecord): string { const host = (log.hostName ?? log.hostIp ?? "session") .replace(/[^a-zA-Z0-9._-]/g, "_") .slice(0, 40); const d = new Date(log.startedAt); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const h = String(d.getHours()).padStart(2, "0"); const min = String(d.getMinutes()).padStart(2, "0"); const s = String(d.getSeconds()).padStart(2, "0"); const extension = log.format === "guacamole" ? "guac" : log.format === "asciicast" ? "cast" : "log"; return `${host}_${y}-${m}-${day}_${h}-${min}-${s}.${extension}`; } function SectionHeader({ label, count }: { label: string; count: number }) { return (
{label} {count}
); } function LogRow({ log, onView, onDownload, onDelete, }: { log: SessionLogRecord; onView: () => void; onDownload: () => void; onDelete: () => void; }) { const hostLabel = log.hostName ?? log.hostIp ?? `Host ${log.hostId}`; return (
{hostLabel} {formatDate(log.startedAt)} {" · "} {(log.protocol ?? "ssh").toUpperCase()} {log.username ? ` · ${log.username}` : ""} {" · "} {formatDuration(log.duration)} {" · "} {formatBytes(log.sizeBytes)}
View log Download Delete
); } export function SessionLogsPanel() { const { t } = useTranslation(); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState(""); const [viewLog, setViewLog] = useState(null); const [viewContent, setViewContent] = useState(""); const [viewBlob, setViewBlob] = useState(null); const [viewLoading, setViewLoading] = useState(false); const [deleteTarget, setDeleteTarget] = useState( null, ); const [deleting, setDeleting] = useState(false); const [copied, setCopied] = useState(false); const [retentionDays, setRetentionDays] = useState(null); const [savingRetention, setSavingRetention] = useState(false); const load = useCallback( (initial = false) => { if (initial) setLoading(true); getSessionLogs() .then((fresh) => { setLogs((prev) => { if ( prev.length === fresh.length && prev.every((log, i) => log.id === fresh[i]?.id) ) { return prev; } return fresh; }); }) .catch(() => { if (initial) toast.error(t("sessionLogs.loadError")); }) .finally(() => { if (initial) setLoading(false); }); }, [t], ); useEffect(() => { load(true); getSessionRecordingRetention() .then(setRetentionDays) .catch(() => setRetentionDays(null)); }, [load]); usePageVisibleInterval( () => { void load(false); }, 5_000, true, { runOnMount: false }, ); const q = filter.trim().toLowerCase(); const filtered = q ? logs.filter((l) => `${l.hostName ?? ""} ${l.hostIp ?? ""} ${l.username ?? ""} ${l.protocol}` .toLowerCase() .includes(q), ) : logs; const handleView = async (log: SessionLogRecord) => { setViewLog(log); setViewContent(""); setViewBlob(null); setViewLoading(true); try { if (log.format === "text") { setViewContent(await getSessionLogContent(log.id)); } else { setViewBlob(await getSessionLogBlob(log.id)); } } catch { toast.error(t("sessionLogs.loadError")); } finally { setViewLoading(false); } }; const handleDownload = async (log: SessionLogRecord) => { try { const blob = await getSessionLogBlob(log.id); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = buildFilename(log); a.click(); URL.revokeObjectURL(url); } catch { toast.error(t("sessionLogs.loadError")); } }; const handleDelete = async () => { if (!deleteTarget) return; setDeleting(true); try { await deleteSessionLog(deleteTarget.id); setLogs((prev) => prev.filter((l) => l.id !== deleteTarget.id)); setDeleteTarget(null); } catch { toast.error(t("sessionLogs.deleteError")); } finally { setDeleting(false); } }; const handleCopy = async () => { if (!viewContent) return; const ok = await copyToClipboard(viewContent); if (ok) { setCopied(true); setTimeout(() => setCopied(false), 1500); } else { toast.error(t("common.copyFailed")); } }; if (loading) { return (
); } // Inline log viewer if (viewLog) { const hostLabel = viewLog.hostName ?? viewLog.hostIp ?? `Host ${viewLog.hostId}`; return (
{/* Viewer header */}
{hostLabel} {formatDate(viewLog.startedAt)} {` · ${viewLog.protocol.toUpperCase()}`} {viewLog.duration != null ? ` · ${formatDuration(viewLog.duration)}` : ""} {" · "} {formatBytes(viewLog.sizeBytes)}
{viewLog.format === "text" && ( {copied ? t("sessionLogs.copied") : t("sessionLogs.copyContent")} )} {t("sessionLogs.downloadLog")}
{/* Log content */}
{viewLoading ? (
) : viewBlob ? ( ) : (
              {viewContent || "(empty)"}
            
)}
); } return (
{retentionDays != null && (
Recording retention setRetentionDays(Number(event.target.value))} className="h-7 w-20 text-xs" aria-label="Recording retention days" /> days
)} {logs.length === 0 ? (
{t("sessionLogs.noLogs")} {t("sessionLogs.noLogsDesc")}
) : ( <>
setFilter(e.target.value)} className="pl-8 h-7 text-xs" />
{filtered.length === 0 ? (
No results for "{filter}"
) : (
{filtered.map((log) => ( handleView(log)} onDownload={() => handleDownload(log)} onDelete={() => setDeleteTarget(log)} /> ))}
)} )}
{/* Inline delete confirmation - positioned against the relative parent in AppShell */} {deleteTarget && (

{t("sessionLogs.confirmDelete")}

{deleteTarget.hostName ?? deleteTarget.hostIp ?? `Session ${deleteTarget.id}`} {" · "} {formatDate(deleteTarget.startedAt)}

)}
); }