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)}
)}
);
}