feat: turn session recording into a plugin (B13)

This commit is contained in:
LukeGus committed 2026-09-24 00:09:48 -05:00
1 parent a276f6898c
commit 0af2c94983
147 files changed
+25915 -2074

No files matched your search

-11
View File
@@ -86,11 +86,6 @@ const MacrosPanel = lazy(() =>
import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })),
);
const SessionLogsPanel = lazy(() =>
import("@/sidebar/SessionLogsPanel").then((m) => ({
default: m.SessionLogsPanel,
})),
);
// One screen behind both rail entries. It lazy-loads the profile and admin
// panels itself, so the shell no longer imports either directly.
const SettingsScreen = lazy(() =>
@@ -2525,12 +2520,6 @@ export function AppShell({
</div>
)}
{railView === "session-logs" && (
<div className="relative flex-1 min-h-0 flex flex-col">
<SessionLogsPanel />
</div>
)}
{(railView === "user-profile" ||
(railView === "admin-settings" && showAdminUI)) && (
<div className="flex flex-col flex-1 min-h-0">
-75
View File
@@ -1,75 +0,0 @@
import { authApi, handleApiError } from "@/main-axios";
export type SessionLogRecord = {
id: number;
hostId: number;
userId: string;
startedAt: string;
endedAt: string | null;
duration: number | null;
recordingPath: string | null;
hostName: string | null;
hostIp: string | null;
sizeBytes: number | null;
protocol: "ssh" | "rdp" | "vnc" | "telnet";
format: "text" | "asciicast" | "guacamole";
username: string | null;
};
export async function getSessionLogs(): Promise<SessionLogRecord[]> {
try {
const response = await authApi.get("/session_logs/");
return response.data.logs;
} catch (error) {
throw handleApiError(error, "fetch session logs");
}
}
export async function getSessionLogBlob(id: number): Promise<Blob> {
try {
const response = await authApi.get(`/session_logs/${id}/content`, {
responseType: "blob",
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch session recording");
}
}
export async function getSessionRecordingRetention(): Promise<number> {
try {
const response = await authApi.get("/session_logs/retention");
return response.data.retentionDays;
} catch (error) {
throw handleApiError(error, "fetch session recording retention");
}
}
export async function setSessionRecordingRetention(
retentionDays: number,
): Promise<void> {
try {
await authApi.put("/session_logs/retention", { retentionDays });
} catch (error) {
throw handleApiError(error, "update session recording retention");
}
}
export async function getSessionLogContent(id: number): Promise<string> {
try {
const response = await authApi.get(`/session_logs/${id}/content`, {
responseType: "text",
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch session log content");
}
}
export async function deleteSessionLog(id: number): Promise<void> {
try {
await authApi.delete(`/session_logs/${id}`);
} catch (error) {
throw handleApiError(error, "delete session log");
}
}
@@ -1,293 +0,0 @@
import { getErrorMessage } from "../../lib/error-message.js";
import { useCallback, useEffect, useRef, useState } from "react";
import { Pause, Play } from "lucide-react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import Guacamole from "guacamole-common-js";
import type { SessionLogRecord } from "@/api/session-log-api";
import { parseAsciicast, type Asciicast } from "./asciicast";
import "@xterm/xterm/css/xterm.css";
import { Select2 } from "@/components/select2";
const SPEEDS = [0.5, 1, 2, 4];
function formatPosition(seconds: number) {
const minutes = Math.floor(seconds / 60);
return `${minutes}:${String(Math.floor(seconds % 60)).padStart(2, "0")}`;
}
function PlaybackControls({
playing,
position,
duration,
speed,
onToggle,
onSeek,
onSpeed,
}: {
playing: boolean;
position: number;
duration: number;
speed: number;
onToggle: () => void;
onSeek: (position: number) => void;
onSpeed: (speed: number) => void;
}) {
return (
<div className="flex items-center gap-2 border-t border-border/60 bg-muted/20 px-3 py-2">
<button
type="button"
onClick={onToggle}
className="flex size-7 items-center justify-center hover:bg-muted"
aria-label={playing ? "Pause recording" : "Play recording"}
>
{playing ? (
<Pause className="size-3.5" />
) : (
<Play className="size-3.5" />
)}
</button>
<span className="w-10 text-[10px] tabular-nums text-muted-foreground">
{formatPosition(position)}
</span>
<input
type="range"
min={0}
max={Math.max(duration, 0.01)}
step={0.01}
value={Math.min(position, duration)}
onChange={(event) => onSeek(Number(event.target.value))}
className="min-w-0 flex-1 accent-primary"
aria-label="Recording timeline"
/>
<span className="w-10 text-right text-[10px] tabular-nums text-muted-foreground">
{formatPosition(duration)}
</span>
<Select2
value={speed}
onChange={(event) => onSpeed(Number(event.target.value))}
className="h-7 border border-border bg-background px-1 text-[10px]"
aria-label="Playback speed"
>
{SPEEDS.map((value) => (
<option key={value} value={value}>
{value}×
</option>
))}
</Select2>
</div>
);
}
function AsciicastPlayer({ blob }: { blob: Blob }) {
const containerRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal | null>(null);
const recordingRef = useRef<Asciicast | null>(null);
const positionRef = useRef(0);
const eventIndexRef = useRef(0);
const [position, setPosition] = useState(0);
const [duration, setDuration] = useState(0);
const [speed, setSpeed] = useState(1);
const [playing, setPlaying] = useState(false);
const [error, setError] = useState("");
const renderAt = useCallback((nextPosition: number) => {
const terminal = terminalRef.current;
const recording = recordingRef.current;
if (!terminal || !recording) return;
if (nextPosition < positionRef.current) {
terminal.reset();
terminal.resize(recording.width, recording.height);
eventIndexRef.current = 0;
}
while (eventIndexRef.current < recording.events.length) {
const [time, type, data] = recording.events[eventIndexRef.current];
if (time > nextPosition) break;
if (type === "o") terminal.write(data);
if (type === "r") {
const [cols, rows] = data.split("x").map(Number);
if (cols > 0 && rows > 0) terminal.resize(cols, rows);
}
eventIndexRef.current++;
}
positionRef.current = nextPosition;
setPosition(nextPosition);
}, []);
useEffect(() => {
if (!containerRef.current) return;
const terminal = new Terminal({
cursorBlink: false,
disableStdin: true,
convertEol: false,
fontSize: 12,
theme: { background: "#09090b" },
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(containerRef.current);
terminalRef.current = terminal;
const observer = new ResizeObserver(() => fitAddon.fit());
observer.observe(containerRef.current);
fitAddon.fit();
blob
.text()
.then((source) => {
const recording = parseAsciicast(source);
recordingRef.current = recording;
setDuration(recording.duration);
terminal.resize(recording.width, recording.height);
renderAt(0);
})
.catch((reason) => setError(getErrorMessage(reason, String(reason))));
return () => {
observer.disconnect();
terminal.dispose();
terminalRef.current = null;
};
}, [blob, renderAt]);
useEffect(() => {
if (!playing || !recordingRef.current) return;
let frame = 0;
let previous = performance.now();
const tick = (now: number) => {
const next = Math.min(
duration,
positionRef.current + ((now - previous) / 1000) * speed,
);
previous = now;
renderAt(next);
if (next >= duration) setPlaying(false);
else frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [duration, playing, renderAt, speed]);
if (error) return <div className="p-4 text-xs text-destructive">{error}</div>;
return (
<div className="flex h-full min-h-0 flex-col">
<div ref={containerRef} className="min-h-0 flex-1 bg-[#09090b] p-2" />
<PlaybackControls
playing={playing}
position={position}
duration={duration}
speed={speed}
onToggle={() => {
if (!playing && position >= duration) renderAt(0);
setPlaying((value) => !value);
}}
onSeek={renderAt}
onSpeed={setSpeed}
/>
</div>
);
}
function GuacamolePlayer({ blob }: { blob: Blob }) {
const containerRef = useRef<HTMLDivElement>(null);
const recordingRef = useRef<Guacamole.SessionRecording | null>(null);
const positionRef = useRef(0);
const [position, setPosition] = useState(0);
const [duration, setDuration] = useState(0);
const [speed, setSpeed] = useState(1);
const [playing, setPlaying] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!containerRef.current) return;
const recording = new Guacamole.SessionRecording(blob, 100);
recordingRef.current = recording;
const display = recording.getDisplay();
const element = display.getElement();
containerRef.current.replaceChildren(element);
const fit = () => {
const width = display.getWidth();
if (width && containerRef.current) {
display.scale(containerRef.current.clientWidth / width);
}
};
const observer = new ResizeObserver(fit);
observer.observe(containerRef.current);
recording.onload = () => {
setDuration(recording.getDuration() / 1000);
fit();
};
recording.onprogress = (nextDuration) => setDuration(nextDuration / 1000);
recording.onseek = (nextPosition) => {
positionRef.current = nextPosition / 1000;
setPosition(positionRef.current);
};
recording.onerror = setError;
return () => {
observer.disconnect();
recording.abort();
recordingRef.current = null;
};
}, [blob]);
useEffect(() => {
const recording = recordingRef.current;
if (!recording || !playing) return;
recording.pause();
let previous = performance.now();
let nextPosition = positionRef.current;
const interval = window.setInterval(() => {
const now = performance.now();
nextPosition = Math.min(
duration,
nextPosition + ((now - previous) / 1000) * speed,
);
previous = now;
recording.seek(nextPosition * 1000);
if (nextPosition >= duration) setPlaying(false);
}, 100);
return () => clearInterval(interval);
}, [duration, playing, speed]);
if (error) return <div className="p-4 text-xs text-destructive">{error}</div>;
return (
<div className="flex h-full min-h-0 flex-col">
<div
ref={containerRef}
className="min-h-0 flex-1 overflow-hidden bg-black"
/>
<PlaybackControls
playing={playing}
position={position}
duration={duration}
speed={speed}
onToggle={() => {
const recording = recordingRef.current;
if (!recording) return;
if (!playing && position >= duration) recording.seek(0);
setPlaying((value) => !value);
}}
onSeek={(nextPosition) => {
recordingRef.current?.seek(nextPosition * 1000);
positionRef.current = nextPosition;
setPosition(nextPosition);
}}
onSpeed={setSpeed}
/>
</div>
);
}
export function SessionRecordingPlayer({
log,
blob,
}: {
log: SessionLogRecord;
blob: Blob;
}) {
return log.format === "guacamole" ? (
<GuacamolePlayer blob={blob} />
) : (
<AsciicastPlayer blob={blob} />
);
}
@@ -1,78 +0,0 @@
import { Terminal } from "@xterm/xterm";
export type AsciicastEvent = [
time: number,
type: "i" | "o" | "r",
data: string,
];
export type Asciicast = {
width: number;
height: number;
duration: number;
events: AsciicastEvent[];
};
export function parseAsciicast(source: string): Asciicast {
const lines = source.trim().split("\n");
const header = JSON.parse(lines.shift() || "{}") as {
version?: number;
width?: number;
height?: number;
};
if (header.version !== 2) throw new Error("Unsupported asciicast version");
const events = lines
.filter(Boolean)
.map((line) => JSON.parse(line) as AsciicastEvent)
.filter(
([time, type, data]) =>
Number.isFinite(time) &&
(type === "i" || type === "o" || type === "r") &&
typeof data === "string",
);
return {
width: header.width || 80,
height: header.height || 24,
duration: events.at(-1)?.[0] || 0,
events,
};
}
function writeAsync(terminal: Terminal, data: string): Promise<void> {
return new Promise((resolve) => terminal.write(data, resolve));
}
// Replays the recording into an offscreen terminal and reads back its
// rendered buffer, so cursor movement/overwrites resolve the same way
// they would on screen instead of leaving raw ANSI escapes in the text.
export async function asciicastToPlainText(
recording: Asciicast,
): Promise<string> {
const terminal = new Terminal({
cols: recording.width,
rows: recording.height,
allowProposedApi: true,
});
try {
for (const [, type, data] of recording.events) {
if (type === "o") await writeAsync(terminal, data);
if (type === "r") {
const [cols, rows] = data.split("x").map(Number);
if (cols > 0 && rows > 0) terminal.resize(cols, rows);
}
}
const buffer = terminal.buffer.active;
const lines: string[] = [];
for (let i = 0; i < buffer.length; i++) {
lines.push(buffer.getLine(i)?.translateToString(true) ?? "");
}
while (lines.length > 0 && lines.at(-1) === "") lines.pop();
return lines.join("\n");
} finally {
terminal.dispose();
}
}
+4 -20
View File
@@ -692,7 +692,6 @@
"quickConnect": "Quick Connect",
"sshTools": "SSH Tools",
"history": "History",
"sessionLogs": "Session Logs",
"openAsTabHint": "Ctrl+click or middle click to open as a tab",
"openAsTab": "Open as tab",
"openInRightDock": "Open in right dock",
@@ -1029,8 +1028,6 @@
"enableAutoMoshDesc": "Prefer Mosh over SSH if available",
"enableAutoTmux": "Enable Auto-Tmux",
"enableAutoTmuxDesc": "Keep your shell and anything running in it alive across disconnects and browser restarts by running inside tmux (tmux must be installed on the host)",
"enableSessionLogging": "Session Logging",
"enableSessionLoggingDesc": "Record terminal session output for later review",
"localEchoLabel": "Local Echo",
"localEchoDesc": "Predict safe printable keystrokes locally on slow connections. Password prompts and control input are never echoed.",
"localEchoDefault": "Use global setting",
@@ -1979,23 +1976,6 @@
"disk": "Disk",
"viewServerDetails": "View server details"
},
"sessionLogs": {
"title": "Session Logs",
"noLogs": "No session logs yet",
"noLogsDesc": "Enable session logging on a host to start recording",
"duration": "Duration",
"viewLog": "View log",
"downloadLog": "Download",
"downloadAsText": "Download as text",
"deleteLog": "Delete",
"confirmDelete": "Delete this session log?",
"confirmDeleteDesc": "This action cannot be undone.",
"copyContent": "Copy",
"copied": "Copied!",
"loadError": "Failed to load session logs",
"deleteError": "Failed to delete session log",
"filterByHost": "Filter by host..."
},
"admin": {
"sectionGeneral": "General",
"sectionOidc": "OIDC",
@@ -3151,6 +3131,10 @@
"title": "Remember small settings",
"consequence": "Kept separate from other plugins."
},
"files:own": {
"title": "Store its own files",
"consequence": "In its own folder on disk, separate from other plugins."
},
"secrets:own": {
"title": "Store its own secrets",
"consequence": "Encrypted by Termix. The plugin never holds the key."
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Vinnige verbinding",
"sshTools": "SSH-gereedskap",
"history": "Geskiedenis",
"sessionLogs": "Sessielogboeke",
"openAsTabHint": "Ctrl+klik of middelklik om as 'n oortjie oop te maak",
"openAsTab": "Maak oop as oortjie",
"openInRightDock": "Maak oop in regterdok",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Verkies Mosh bo SSH indien beskikbaar",
"enableAutoTmux": "Aktiveer Auto-Tmux",
"enableAutoTmuxDesc": "Hou jou dop en enigiets wat daarin loop lewendig oor ontkoppelings en herbegin van blaaiers deur dit binne tmux te laat loop (tmux moet op die gasheer geïnstalleer wees)",
"enableSessionLogging": "Sessie-logging",
"enableSessionLoggingDesc": "Neem terminaalsessie-uitvoer op vir latere hersiening",
"localEchoLabel": "Plaaslike Eggo",
"localEchoDesc": "Voorspel veilige drukbare toetsaanslagen plaaslik op stadige verbindings. Wagwoordaanwysings en beheerinvoer word nooit herhaal nie.",
"localEchoDefault": "Gebruik globale instelling",
@@ -1969,23 +1966,6 @@
"disk": "Skyf",
"viewServerDetails": "Bekyk bedienerbesonderhede"
},
"sessionLogs": {
"title": "Sessielogboeke",
"noLogs": "Geen sessielogboeke nog nie",
"noLogsDesc": "Aktiveer sessie-aanmelding op 'n gasheer om opname te begin",
"duration": "Duur",
"viewLog": "Bekyk logboek",
"downloadLog": "Laai af",
"downloadAsText": "Laai af as teks",
"deleteLog": "Vee uit",
"confirmDelete": "Vee hierdie sessielog uit?",
"confirmDeleteDesc": "Hierdie aksie kan nie ongedaan gemaak word nie.",
"copyContent": "Kopieer",
"copied": "Gekopieer!",
"loadError": "Kon nie sessielogboeke laai nie",
"deleteError": "Kon nie sessielogboek uitvee nie",
"filterByHost": "Filtreer volgens gasheer..."
},
"admin": {
"sectionGeneral": "Algemeen",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "اتصال سريع",
"sshTools": "أدوات SSH",
"history": "السجل",
"sessionLogs": "سجلات الجلسات",
"openAsTabHint": "اضغط على مفتاح Ctrl + انقر أو انقر بالزر الأوسط لفتحها كعلامة تبويب",
"openAsTab": "افتح كعلامة تبويب",
"openInRightDock": "افتح في الرصيف الأيمن",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "تفضيل Mosh على SSH إذا كان متاحاً",
"enableAutoTmux": "تمكين tmux التلقائي",
"enableAutoTmuxDesc": "حافظ على تشغيل shell وأي شيء يعمل بداخله أثناء انقطاع الاتصال وإعادة تشغيل المتصفح عن طريق التشغيل داخل tmux (يجب تثبيت tmux على المضيف).",
"enableSessionLogging": "تسجيل الجلسات",
"enableSessionLoggingDesc": "تسجيل مخرجات الجلسة الطرفية للمراجعة لاحقاً",
"localEchoLabel": "صدى محلي",
"localEchoDesc": "توقع ضغطات المفاتيح الآمنة القابلة للطباعة محليًا على الاتصالات البطيئة. لا يتم عرض مطالبات كلمة المرور أو مدخلات التحكم على الشاشة.",
"localEchoDefault": "استخدم الإعدادات العامة",
@@ -1969,23 +1966,6 @@
"disk": "القرص",
"viewServerDetails": "عرض تفاصيل الخادم"
},
"sessionLogs": {
"title": "سجلات الجلسات",
"noLogs": "لا توجد سجلات جلسات بعد",
"noLogsDesc": "فعّل تسجيل الجلسات على مضيف لبدء التسجيل",
"duration": "المدة",
"viewLog": "عرض السجل",
"downloadLog": "تنزيل",
"downloadAsText": "تنزيل كنص",
"deleteLog": "حذف",
"confirmDelete": "حذف سجل الجلسة هذا؟",
"confirmDeleteDesc": "لا يمكن التراجع عن هذا الإجراء.",
"copyContent": "نسخ",
"copied": "تم النسخ!",
"loadError": "فشل في تحميل سجلات الجلسات",
"deleteError": "فشل في حذف سجل الجلسة",
"filterByHost": "تصفية حسب المضيف..."
},
"admin": {
"sectionGeneral": "عام",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Бързо свързване",
"sshTools": "SSH инструменти",
"history": "История",
"sessionLogs": "Дневници на сесиите",
"openAsTabHint": "Ctrl+щракване или щракване със среден бутон, за да отворите като раздел",
"openAsTab": "Отвори като раздел",
"openInRightDock": "Отваряне в десния док",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Предпочитайте Mosh пред SSH, ако е наличен",
"enableAutoTmux": "Активиране на Auto-Tmux",
"enableAutoTmuxDesc": "Поддържайте вашата обвивка и всичко, което се изпълнява в нея, активно при прекъсвания на връзката и рестартиране на браузъра, като я стартирате вътре в tmux (tmux трябва да е инсталиран на хоста)",
"enableSessionLogging": "Записване на сесии",
"enableSessionLoggingDesc": "Записване на изхода от терминалната сесия за по-късен преглед",
"localEchoLabel": "Локално ехо",
"localEchoDesc": "Предсказвайте безопасни за печат натискания на клавиши локално при бавни връзки. Подканите за парола и контролните въведени команди никога не се повтарят.",
"localEchoDefault": "Използвайте глобална настройка",
@@ -1969,23 +1966,6 @@
"disk": "Диск",
"viewServerDetails": "Преглед на подробности за сървъра"
},
"sessionLogs": {
"title": "Дневници на сесии",
"noLogs": "Все още няма логове на сесии",
"noLogsDesc": "Активирайте регистрирането на сесията на хост, за да започнете записа",
"duration": "Продължителност",
"viewLog": "Преглед на дневника",
"downloadLog": "Изтегляне",
"downloadAsText": "Изтегляне като текст",
"deleteLog": "Изтриване",
"confirmDelete": "Да се изтрие ли този дневник на сесията?",
"confirmDeleteDesc": "Това действие не може да бъде отменено.",
"copyContent": "Копиране",
"copied": "Копирано!",
"loadError": "Зареждането на лог файловете на сесията не бе успешно",
"deleteError": "Неуспешно изтриване на дневника на сесията",
"filterByHost": "Филтриране по хост..."
},
"admin": {
"sectionGeneral": "Общи",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "দ্রুত সংযোগ",
"sshTools": "SSH টুলস",
"history": "ইতিহাস",
"sessionLogs": "সেশন লগ",
"openAsTabHint": "ট্যাব হিসেবে খুলতে Ctrl+ক্লিক অথবা মিডল ক্লিক করুন।",
"openAsTab": "ট্যাব হিসাবে খুলুন",
"openInRightDock": "ডান ডকে খুলুন",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "উপলব্ধ থাকলে SSH-এর চেয়ে Mosh ব্যবহার করা শ্রেয়।",
"enableAutoTmux": "অটো-Tmux সক্রিয় করুন",
"enableAutoTmuxDesc": "সংযোগ বিচ্ছিন্ন হওয়া এবং ব্রাউজার পুনরায় চালু হওয়ার পরেও আপনার শেল এবং এর মধ্যে চলমান সবকিছু সচল রাখতে tmux-এর ভিতরে এটি চালান (tmux অবশ্যই হোস্টে ইনস্টল করা থাকতে হবে)।",
"enableSessionLogging": "সেশন লগিং",
"enableSessionLoggingDesc": "পরবর্তী পর্যালোচনার জন্য টার্মিনাল সেশনের আউটপুট রেকর্ড করুন।",
"localEchoLabel": "স্থানীয় প্রতিধ্বনি",
"localEchoDesc": "ধীরগতির সংযোগে স্থানীয়ভাবে নিরাপদ ও মুদ্রণযোগ্য কীস্ট্রোক অনুমান করুন। পাসওয়ার্ড প্রম্পট এবং কন্ট্রোল ইনপুট কখনও প্রতিধ্বনিত হয় না।",
"localEchoDefault": "গ্লোবাল সেটিং ব্যবহার করুন",
@@ -1969,23 +1966,6 @@
"disk": "ডিস্ক",
"viewServerDetails": "সার্ভারের বিবরণ দেখুন"
},
"sessionLogs": {
"title": "সেশন লগ",
"noLogs": "এখনো কোনো সেশন লগ নেই",
"noLogsDesc": "রেকর্ডিং শুরু করতে কোনো হোস্টে সেশন লগিং চালু করুন।",
"duration": "সময়কাল",
"viewLog": "লগ দেখুন",
"downloadLog": "ডাউনলোড করুন",
"downloadAsText": "টেক্সট হিসেবে ডাউনলোড করুন",
"deleteLog": "মুছে ফেলুন",
"confirmDelete": "এই সেশন লগটি মুছে ফেলবেন?",
"confirmDeleteDesc": "এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
"copyContent": "অনুলিপি",
"copied": "অনুলিপি করা হয়েছে!",
"loadError": "সেশন লগ লোড করতে ব্যর্থ হয়েছে",
"deleteError": "সেশন লগ মুছে ফেলতে ব্যর্থ হয়েছে",
"filterByHost": "হোস্ট দ্বারা ফিল্টার করুন..."
},
"admin": {
"sectionGeneral": "সাধারণ",
"sectionOidc": "ওআইডিসি",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Connexió ràpida",
"sshTools": "Eines SSH",
"history": "Història",
"sessionLogs": "Registres de sessió",
"openAsTabHint": "Ctrl+clic o clic amb el botó del mig per obrir com a pestanya",
"openAsTab": "Obre com a pestanya",
"openInRightDock": "Obre al moll dret",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Prefereix Mosh a SSH si està disponible",
"enableAutoTmux": "Activa la transmissió automàtica",
"enableAutoTmuxDesc": "Mantingueu actiu el vostre shell i tot el que s'hi executi després de desconnexions i reinicis del navegador executant-lo dins de tmux (tmux ha d'estar instal·lat a l'amfitrió)",
"enableSessionLogging": "Registre de sessions",
"enableSessionLoggingDesc": "Enregistra la sortida de la sessió de terminal per a una revisió posterior",
"localEchoLabel": "Eco local",
"localEchoDesc": "Prediu localment les pulsacions de tecles imprimibles de manera segura en connexions lentes. Les sol·licituds de contrasenya i l'entrada de control no es reprodueixen mai.",
"localEchoDefault": "Utilitza la configuració global",
@@ -1969,23 +1966,6 @@
"disk": "Disc",
"viewServerDetails": "Veure els detalls del servidor"
},
"sessionLogs": {
"title": "Registres de sessió",
"noLogs": "Encara no hi ha registres de sessió",
"noLogsDesc": "Habilita el registre de sessions en un amfitrió per iniciar l'enregistrament",
"duration": "Durada",
"viewLog": "Veure registre",
"downloadLog": "Descarrega",
"downloadAsText": "Descarrega com a text",
"deleteLog": "Suprimeix",
"confirmDelete": "Voleu suprimir aquest registre de sessió?",
"confirmDeleteDesc": "Aquesta acció no es pot desfer.",
"copyContent": "Còpia",
"copied": "Copiat!",
"loadError": "No s'han pogut carregar els registres de sessió",
"deleteError": "No s'ha pogut suprimir el registre de sessió",
"filterByHost": "Filtra per amfitrió..."
},
"admin": {
"sectionGeneral": "General",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Rychlé připojení",
"sshTools": "SSH nástroje",
"history": "Dějiny",
"sessionLogs": "Protokoly relací",
"openAsTabHint": "Ctrl+kliknutí nebo prostřední kliknutí pro otevření jako záložky",
"openAsTab": "Otevřít jako kartu",
"openInRightDock": "Otevřít v pravém doku",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Pokud je k dispozici, preferujte Mosh před SSH.",
"enableAutoTmux": "Povolit Auto-Tmux",
"enableAutoTmuxDesc": "Udržujte svůj shell a vše, co v něm běží, aktivní i po odpojení a restartu prohlížeče spuštěním uvnitř tmuxu (tmux musí být nainstalován na hostiteli).",
"enableSessionLogging": "Protokolování relací",
"enableSessionLoggingDesc": "Zaznamenejte výstup terminálové relace pro pozdější kontrolu",
"localEchoLabel": "Místní ozvěna",
"localEchoDesc": "Předvídat bezpečně tisknutelné stisky kláves lokálně na pomalém připojení. Výzvy k zadání hesla a ovládací prvky se nikdy nezobrazují.",
"localEchoDefault": "Použít globální nastavení",
@@ -1969,23 +1966,6 @@
"disk": "Disk",
"viewServerDetails": "Zobrazit podrobnosti o serveru"
},
"sessionLogs": {
"title": "Protokoly relací",
"noLogs": "Zatím žádné protokoly relací",
"noLogsDesc": "Povolte protokolování relace na hostiteli pro zahájení nahrávání",
"duration": "Trvání",
"viewLog": "Zobrazit protokol",
"downloadLog": "Stáhnout",
"downloadAsText": "Stáhnout jako text",
"deleteLog": "Vymazat",
"confirmDelete": "Smazat tento protokol relace?",
"confirmDeleteDesc": "Tuto akci nelze vrátit zpět.",
"copyContent": "Kopie",
"copied": "Zkopírováno!",
"loadError": "Nepodařilo se načíst protokoly relací",
"deleteError": "Nepodařilo se smazat protokol relace",
"filterByHost": "Filtrovat podle hostitele..."
},
"admin": {
"sectionGeneral": "Generál",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Hurtig forbindelse",
"sshTools": "SSH-værktøjer",
"history": "Historie",
"sessionLogs": "Sessionslogfiler",
"openAsTabHint": "Ctrl+klik eller midterste klik for at åbne som faneblad",
"openAsTab": "Åbn som faneblad",
"openInRightDock": "Åbn i højre dock",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Foretræk Mosh frem for SSH, hvis det er muligt",
"enableAutoTmux": "Aktivér Auto-Tmux",
"enableAutoTmuxDesc": "Hold din shell og alt, der kører i den, i live på tværs af afbrydelser og genstart af browseren ved at køre den inde i tmux (tmux skal være installeret på værten)",
"enableSessionLogging": "Sessionslogning",
"enableSessionLoggingDesc": "Optag output fra terminalsessionen til senere gennemgang",
"localEchoLabel": "Lokalt ekko",
"localEchoDesc": "Forudsig sikre, udskrivbare tastetryk lokalt på langsomme forbindelser. Adgangskodeprompter og kontrolinput gentages aldrig.",
"localEchoDefault": "Brug global indstilling",
@@ -1969,23 +1966,6 @@
"disk": "Disk",
"viewServerDetails": "Se serveroplysninger"
},
"sessionLogs": {
"title": "Sessionslogfiler",
"noLogs": "Ingen sessionslogfiler endnu",
"noLogsDesc": "Aktivér sessionslogning på en vært for at starte optagelse",
"duration": "Varighed",
"viewLog": "Se log",
"downloadLog": "Download",
"downloadAsText": "Download som tekst",
"deleteLog": "Slet",
"confirmDelete": "Slet denne sessionslog?",
"confirmDeleteDesc": "Denne handling kan ikke fortrydes.",
"copyContent": "Kopi",
"copied": "Kopieret!",
"loadError": "Kunne ikke indlæse sessionslogfiler",
"deleteError": "Kunne ikke slette sessionsloggen",
"filterByHost": "Filtrer efter vært..."
},
"admin": {
"sectionGeneral": "Generel",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Schnellverbindung",
"sshTools": "SSH-Tools",
"history": "Verlauf",
"sessionLogs": "Sitzungsprotokolle",
"openAsTabHint": "Strg+Klick oder mittlerer Klick zum Öffnen als Tab",
"openAsTab": "Als Tab öffnen",
"openInRightDock": "Im rechten Dock öffnen",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Mosh gegenüber SSH bevorzugen, falls verfügbar",
"enableAutoTmux": "Auto-Tmux aktivieren",
"enableAutoTmuxDesc": "Halten Sie Ihre Shell und alle darin laufenden Prozesse auch bei Verbindungsabbrüchen und Browserneustarts am Laufen, indem Sie den Befehl innerhalb von tmux ausführen (tmux muss auf dem Host installiert sein).",
"enableSessionLogging": "Sitzungsprotokollierung",
"enableSessionLoggingDesc": "Terminal-Sitzungsausgabe zur späteren Überprüfung aufzeichnen",
"localEchoLabel": "Lokales Echo",
"localEchoDesc": "Bei langsamen Verbindungen werden sichere, druckbare Tastatureingaben lokal vorhergesagt. Passwortabfragen und Steuereingaben werden niemals wiederholt.",
"localEchoDefault": "Globale Einstellung verwenden",
@@ -1969,23 +1966,6 @@
"disk": "Datenträger",
"viewServerDetails": "Serverdetails anzeigen"
},
"sessionLogs": {
"title": "Sitzungsprotokolle",
"noLogs": "Noch keine Sitzungsprotokolle",
"noLogsDesc": "Aktivieren Sie die Sitzungsprotokollierung auf einem Host, um die Aufzeichnung zu starten",
"duration": "Dauer",
"viewLog": "Protokoll anzeigen",
"downloadLog": "Herunterladen",
"downloadAsText": "Als Text herunterladen",
"deleteLog": "Löschen",
"confirmDelete": "Dieses Sitzungsprotokoll löschen?",
"confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.",
"copyContent": "Kopieren",
"copied": "Kopiert!",
"loadError": "Fehler beim Laden der Sitzungsprotokolle",
"deleteError": "Fehler beim Löschen des Sitzungsprotokolls",
"filterByHost": "Nach Host filtern..."
},
"admin": {
"sectionGeneral": "Allgemein",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Γρήγορη σύνδεση",
"sshTools": "Εργαλεία SSH",
"history": "Ιστορία",
"sessionLogs": "Αρχεία καταγραφής συνεδρίας",
"openAsTabHint": "Ctrl+κλικ ή μεσαίο κλικ για άνοιγμα ως καρτέλα",
"openAsTab": "Άνοιγμα ως καρτέλα",
"openInRightDock": "Άνοιγμα στη δεξιά βάση σύνδεσης",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Προτιμήστε το Mosh αντί για SSH, εάν είναι διαθέσιμο",
"enableAutoTmux": "Ενεργοποίηση Auto-Tmux",
"enableAutoTmuxDesc": "Διατηρήστε το κέλυφος (shell) και οτιδήποτε εκτελείται σε αυτό ενεργό σε αποσυνδέσεις και επανεκκινήσεις του προγράμματος περιήγησης, εκτελώντας το μέσα στο tmux (το tmux πρέπει να είναι εγκατεστημένο στον κεντρικό υπολογιστή)",
"enableSessionLogging": "Καταγραφή συνεδρίας",
"enableSessionLoggingDesc": "Καταγραφή εξόδου συνεδρίας τερματικού για μελλοντική αναθεώρηση",
"localEchoLabel": "Τοπική Ηχώ",
"localEchoDesc": "Προβλέψτε ασφαλείς εκτυπώσιμες πληκτρολογήσεις τοπικά σε αργές συνδέσεις. Οι προτροπές κωδικού πρόσβασης και η εισαγωγή ελέγχου δεν επαναλαμβάνονται ποτέ.",
"localEchoDefault": "Χρήση καθολικής ρύθμισης",
@@ -1969,23 +1966,6 @@
"disk": "Δίσκος",
"viewServerDetails": "Προβολή λεπτομερειών διακομιστή"
},
"sessionLogs": {
"title": "Αρχεία καταγραφής συνεδρίας",
"noLogs": "Δεν υπάρχουν ακόμη αρχεία καταγραφής συνεδρίας",
"noLogsDesc": "Ενεργοποίηση καταγραφής περιόδου λειτουργίας σε έναν κεντρικό υπολογιστή για έναρξη εγγραφής",
"duration": "Διάρκεια",
"viewLog": "Προβολή αρχείου καταγραφής",
"downloadLog": "Λήψη",
"downloadAsText": "Λήψη ως κείμενο",
"deleteLog": "Διαγράφω",
"confirmDelete": "Διαγραφή αυτού του αρχείου καταγραφής περιόδου σύνδεσης;",
"confirmDeleteDesc": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"copyContent": "Αντίγραφο",
"copied": "Αντιγράφηκε!",
"loadError": "Αποτυχία φόρτωσης αρχείων καταγραφής περιόδου λειτουργίας",
"deleteError": "Η διαγραφή του αρχείου καταγραφής περιόδου λειτουργίας απέτυχε",
"filterByHost": "Φιλτράρισμα κατά κεντρικό υπολογιστή..."
},
"admin": {
"sectionGeneral": "Γενικός",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Conexión rápida",
"sshTools": "Herramientas SSH",
"history": "Historial",
"sessionLogs": "Registros de sesión",
"openAsTabHint": "Ctrl+clic o clic central para abrir como pestaña",
"openAsTab": "Abrir como pestaña",
"openInRightDock": "Abrir en el muelle derecho",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Preferir Mosh sobre SSH si está disponible",
"enableAutoTmux": "Habilitar Auto-Tmux",
"enableAutoTmuxDesc": "Mantén tu shell y todo lo que se esté ejecutando en él activo durante desconexiones y reinicios del navegador ejecutándolo dentro de tmux (tmux debe estar instalado en el host).",
"enableSessionLogging": "Registro de sesión",
"enableSessionLoggingDesc": "Registrar la salida de la sesión de terminal para su revisión posterior",
"localEchoLabel": "Eco local",
"localEchoDesc": "Predice localmente las pulsaciones de teclas seguras para imprimir en conexiones lentas. Las solicitudes de contraseña y la entrada de control nunca se repiten.",
"localEchoDefault": "Utilizar la configuración global",
@@ -1969,23 +1966,6 @@
"disk": "Disco",
"viewServerDetails": "Ver detalles del servidor"
},
"sessionLogs": {
"title": "Registros de sesión",
"noLogs": "Aún no hay registros de sesión",
"noLogsDesc": "Activa el registro de sesiones en un host para empezar a grabar",
"duration": "Duración",
"viewLog": "Ver registro",
"downloadLog": "Descargar",
"downloadAsText": "Descargar como texto",
"deleteLog": "Eliminar",
"confirmDelete": "¿Eliminar este registro de sesión?",
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
"copyContent": "Copiar",
"copied": "¡Copiado!",
"loadError": "Error al cargar los registros de sesión",
"deleteError": "Error al eliminar el registro de sesión",
"filterByHost": "Filtrar por host..."
},
"admin": {
"sectionGeneral": "General",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Pikayhteys",
"sshTools": "SSH-työkalut",
"history": "Historia",
"sessionLogs": "Istuntolokit",
"openAsTabHint": "Ctrl+napsauta tai keskinäppäinpainike avaa välilehdenä",
"openAsTab": "Avaa välilehtenä",
"openInRightDock": "Avaa oikeanpuoleisessa telakassa",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Suosi Moshia SSH:n sijaan, jos saatavilla",
"enableAutoTmux": "Ota automaattinen Tmux käyttöön",
"enableAutoTmuxDesc": "Pidä komentotulkkisi ja kaikki siinä käynnissä olevat toiminnot toiminnassa yhteyskatkosten ja selaimen uudelleenkäynnistysten aikana suorittamalla se tmuxin sisällä (tmuxin on oltava asennettuna isäntäkoneelle)",
"enableSessionLogging": "Istuntojen lokitiedot",
"enableSessionLoggingDesc": "Tallenna pääteistunnon tuloste myöhempää tarkastelua varten",
"localEchoLabel": "Paikallinen kaiku",
"localEchoDesc": "Ennusta turvalliset tulostettavat näppäinpainallukset paikallisesti hitailla yhteyksillä. Salasanakehotteita ja ohjaussyötettä ei koskaan toisteta.",
"localEchoDefault": "Käytä yleistä asetusta",
@@ -1969,23 +1966,6 @@
"disk": "Levy",
"viewServerDetails": "Näytä palvelimen tiedot"
},
"sessionLogs": {
"title": "Istuntolokit",
"noLogs": "Ei vielä istuntolokeja",
"noLogsDesc": "Ota istunnon lokikirjaus käyttöön isännässä aloittaaksesi tallennuksen",
"duration": "Kesto",
"viewLog": "Näytä loki",
"downloadLog": "Lataa",
"downloadAsText": "Lataa tekstinä",
"deleteLog": "Poistaa",
"confirmDelete": "Poistetaanko tämä istuntoloki?",
"confirmDeleteDesc": "Tätä toimintoa ei voi perua.",
"copyContent": "Kopioida",
"copied": "Kopioitu!",
"loadError": "Istuntolokien lataaminen epäonnistui",
"deleteError": "Istuntolokitietojen poistaminen epäonnistui",
"filterByHost": "Suodata isännän mukaan..."
},
"admin": {
"sectionGeneral": "Yleistä",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Connexion rapide",
"sshTools": "Outils SSH",
"history": "Historique",
"sessionLogs": "Journaux de session",
"openAsTabHint": "Ctrl+clic ou clic du milieu pour ouvrir dans un onglet",
"openAsTab": "Ouvrir comme onglet",
"openInRightDock": "Ouvrir dans le dock de droite",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Préférer Mosh à SSH si disponible",
"enableAutoTmux": "Activer Auto-Tmux",
"enableAutoTmuxDesc": "Pour que votre shell et tout ce qui y est exécuté restent actifs malgré les déconnexions et les redémarrages du navigateur, exécutez la commande dans tmux (tmux doit être installé sur l'hôte).",
"enableSessionLogging": "Journalisation de session",
"enableSessionLoggingDesc": "Enregistrer la sortie de la session terminal pour une consultation ultérieure",
"localEchoLabel": "Écho local",
"localEchoDesc": "Prédiction locale des frappes au clavier imprimables en toute sécurité, même sur les connexions lentes. Les invites de mot de passe et les entrées de contrôle ne sont jamais affichées.",
"localEchoDefault": "Utiliser les paramètres globaux",
@@ -1969,23 +1966,6 @@
"disk": "Disque",
"viewServerDetails": "Afficher les détails du serveur"
},
"sessionLogs": {
"title": "Journaux de session",
"noLogs": "Aucun journal de session pour l’instant",
"noLogsDesc": "Activez la journalisation des sessions sur un hôte pour démarrer l’enregistrement",
"duration": "Durée",
"viewLog": "Afficher le journal",
"downloadLog": "Télécharger",
"downloadAsText": "Télécharger au format texte",
"deleteLog": "Supprimer",
"confirmDelete": "Supprimer ce journal de session ?",
"confirmDeleteDesc": "Cette action est irréversible.",
"copyContent": "Copier",
"copied": "Copié !",
"loadError": "Échec du chargement des journaux de session",
"deleteError": "Échec de la suppression du journal de session",
"filterByHost": "Filtrer par hôte..."
},
"admin": {
"sectionGeneral": "Général",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "חיבור מהיר",
"sshTools": "כלי SSH",
"history": "הִיסטוֹרִיָה",
"sessionLogs": "יומני סשן",
"openAsTabHint": "לחיצה על Ctrl או לחיצה אמצעית כדי לפתוח ככרטיסייה",
"openAsTab": "פתח ככרטיסייה",
"openInRightDock": "פתח בעגינה הימנית",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "העדיפו Mosh על פני SSH אם זמין",
"enableAutoTmux": "הפעל את Auto-Tmux",
"enableAutoTmuxDesc": "שמרו על המעטפת שלכם וכל דבר שפועל בה בחיים גם לאחר ניתוקים והפעלות מחדש של הדפדפן על ידי הפעלתה בתוך tmux (tmux חייב להיות מותקן על המארח)",
"enableSessionLogging": "רישום מושבים",
"enableSessionLoggingDesc": "הקלט את פלט הפעלת הטרמינל לצורך סקירה מאוחרת יותר",
"localEchoLabel": "הד מקומי",
"localEchoDesc": "חזה הקשות מקלדת להדפסה בטוחות באופן מקומי בחיבורים איטיים. בקשות סיסמה וקלט בקרה לעולם לא יוחזרו.",
"localEchoDefault": "השתמש בהגדרה גלובלית",
@@ -1969,23 +1966,6 @@
"disk": "דִיסק",
"viewServerDetails": "הצג פרטי שרת"
},
"sessionLogs": {
"title": "יומני סשן",
"noLogs": "אין עדיין יומני סשן",
"noLogsDesc": "הפעל רישום סשן במארח כדי להתחיל להקליט",
"duration": "מֶשֶׁך",
"viewLog": "הצג יומן",
"downloadLog": "הורדה",
"downloadAsText": "הורד כטקסט",
"deleteLog": "לִמְחוֹק",
"confirmDelete": "למחוק את יומן הפעילויות הזה?",
"confirmDeleteDesc": "לא ניתן לבטל פעולה זו.",
"copyContent": "לְהַעְתִיק",
"copied": "מוּעֲתָק!",
"loadError": "טעינת יומני הסשן נכשלה",
"deleteError": "נכשלה מחיקת יומן ההפעלה",
"filterByHost": "סנן לפי מארח..."
},
"admin": {
"sectionGeneral": "כְּלָלִי",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "त्वरित कनेक्ट",
"sshTools": "SSH टूल्स",
"history": "इतिहास",
"sessionLogs": "सत्र लॉग्स",
"openAsTabHint": "Ctrl+क्लिक या मिडिल क्लिक करके इसे टैब के रूप में खोलें",
"openAsTab": "टैब के रूप में खोलें",
"openInRightDock": "दाईं ओर के डॉक में खोलें",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "यदि उपलब्ध हो तो SSH के बजाय Mosh को प्राथमिकता दें",
"enableAutoTmux": "ऑटो-tmux सक्षम करें",
"enableAutoTmuxDesc": "अपने शेल और उसमें चल रहे किसी भी चीज़ को डिस्कनेक्ट होने और ब्राउज़र के रीस्टार्ट होने पर भी चालू रखने के लिए, tmux के अंदर चलाएँ (होस्ट पर tmux इंस्टॉल होना चाहिए)।",
"enableSessionLogging": "सत्र लॉगिंग",
"enableSessionLoggingDesc": "बाद में समीक्षा के लिए टर्मिनल सत्र का आउटपुट रिकॉर्ड करें",
"localEchoLabel": "स्थानीय प्रतिध्वनि",
"localEchoDesc": "धीमे कनेक्शन पर स्थानीय रूप से सुरक्षित और प्रिंट करने योग्य कीस्ट्रोक्स का अनुमान लगाएं। पासवर्ड प्रॉम्प्ट और नियंत्रण इनपुट कभी भी प्रदर्शित नहीं होते हैं।",
"localEchoDefault": "वैश्विक सेटिंग का उपयोग करें",
@@ -1969,23 +1966,6 @@
"disk": "डिस्क",
"viewServerDetails": "सर्वर विवरण देखें"
},
"sessionLogs": {
"title": "सेशन लॉग्स",
"noLogs": "अभी तक कोई सेशन लॉग नहीं",
"noLogsDesc": "रिकॉर्डिंग शुरू करने के लिए किसी होस्ट पर सेशन लॉगिंग सक्षम करें",
"duration": "अवधि",
"viewLog": "लॉग देखें",
"downloadLog": "डाउनलोड करें",
"downloadAsText": "टेक्स्ट के रूप में डाउनलोड करें",
"deleteLog": "हटाएं",
"confirmDelete": "क्या इस सेशन लॉग को हटाएं?",
"confirmDeleteDesc": "यह क्रिया वापस नहीं ली जा सकती।",
"copyContent": "कॉपी करें",
"copied": "कॉपी हो गया!",
"loadError": "सेशन लॉग लोड करने में विफल",
"deleteError": "सेशन लॉग हटाने में विफल",
"filterByHost": "होस्ट द्वारा फ़िल्टर करें..."
},
"admin": {
"sectionGeneral": "सामान्य",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Gyors csatlakozás",
"sshTools": "SSH eszközök",
"history": "Történelem",
"sessionLogs": "Munkamenet-naplók",
"openAsTabHint": "Ctrl+kattintás vagy középső kattintás fülként való megnyitáshoz",
"openAsTab": "Megnyitás lapként",
"openInRightDock": "Megnyitás a jobb oldali dokkban",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Ha elérhető, akkor a Mosh-t részesítsd előnyben az SSH-val szemben.",
"enableAutoTmux": "Auto-Tmux engedélyezése",
"enableAutoTmuxDesc": "Tartsd életben a shell-edet és a benne futó dolgokat a lecsatlakozások és a böngésző újraindítása során a tmux-on belüli futtatással (a tmux-nak telepítve kell lennie a gazdagépen)",
"enableSessionLogging": "Munkamenet-naplózás",
"enableSessionLoggingDesc": "Terminál munkamenet kimenetének rögzítése későbbi áttekintés céljából",
"localEchoLabel": "Helyi visszhang",
"localEchoDesc": "Lassú kapcsolatok esetén is helyben előrejelzi a biztonságosan nyomtatható billentyűleütéseket. A jelszókérések és a vezérlőbevitel soha nem jelenik meg.",
"localEchoDefault": "Globális beállítás használata",
@@ -1969,23 +1966,6 @@
"disk": "Korong",
"viewServerDetails": "Szerver részleteinek megtekintése"
},
"sessionLogs": {
"title": "Munkamenet-naplók",
"noLogs": "Még nincsenek munkamenet-naplók",
"noLogsDesc": "A felvétel megkezdéséhez engedélyezze a munkamenet-naplózást egy gazdagépen",
"duration": "Időtartam",
"viewLog": "Napló megtekintése",
"downloadLog": "Letöltés",
"downloadAsText": "Letöltés szövegként",
"deleteLog": "Töröl",
"confirmDelete": "Törli ezt a munkamenet-naplót?",
"confirmDeleteDesc": "Ez a művelet nem vonható vissza.",
"copyContent": "Másolat",
"copied": "Másolva!",
"loadError": "Nem sikerült betölteni a munkamenet-naplókat",
"deleteError": "Nem sikerült törölni a munkamenet-naplót",
"filterByHost": "Szűrés hoszt szerint..."
},
"admin": {
"sectionGeneral": "Általános",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Koneksi Cepat",
"sshTools": "Alat SSH",
"history": "Sejarah",
"sessionLogs": "Log Sesi",
"openAsTabHint": "Tekan Ctrl+klik atau klik tengah untuk membuka sebagai tab.",
"openAsTab": "Buka sebagai tab",
"openInRightDock": "Buka di dermaga kanan",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Lebih baik menggunakan Mosh daripada SSH jika tersedia.",
"enableAutoTmux": "Aktifkan Auto-Tmux",
"enableAutoTmuxDesc": "Jaga agar shell Anda dan semua yang berjalan di dalamnya tetap aktif meskipun terjadi pemutusan koneksi dan restart browser dengan menjalankan perintah di dalam tmux (tmux harus diinstal pada host).",
"enableSessionLogging": "Pencatatan Sesi",
"enableSessionLoggingDesc": "Rekam output sesi terminal untuk ditinjau nanti.",
"localEchoLabel": "Gema Lokal",
"localEchoDesc": "Memprediksi penekanan tombol yang aman dan dapat dicetak secara lokal pada koneksi lambat. Permintaan kata sandi dan input kontrol tidak pernah diulang.",
"localEchoDefault": "Gunakan pengaturan global",
@@ -1969,23 +1966,6 @@
"disk": "Disk",
"viewServerDetails": "Lihat detail server"
},
"sessionLogs": {
"title": "Log Sesi",
"noLogs": "Belum ada log sesi.",
"noLogsDesc": "Aktifkan pencatatan sesi pada host untuk memulai perekaman.",
"duration": "Lamanya",
"viewLog": "Lihat log",
"downloadLog": "Unduh",
"downloadAsText": "Unduh sebagai teks",
"deleteLog": "Menghapus",
"confirmDelete": "Hapus log sesi ini?",
"confirmDeleteDesc": "Tindakan ini tidak dapat dibatalkan.",
"copyContent": "Menyalin",
"copied": "Disalin!",
"loadError": "Gagal memuat log sesi",
"deleteError": "Gagal menghapus log sesi",
"filterByHost": "Saring berdasarkan host..."
},
"admin": {
"sectionGeneral": "Umum",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Connessione rapida",
"sshTools": "Strumenti SSH",
"history": "Cronologia",
"sessionLogs": "Log di sessione",
"openAsTabHint": "Ctrl+clic o clic centrale per aprire in una scheda",
"openAsTab": "Apri come scheda",
"openInRightDock": "Apri nella sezione dock a destra",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Preferisci Mosh rispetto a SSH se disponibile",
"enableAutoTmux": "Abilita Auto-Tmux",
"enableAutoTmuxDesc": "Mantieni la tua shell e tutto ciò che è in esecuzione al suo interno attivi anche in caso di disconnessioni e riavvii del browser, eseguendoli all'interno di tmux (tmux deve essere installato sull'host).",
"enableSessionLogging": "Registrazione sessione",
"enableSessionLoggingDesc": "Registra l'output della sessione del terminale per la revisione successiva",
"localEchoLabel": "Eco locale",
"localEchoDesc": "Prevedi localmente le sequenze di tasti sicure e stampabili anche con connessioni lente. Le richieste di password e gli input di controllo non vengono mai ripetuti.",
"localEchoDefault": "Utilizza le impostazioni globali",
@@ -1969,23 +1966,6 @@
"disk": "Disco",
"viewServerDetails": "Visualizza dettagli server"
},
"sessionLogs": {
"title": "Registri di sessione",
"noLogs": "Ancora nessun registro di sessione",
"noLogsDesc": "Abilita la registrazione delle sessioni su un host per iniziare a registrare",
"duration": "Durata",
"viewLog": "Visualizza registro",
"downloadLog": "Scarica",
"downloadAsText": "Scarica come testo",
"deleteLog": "Elimina",
"confirmDelete": "Eliminare questo registro di sessione?",
"confirmDeleteDesc": "Questa azione non può essere annullata.",
"copyContent": "Copia",
"copied": "Copiato!",
"loadError": "Impossibile caricare i registri di sessione",
"deleteError": "Impossibile eliminare il registro di sessione",
"filterByHost": "Filtra per host..."
},
"admin": {
"sectionGeneral": "Generale",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "クイック接続",
"sshTools": "SSHツール",
"history": "履歴",
"sessionLogs": "セッションログ",
"openAsTabHint": "タブとして開くには、Ctrlキーを押しながらクリックするか、マウスの中央ボタンをクリックしてください。",
"openAsTab": "タブとして開く",
"openInRightDock": "右側のドックで開く",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "利用可能な場合、SSH よりも Mosh を優先します",
"enableAutoTmux": "Auto-Tmux を有効にする",
"enableAutoTmuxDesc": "tmux内で実行することで、接続切断やブラウザの再起動後もシェルとその中で実行されているすべての処理を正常に実行状態に保つことができます(ホストにtmuxがインストールされている必要があります)。",
"enableSessionLogging": "セッションログ",
"enableSessionLoggingDesc": "後で確認できるようにターミナルセッションの出力を記録します",
"localEchoLabel": "ローカルエコー",
"localEchoDesc": "低速回線でも、印刷可能な安全なキー入力をローカルで予測します。パスワード入力や制御入力はエコーバックされません。",
"localEchoDefault": "グローバル設定を使用する",
@@ -1969,23 +1966,6 @@
"disk": "ディスク",
"viewServerDetails": "サーバーの詳細を表示"
},
"sessionLogs": {
"title": "セッションログ",
"noLogs": "セッションログはまだありません",
"noLogsDesc": "ホストでセッションログを有効にして記録を開始してください",
"duration": "所要時間",
"viewLog": "ログを表示",
"downloadLog": "ダウンロード",
"downloadAsText": "テキストとしてダウンロード",
"deleteLog": "削除",
"confirmDelete": "このセッションログを削除しますか?",
"confirmDeleteDesc": "この操作は元に戻せません。",
"copyContent": "コピー",
"copied": "コピーしました!",
"loadError": "セッションログの読み込みに失敗しました",
"deleteError": "セッションログの削除に失敗しました",
"filterByHost": "ホストでフィルター..."
},
"admin": {
"sectionGeneral": "全般",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "빠른 연결",
"sshTools": "SSH 도구",
"history": "기록",
"sessionLogs": "세션 로그",
"openAsTabHint": "Ctrl 키를 누른 상태에서 클릭하거나 마우스 가운데 버튼을 클릭하여 탭으로 엽니다.",
"openAsTab": "탭에서 열기",
"openInRightDock": "오른쪽 도크에서 열기",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "사용 가능한 경우 SSH보다 Mosh를 우선합니다",
"enableAutoTmux": "Auto-Tmux 활성화",
"enableAutoTmuxDesc": "tmux 내부에서 실행하면 연결이 끊기거나 브라우저가 다시 시작되더라도 셸과 그 안에서 실행 중인 모든 작업이 계속 유지됩니다(호스트에 tmux가 설치되어 있어야 함).",
"enableSessionLogging": "세션 로깅",
"enableSessionLoggingDesc": "나중에 검토할 수 있도록 터미널 세션 출력을 기록합니다",
"localEchoLabel": "로컬 에코",
"localEchoDesc": "연결 속도가 느린 환경에서도 안전하게 입력 가능한 키 입력을 로컬에서 예측합니다. 비밀번호 입력 및 제어 입력은 절대 화면에 표시되지 않습니다.",
"localEchoDefault": "전역 설정을 사용하세요",
@@ -1969,23 +1966,6 @@
"disk": "디스크",
"viewServerDetails": "서버 세부정보 보기"
},
"sessionLogs": {
"title": "세션 로그",
"noLogs": "아직 세션 로그 없음",
"noLogsDesc": "호스트에서 세션 로깅을 활성화하여 기록 시작",
"duration": "지속 시간",
"viewLog": "로그 보기",
"downloadLog": "다운로드",
"downloadAsText": "텍스트 파일로 다운로드",
"deleteLog": "삭제",
"confirmDelete": "이 세션 로그를 삭제하시겠습니까?",
"confirmDeleteDesc": "이 작업은 취소할 수 없습니다.",
"copyContent": "복사",
"copied": "복사됨!",
"loadError": "세션 로그 로드 실패",
"deleteError": "세션 로그 삭제 실패",
"filterByHost": "호스트로 필터링..."
},
"admin": {
"sectionGeneral": "일반",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Snel verbinden",
"sshTools": "SSH-tools",
"history": "Geschiedenis",
"sessionLogs": "Sessielogboeken",
"openAsTabHint": "Ctrl+klik of middelste muisknop om te openen als een tabblad.",
"openAsTab": "Openen als tabblad",
"openInRightDock": "Openen in het rechter dock",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Geef de voorkeur aan Mosh boven SSH indien beschikbaar.",
"enableAutoTmux": "Auto-Tmux inschakelen",
"enableAutoTmuxDesc": "Zorg ervoor dat je shell en alles wat daarin draait, blijft functioneren, ook na een verbindingsoverbreking of een herstart van de browser, door tmux te gebruiken (tmux moet wel op de host geïnstalleerd zijn).",
"enableSessionLogging": "Sessieregistratie",
"enableSessionLoggingDesc": "Sla de uitvoer van de terminalsessie op voor latere beoordeling.",
"localEchoLabel": "Local Echo",
"localEchoDesc": "Voorspel lokaal veilige toetsaanslagen, ook bij trage internetverbindingen. Wachtwoordprompts en besturingsinvoer worden nooit weergegeven.",
"localEchoDefault": "Gebruik de globale instelling",
@@ -1969,23 +1966,6 @@
"disk": "Schijf",
"viewServerDetails": "Servergegevens bekijken"
},
"sessionLogs": {
"title": "Sessielogboeken",
"noLogs": "Nog geen sessielogboeken",
"noLogsDesc": "Schakel sessielogging in op een host om te beginnen met opnemen.",
"duration": "Duur",
"viewLog": "Logboek bekijken",
"downloadLog": "Download",
"downloadAsText": "Downloaden als tekst",
"deleteLog": "Verwijderen",
"confirmDelete": "Dit sessielogboek verwijderen?",
"confirmDeleteDesc": "Deze actie kan niet ongedaan gemaakt worden.",
"copyContent": "Kopiëren",
"copied": "Gekopieerd!",
"loadError": "Het laden van de sessielogboeken is mislukt.",
"deleteError": "Het verwijderen van het sessielogboek is mislukt.",
"filterByHost": "Filteren op host..."
},
"admin": {
"sectionGeneral": "Algemeen",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Hurtigtilkobling",
"sshTools": "SSH-verktøy",
"history": "Historie",
"sessionLogs": "Øktlogger",
"openAsTabHint": "Ctrl+klikk eller midtklikk for å åpne som fane",
"openAsTab": "Åpne som fane",
"openInRightDock": "Åpne i høyre dokk",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Foretrekker Mosh fremfor SSH hvis tilgjengelig",
"enableAutoTmux": "Aktiver Auto-Tmux",
"enableAutoTmuxDesc": "Hold skallet ditt og alt som kjører i det i live på tvers av frakoblinger og omstart av nettleseren ved å kjøre det i tmux (tmux må være installert på verten)",
"enableSessionLogging": "Øktlogging",
"enableSessionLoggingDesc": "Ta opp utdata fra terminaløkten for senere gjennomgang",
"localEchoLabel": "Lokalt ekko",
"localEchoDesc": "Forutsi trygge utskrivbare tastetrykk lokalt på trege tilkoblinger. Passordforespørsler og kontrollinndata gjentas aldri.",
"localEchoDefault": "Bruk global innstilling",
@@ -1969,23 +1966,6 @@
"disk": "Disk",
"viewServerDetails": "Vis serverdetaljer"
},
"sessionLogs": {
"title": "Øktlogger",
"noLogs": "Ingen øktlogger ennå",
"noLogsDesc": "Aktiver øktlogging på en vert for å starte opptaket",
"duration": "Varighet",
"viewLog": "Vis logg",
"downloadLog": "Last ned",
"downloadAsText": "Last ned som tekst",
"deleteLog": "Slett",
"confirmDelete": "Slette denne øktloggen?",
"confirmDeleteDesc": "Denne handlingen kan ikke angres.",
"copyContent": "Kopiere",
"copied": "Kopiert!",
"loadError": "Kunne ikke laste inn øktlogger",
"deleteError": "Kunne ikke slette øktloggen",
"filterByHost": "Filtrer etter vert..."
},
"admin": {
"sectionGeneral": "General",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Szybkie połączenie",
"sshTools": "Narzędzia SSH",
"history": "Historia",
"sessionLogs": "Logi sesji",
"openAsTabHint": "Ctrl+kliknięcie lub środkowy przycisk myszy, aby otworzyć jako kartę",
"openAsTab": "Otwórz jako kartę",
"openInRightDock": "Otwórz w prawym doku",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Jeśli to możliwe, preferuj Mosh zamiast SSH",
"enableAutoTmux": "Włącz Auto-Tmux",
"enableAutoTmuxDesc": "Utrzymuj działanie powłoki i wszystkiego, co w niej działa, nawet po rozłączeniu i ponownym uruchomieniu przeglądarki, uruchamiając ją wewnątrz tmux (tmux musi być zainstalowany na hoście).",
"enableSessionLogging": "Rejestrowanie sesji",
"enableSessionLoggingDesc": "Zapisuj wyjście sesji terminala do późniejszego przejrzenia",
"localEchoLabel": "Lokalne echo",
"localEchoDesc": "Przewiduj lokalnie bezpieczne, drukowalne znaki na wolnych łączach. Monity o hasło i znaki sterujące nigdy nie są powtarzane.",
"localEchoDefault": "Użyj ustawienia globalnego",
@@ -1969,23 +1966,6 @@
"disk": "Dysk",
"viewServerDetails": "Wyświetl szczegóły serwera"
},
"sessionLogs": {
"title": "Logi sesji",
"noLogs": "Brak logów sesji",
"noLogsDesc": "Włącz rejestrowanie sesji na hoście, aby rozpocząć nagrywanie",
"duration": "Czas trwania",
"viewLog": "Wyświetl log",
"downloadLog": "Pobierz",
"downloadAsText": "Pobierz jako tekst",
"deleteLog": "Usuń",
"confirmDelete": "Usunąć ten log sesji?",
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
"copyContent": "Kopiuj",
"copied": "Skopiowano!",
"loadError": "Nie udało się załadować logów sesji",
"deleteError": "Nie udało się usunąć logu sesji",
"filterByHost": "Filtruj według hosta..."
},
"admin": {
"sectionGeneral": "Ogólne",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Conexão rápida",
"sshTools": "Ferramentas SSH",
"history": "História",
"sessionLogs": "Registros de sessão",
"openAsTabHint": "Ctrl+clique ou clique com o botão do meio para abrir como uma aba",
"openAsTab": "Abrir como guia",
"openInRightDock": "Abrir no painel direito",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Prefira Mosh a SSH, se disponível.",
"enableAutoTmux": "Ativar Auto-Tmux",
"enableAutoTmuxDesc": "Mantenha seu shell e tudo o que estiver sendo executado nele em funcionamento mesmo após desconexões e reinicializações do navegador, executando o comando dentro do tmux (o tmux precisa estar instalado no host).",
"enableSessionLogging": "Registro de sessão",
"enableSessionLoggingDesc": "Registre a saída da sessão do terminal para revisão posterior.",
"localEchoLabel": "Eco Local",
"localEchoDesc": "Preveja sequências de teclas seguras e imprimíveis localmente em conexões lentas. Os prompts de senha e as entradas de controle nunca são repetidos.",
"localEchoDefault": "Usar configuração global",
@@ -1969,23 +1966,6 @@
"disk": "Disco",
"viewServerDetails": "Ver detalhes do servidor"
},
"sessionLogs": {
"title": "Registros de sessão",
"noLogs": "Ainda não há registros de sessão.",
"noLogsDesc": "Habilite o registro de sessão em um host para iniciar a gravação.",
"duration": "Duração",
"viewLog": "Ver registro",
"downloadLog": "Download",
"downloadAsText": "Baixar como texto",
"deleteLog": "Excluir",
"confirmDelete": "Excluir este registro de sessão?",
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
"copyContent": "Cópia",
"copied": "Copiado!",
"loadError": "Falha ao carregar os registros da sessão.",
"deleteError": "Falha ao excluir o registro da sessão",
"filterByHost": "Filtrar por host..."
},
"admin": {
"sectionGeneral": "Em geral",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Ligação Rápida",
"sshTools": "Ferramentas SSH",
"history": "Histórico",
"sessionLogs": "Registos de Sessão",
"openAsTabHint": "Ctrl+clique ou clique com o botão do meio para abrir como separador",
"openAsTab": "Abrir como separador",
"openInRightDock": "Abrir no painel direito",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Preferir Mosh a SSH, se disponível",
"enableAutoTmux": "Ativar Auto-Tmux",
"enableAutoTmuxDesc": "Mantenha o seu shell e tudo o que nele está a ser executado em funcionamento mesmo após desconexões e reinicializações do browser, executando o comando dentro do tmux (o tmux necessita de estar instalado no host).",
"enableSessionLogging": "Registo de sessão",
"enableSessionLoggingDesc": "Gravar a saída da sessão de terminal para revisão posterior",
"localEchoLabel": "Eco Local",
"localEchoDesc": "Preveja sequências de teclas seguras e imprimíveis localmente em ligações lentas. Os avisos de palavra-passe e as entradas de controlo nunca são repetidos.",
"localEchoDefault": "Usar configuração global",
@@ -1969,23 +1966,6 @@
"disk": "Disco",
"viewServerDetails": "Ver detalhes do servidor"
},
"sessionLogs": {
"title": "Registos de Sessão",
"noLogs": "Ainda sem registos de sessão",
"noLogsDesc": "Ative o registo de sessão num host para começar a gravar",
"duration": "Duração",
"viewLog": "Ver registo",
"downloadLog": "Transferir",
"downloadAsText": "Descarregar como texto",
"deleteLog": "Eliminar",
"confirmDelete": "Eliminar este registo de sessão?",
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
"copyContent": "Copiar",
"copied": "Copiado!",
"loadError": "Falha ao carregar registos de sessão",
"deleteError": "Falha ao eliminar registo de sessão",
"filterByHost": "Filtrar por host..."
},
"admin": {
"sectionGeneral": "Geral",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Conectare rapidă",
"sshTools": "Instrumente SSH",
"history": "Istorie",
"sessionLogs": "Jurnalele de sesiune",
"openAsTabHint": "Ctrl+clic sau clic mijloc pentru a deschide ca filă",
"openAsTab": "Deschide ca filă",
"openInRightDock": "Deschideți în docul din dreapta",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Preferă Mosh în locul SSH dacă este disponibil",
"enableAutoTmux": "Activează Auto-Tmux",
"enableAutoTmuxDesc": "Mențineți shell-ul și tot ce rulează în el în funcțiune după deconectări și reporniri ale browserului, rulând în interiorul tmux (tmux trebuie să fie instalat pe gazdă).",
"enableSessionLogging": "Înregistrarea sesiunilor",
"enableSessionLoggingDesc": "Înregistrați ieșirea sesiunii terminale pentru o revizuire ulterioară",
"localEchoLabel": "Ecou local",
"localEchoDesc": "Preziceți local apăsările de taste imprimabile în siguranță pe conexiuni lente. Solicitările de parolă și intrările de control nu sunt niciodată reluate.",
"localEchoDefault": "Folosește setarea globală",
@@ -1969,23 +1966,6 @@
"disk": "Disc",
"viewServerDetails": "Vizualizați detaliile serverului"
},
"sessionLogs": {
"title": "Jurnalele de sesiune",
"noLogs": "Încă nu există jurnale de sesiune",
"noLogsDesc": "Activați înregistrarea în jurnal pe o gazdă pentru a începe înregistrarea",
"duration": "Durată",
"viewLog": "Vizualizare jurnal",
"downloadLog": "Descărcare",
"downloadAsText": "Descărcați ca text",
"deleteLog": "Şterge",
"confirmDelete": "Ștergeți acest jurnal de sesiune?",
"confirmDeleteDesc": "Această acțiune nu poate fi anulată.",
"copyContent": "Copie",
"copied": "Copiat!",
"loadError": "Nu s-au putut încărca jurnalele de sesiune",
"deleteError": "Nu s-a putut șterge jurnalul sesiunii",
"filterByHost": "Filtrare după gazdă..."
},
"admin": {
"sectionGeneral": "General",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Быстрое подключение",
"sshTools": "Инструменты SSH",
"history": "История",
"sessionLogs": "Журналы сессий",
"openAsTabHint": "Нажмите Ctrl+клик или среднюю кнопку мыши, чтобы открыть вкладку.",
"openAsTab": "Открыть как вкладку",
"openInRightDock": "Открыть в правом доке",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Предпочитать Mosh вместо SSH, если доступен",
"enableAutoTmux": "Включить Auto-Tmux",
"enableAutoTmuxDesc": "Поддерживайте работоспособность вашей оболочки и всего, что в ней работает, даже при разрыве соединения и перезапуске браузера, запустив tmux (tmux должен быть установлен на хосте).",
"enableSessionLogging": "Логирование сессии",
"enableSessionLoggingDesc": "Записывать вывод терминальной сессии для последующего просмотра",
"localEchoLabel": "Локальное эхо",
"localEchoDesc": "Предсказывает безопасные нажатия клавиш для печати локально даже при медленном соединении. Запросы паролей и управляющий ввод никогда не отображаются.",
"localEchoDefault": "Использовать глобальные настройки",
@@ -1969,23 +1966,6 @@
"disk": "Диск",
"viewServerDetails": "Просмотреть сведения о сервере"
},
"sessionLogs": {
"title": "Журналы сеансов",
"noLogs": "Пока нет журналов сеансов",
"noLogsDesc": "Включите ведение журнала сеансов на хосте, чтобы начать запись",
"duration": "Продолжительность",
"viewLog": "Просмотреть журнал",
"downloadLog": "Скачать",
"downloadAsText": "Скачать как текст",
"deleteLog": "Удалить",
"confirmDelete": "Удалить этот журнал сеансов?",
"confirmDeleteDesc": "Это действие нельзя отменить.",
"copyContent": "Копировать",
"copied": "Скопировано!",
"loadError": "Не удалось загрузить журналы сеансов",
"deleteError": "Не удалось удалить журнал сеанса",
"filterByHost": "Фильтровать по хосту..."
},
"admin": {
"sectionGeneral": "Общие",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Брзо повезивање",
"sshTools": "SSH алати",
"history": "Историја",
"sessionLogs": "Записи сесија",
"openAsTabHint": "Ctrl+клик или средњи клик за отварање као картице",
"openAsTab": "Отвори као картицу",
"openInRightDock": "Отвори у десном доку",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Преферирај Mosh у односу на SSH ако је доступан",
"enableAutoTmux": "Омогући аутоматски Tmux",
"enableAutoTmuxDesc": "Одржавајте своју шкољку и све што се у њој покреће активним након прекида везе и поновног покретања прегледача покретањем унутар tmux-а (tmux мора бити инсталиран на хосту)",
"enableSessionLogging": "Записивање сесије",
"enableSessionLoggingDesc": "Снимите излаз терминалне сесије за каснији преглед",
"localEchoLabel": "Локални одјек",
"localEchoDesc": "Предвиди безбедне притиске тастера за штампање локално на спорим везама. Упити за лозинку и контролни уноси се никада не понављају.",
"localEchoDefault": "Користите глобална подешавања",
@@ -1969,23 +1966,6 @@
"disk": "Диск",
"viewServerDetails": "Погледајте детаље сервера"
},
"sessionLogs": {
"title": "Записи сесија",
"noLogs": "Још нема евиденција сесија",
"noLogsDesc": "Омогућите евидентирање сесије на хосту да бисте започели снимање",
"duration": "Трајање",
"viewLog": "Прикажи дневник",
"downloadLog": "Преузми",
"downloadAsText": "Преузми као текст",
"deleteLog": "Обриши",
"confirmDelete": "Да ли желите да обришете овај дневник сесије?",
"confirmDeleteDesc": "Ова радња се не може поништити.",
"copyContent": "Копија",
"copied": "Копирано!",
"loadError": "Учитавање евиденције сесије није успело",
"deleteError": "Брисање дневника сесије није успело",
"filterByHost": "Филтрирај по хосту..."
},
"admin": {
"sectionGeneral": "Опште",
"sectionOidc": "ОИДЦ",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Snabbanslutning",
"sshTools": "SSH-verktyg",
"history": "Historia",
"sessionLogs": "Sessionsloggar",
"openAsTabHint": "Ctrl+klicka eller mellanklicka för att öppna som flik",
"openAsTab": "Öppna som flik",
"openInRightDock": "Öppna i höger docka",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Föredra Mosh framför SSH om tillgängligt",
"enableAutoTmux": "Aktivera Auto-Tmux",
"enableAutoTmuxDesc": "Håll ditt skal och allt som körs i det vid liv även efter frånkopplingar och omstart av webbläsare genom att köra det inuti tmux (tmux måste vara installerat på värden)",
"enableSessionLogging": "Sessionsloggning",
"enableSessionLoggingDesc": "Spela in utdata från terminalsessionen för senare granskning",
"localEchoLabel": "Lokalt eko",
"localEchoDesc": "Förutsäg säkra utskrivbara tangenttryckningar lokalt på långsamma anslutningar. Lösenordsförfrågningar och kontrollinmatning upprepas aldrig.",
"localEchoDefault": "Använd global inställning",
@@ -1969,23 +1966,6 @@
"disk": "Disk",
"viewServerDetails": "Visa serverinformation"
},
"sessionLogs": {
"title": "Sessionsloggar",
"noLogs": "Inga sessionsloggar ännu",
"noLogsDesc": "Aktivera sessionsloggning på en värd för att starta inspelning",
"duration": "Varaktighet",
"viewLog": "Visa logg",
"downloadLog": "Ladda ner",
"downloadAsText": "Ladda ner som text",
"deleteLog": "Radera",
"confirmDelete": "Radera den här sessionsloggen?",
"confirmDeleteDesc": "Den här åtgärden kan inte ångras.",
"copyContent": "Kopiera",
"copied": "Kopierad!",
"loadError": "Misslyckades med att läsa in sessionsloggar",
"deleteError": "Misslyckades med att radera sessionsloggen",
"filterByHost": "Filtrera efter värd..."
},
"admin": {
"sectionGeneral": "Allmän",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "การเชื่อมต่อด่วน",
"sshTools": "เครื่องมือ SSH",
"history": "ประวัติศาสตร์",
"sessionLogs": "บันทึกเซสชัน",
"openAsTabHint": "Ctrl+คลิก หรือคลิกเมาส์กลาง เพื่อเปิดเป็นแท็บ",
"openAsTab": "เปิดเป็นแท็บ",
"openInRightDock": "เปิดในแถบด้านขวา",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "หากมีตัวเลือก ให้เลือกใช้ Mosh แทน SSH",
"enableAutoTmux": "เปิดใช้งาน Auto-Tmux",
"enableAutoTmuxDesc": "รักษาการทำงานของเชลล์และโปรแกรมใดๆ ที่ทำงานอยู่ภายในเชลล์ให้คงอยู่ต่อไปแม้การตัดการเชื่อมต่อหรือการรีสตาร์ทเบราว์เซอร์ โดยการเรียกใช้คำสั่งภายใน tmux (ต้องติดตั้ง tmux บนเครื่องโฮสต์)",
"enableSessionLogging": "การบันทึกเซสชัน",
"enableSessionLoggingDesc": "บันทึกผลลัพธ์จากเทอร์มินัลเพื่อตรวจสอบในภายหลัง",
"localEchoLabel": "เสียงสะท้อนในท้องถิ่น",
"localEchoDesc": "คาดการณ์การกดแป้นพิมพ์ที่ปลอดภัยสำหรับการพิมพ์ในเครื่องบนการเชื่อมต่อที่ช้า ข้อความแจ้งให้ป้อนรหัสผ่านและการป้อนข้อมูลควบคุมจะไม่ถูกแสดงซ้ำ",
"localEchoDefault": "ใช้การตั้งค่าทั่วโลก",
@@ -1969,23 +1966,6 @@
"disk": "ดิสก์",
"viewServerDetails": "ดูรายละเอียดเซิร์ฟเวอร์"
},
"sessionLogs": {
"title": "บันทึกเซสชัน",
"noLogs": "ยังไม่มีบันทึกการใช้งานใดๆ",
"noLogsDesc": "เปิดใช้งานการบันทึกเซสชันบนโฮสต์เพื่อเริ่มการบันทึก",
"duration": "ระยะเวลา",
"viewLog": "ดูบันทึก",
"downloadLog": "ดาวน์โหลด",
"downloadAsText": "ดาวน์โหลดเป็นข้อความ",
"deleteLog": "ลบ",
"confirmDelete": "ลบบันทึกการใช้งานนี้หรือไม่?",
"confirmDeleteDesc": "การกระทำนี้ไม่สามารถย้อนกลับได้",
"copyContent": "สำเนา",
"copied": "คัดลอกแล้ว!",
"loadError": "ไม่สามารถโหลดบันทึกเซสชันได้",
"deleteError": "ไม่สามารถลบบันทึกเซสชันได้",
"filterByHost": "กรองตามโฮสต์..."
},
"admin": {
"sectionGeneral": "ทั่วไป",
"sectionOidc": "โอไอดีซี",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Hızlı Bağlantı",
"sshTools": "SSH Araçları",
"history": "Geçmiş",
"sessionLogs": "Oturum Kayıtları",
"openAsTabHint": "Sekme olarak açmak için Ctrl+tıklayın veya orta fare tuşuna tıklayın.",
"openAsTab": "Sekme olarak aç",
"openInRightDock": "Sağ taraftaki yuvayı açın",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Varsa, SSH yerine Mosh'u tercih et",
"enableAutoTmux": "Otomatik Tmux'u Etkinleştir",
"enableAutoTmuxDesc": "Kabuk uygulamanızı ve içinde çalışan her şeyi bağlantı kesintileri ve tarayıcı yeniden başlatmaları sırasında çalışır durumda tutmak için tmux içinde çalıştırın (tmux ana bilgisayarda kurulu olmalıdır).",
"enableSessionLogging": "Oturum Kaydı",
"enableSessionLoggingDesc": "Daha sonra gözden geçirmek için terminal oturumu çıktısını kaydet",
"localEchoLabel": "Yerel Yankı",
"localEchoDesc": "Yavaş bağlantılarda bile yerel olarak güvenli yazdırılabilir tuş vuruşlarını tahmin edin. Parola istemleri ve kontrol girişleri asla tekrarlanmaz.",
"localEchoDefault": "Genel ayarı kullanın",
@@ -1969,23 +1966,6 @@
"disk": "Disk",
"viewServerDetails": "Sunucu ayrıntılarını görüntüle"
},
"sessionLogs": {
"title": "Oturum Günlükleri",
"noLogs": "Henüz oturum günlüğü yok",
"noLogsDesc": "Kayda başlamak için bir sunucuda oturum günlüğünü etkinleştirin",
"duration": "Süre",
"viewLog": "Günlüğü görüntüle",
"downloadLog": "İndir",
"downloadAsText": "Metin olarak indir",
"deleteLog": "Sil",
"confirmDelete": "Bu oturum günlüğü silinsin mi?",
"confirmDeleteDesc": "Bu işlem geri alınamaz.",
"copyContent": "Kopyala",
"copied": "Kopyalandı!",
"loadError": "Oturum günlükleri yüklenemedi",
"deleteError": "Oturum günlüğü silinemedi",
"filterByHost": "Sunucuya göre filtrele..."
},
"admin": {
"sectionGeneral": "Genel",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Швидке підключення",
"sshTools": "SSH-інструменти",
"history": "Історія",
"sessionLogs": "Журнали сеансів",
"openAsTabHint": "Ctrl+клік або клік середньою кнопкою миші, щоб відкрити як вкладку",
"openAsTab": "Відкрити як вкладку",
"openInRightDock": "Відкрити в правому доку",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Надавати перевагу Mosh над SSH, якщо доступно",
"enableAutoTmux": "Увімкнути автоматичне Tmux",
"enableAutoTmuxDesc": "Зберігайте вашу оболонку та все, що в ній працює, активними після відключень та перезапусків браузера, запускаючи її всередині tmux (tmux має бути встановлений на хості)",
"enableSessionLogging": "Ведення журналу сеансу",
"enableSessionLoggingDesc": "Запис виводу сеансу терміналу для подальшого перегляду",
"localEchoLabel": "Локальне відлуння",
"localEchoDesc": "Передбачати безпечні для друку натискання клавіш локально на повільних з'єднаннях. Запити на введення пароля та введені елементи керування ніколи не відображаються.",
"localEchoDefault": "Використовувати глобальні налаштування",
@@ -1969,23 +1966,6 @@
"disk": "Диск",
"viewServerDetails": "Переглянути деталі сервера"
},
"sessionLogs": {
"title": "Журнали сеансів",
"noLogs": "Журналів сеансів поки що немає",
"noLogsDesc": "Увімкнути ведення журналу сеансу на хості, щоб розпочати запис",
"duration": "Тривалість",
"viewLog": "Переглянути журнал",
"downloadLog": "Завантажити",
"downloadAsText": "Завантажити як текст",
"deleteLog": "Видалити",
"confirmDelete": "Видалити цей журнал сеансу?",
"confirmDeleteDesc": "Цю дію не можна скасувати.",
"copyContent": "Копіювати",
"copied": "Скопійовано!",
"loadError": "Не вдалося завантажити журнали сеансу",
"deleteError": "Не вдалося видалити журнал сеансу",
"filterByHost": "Фільтрувати за хостом..."
},
"admin": {
"sectionGeneral": "Загальне",
"sectionOidc": "ОІДК",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "Kết nối nhanh",
"sshTools": "Công cụ SSH",
"history": "Lịch sử",
"sessionLogs": "Nhật ký phiên",
"openAsTabHint": "Nhấn Ctrl + chuột hoặc chuột giữa để mở dưới dạng tab",
"openAsTab": "Mở dưới dạng tab",
"openInRightDock": "Mở trong thanh công cụ bên phải",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "Ưu tiên Mosh hơn SSH nếu có",
"enableAutoTmux": "Kích hoạt Auto-Tmux",
"enableAutoTmuxDesc": "Giữ cho shell và mọi thứ đang chạy trong đó hoạt động bình thường ngay cả khi bị ngắt kết nối hoặc khởi động lại trình duyệt bằng cách chạy bên trong tmux (tmux phải được cài đặt trên máy chủ).",
"enableSessionLogging": "Ghi nhật ký phiên",
"enableSessionLoggingDesc": "Ghi lại đầu ra phiên terminal để xem lại sau",
"localEchoLabel": "Tiếng vọng địa phương",
"localEchoDesc": "Dự đoán các thao tác gõ phím an toàn có thể in được cục bộ trên các kết nối chậm. Lời nhắc nhập mật khẩu và đầu vào điều khiển không bao giờ được hiển thị lại.",
"localEchoDefault": "Sử dụng cài đặt toàn cầu",
@@ -1969,23 +1966,6 @@
"disk": "Ổ đĩa",
"viewServerDetails": "Xem chi tiết máy chủ"
},
"sessionLogs": {
"title": "Nhật ký phiên",
"noLogs": "Chưa có nhật ký phiên nào",
"noLogsDesc": "Bật ghi nhật ký phiên trên máy chủ để bắt đầu ghi",
"duration": "Thời lượng",
"viewLog": "Xem nhật ký",
"downloadLog": "Tải xuống",
"downloadAsText": "Tải xuống dưới dạng văn bản",
"deleteLog": "Xóa",
"confirmDelete": "Xóa nhật ký phiên này?",
"confirmDeleteDesc": "Hành động này không thể hoàn tác.",
"copyContent": "Sao chép",
"copied": "Đã sao chép!",
"loadError": "Tải nhật ký phiên thất bại",
"deleteError": "Xóa nhật ký phiên thất bại",
"filterByHost": "Lọc theo máy chủ..."
},
"admin": {
"sectionGeneral": "Chung",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "快速连接",
"sshTools": "SSH 工具",
"history": "历史记录",
"sessionLogs": "会话日志",
"openAsTabHint": "按 Ctrl+单击或单击鼠标中键即可在新标签页中打开",
"openAsTab": "在新标签页中打开",
"openInRightDock": "在右侧停靠栏打开",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "若可用,优先使用 Mosh 而非 SSH",
"enableAutoTmux": "启用自动 Tmux",
"enableAutoTmuxDesc": "通过在 tmux 中运行(tmux 必须安装在主机上),可以在断开连接和浏览器重启后保持 shell 及其中运行的所有内容处于活动状态。",
"enableSessionLogging": "会话日志记录",
"enableSessionLoggingDesc": "记录终端会话输出以供日后查看",
"localEchoLabel": "本地回声",
"localEchoDesc": "在连接速度较慢的情况下,本地预测可安全打印的按键操作。密码提示和控制输入永远不会被回显。",
"localEchoDefault": "使用全局设置",
@@ -1969,23 +1966,6 @@
"disk": "磁盘",
"viewServerDetails": "查看服务器详情"
},
"sessionLogs": {
"title": "会话日志",
"noLogs": "暂无会话日志",
"noLogsDesc": "在主机上启用会话日志以开始记录",
"duration": "持续时间",
"viewLog": "查看日志",
"downloadLog": "下载",
"downloadAsText": "下载为文本",
"deleteLog": "删除",
"confirmDelete": "删除此会话日志?",
"confirmDeleteDesc": "此操作无法撤消。",
"copyContent": "复制",
"copied": "已复制!",
"loadError": "加载会话日志失败",
"deleteError": "删除会话日志失败",
"filterByHost": "按主机筛选..."
},
"admin": {
"sectionGeneral": "通用",
"sectionOidc": "OIDC",
-20
View File
@@ -695,7 +695,6 @@
"quickConnect": "快速連線",
"sshTools": "SSH 工具",
"history": "歷史記錄",
"sessionLogs": "工作階段記錄",
"openAsTabHint": "按 Ctrl+按一下或按滑鼠中鍵即可在新標籤頁中開啟",
"openAsTab": "在新分頁中開啟",
"openInRightDock": "在右側碼頭打開",
@@ -1033,8 +1032,6 @@
"enableAutoMoshDesc": "如果可用,優先使用 Mosh 而非 SSH",
"enableAutoTmux": "啟用自動 Tmux",
"enableAutoTmuxDesc": "透過在 tmux 中執行(tmux 必須安裝在主機上),可以在斷開連線和瀏覽器重新啟動後保持 shell 及其中執行的所有內容處於活動狀態。",
"enableSessionLogging": "工作階段記錄",
"enableSessionLoggingDesc": "記錄終端機工作階段輸出以供日後檢視",
"localEchoLabel": "本地迴聲",
"localEchoDesc": "在連線速度較慢的情況下,本地預測可安全列印的按鍵操作。密碼提示和控制輸入永遠不會被回顯。",
"localEchoDefault": "使用全域設定",
@@ -1969,23 +1966,6 @@
"disk": "磁碟",
"viewServerDetails": "檢視伺服器詳細資訊"
},
"sessionLogs": {
"title": "工作階段記錄",
"noLogs": "尚無任何工作階段記錄",
"noLogsDesc": "在主機上啟用工作階段記錄以開始錄製",
"duration": "持續時間",
"viewLog": "檢視記錄",
"downloadLog": "下載",
"downloadAsText": "下載為文字",
"deleteLog": "刪除",
"confirmDelete": "刪除此工作階段記錄?",
"confirmDeleteDesc": "此動作無法復原。",
"copyContent": "複製",
"copied": "已複製!",
"loadError": "載入工作階段記錄失敗",
"deleteError": "刪除工作階段記錄失敗",
"filterByHost": "依主機篩選..."
},
"admin": {
"sectionGeneral": "一般",
"sectionOidc": "OIDC",
-2
View File
@@ -21,7 +21,6 @@ import { getTabType } from "./tab-registry";
const CORE_CAPTURABLE = new Set([
"homepage",
"termix-id",
"session-logs",
"macros",
"ssh-tools",
]);
@@ -30,7 +29,6 @@ const CORE_CAPTURABLE = new Set([
const CORE_SINGLETON = new Set([
"homepage",
"termix-id",
"session-logs",
"macros",
"ssh-tools",
]);
-15
View File
@@ -9,7 +9,6 @@ import {
User,
Fingerprint,
Hammer,
ScrollText,
} from "lucide-react";
import { lazy, Suspense } from "react";
import { useTranslation } from "react-i18next";
@@ -41,11 +40,6 @@ const HomepageCanvas = lazy(() =>
const TermixIdPanel = lazy(() =>
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
);
const SessionLogsPanel = lazy(() =>
import("@/sidebar/SessionLogsPanel").then((m) => ({
default: m.SessionLogsPanel,
})),
);
const MacrosPanel = lazy(() =>
import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })),
);
@@ -124,8 +118,6 @@ export function tabIcon(type: TabType) {
return <LayoutGrid className="size-3.5" />;
case "termix-id":
return <Fingerprint className="size-3.5" />;
case "session-logs":
return <ScrollText className="size-3.5" />;
case "macros":
return <Braces className="size-3.5" />;
@@ -229,13 +221,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) {
</PanelTabFrame>,
);
case "session-logs":
return withTabSuspense(
<PanelTabFrame>
<SessionLogsPanel />
</PanelTabFrame>,
);
case "split-screen":
return null;
-1
View File
@@ -23,7 +23,6 @@ export type CoreRailView =
| "quick-connect"
| ToolsTab
| "connections"
| "session-logs"
| "user-profile"
| "admin-settings";
-21
View File
@@ -1789,27 +1789,6 @@ export function HostEditor({
onChange={(v) => setField("autoMosh", v)}
/>
</SettingRow>
<SettingRow
label={t("hosts.enableSessionLogging")}
description={
<>
{t("hosts.enableSessionLoggingDesc")}{" "}
<a
href="https://docs.termix.site/features/terminal/session-recording"
target="_blank"
rel="noreferrer"
className="text-accent-brand hover:underline"
>
{t("hosts.docsLink")}
</a>
</>
}
>
<FakeSwitch
checked={form.enableSessionLogging}
onChange={(v) => setField("enableSessionLogging", v)}
/>
</SettingRow>
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t("hosts.localEchoLabel")}
-603
View File
@@ -1,603 +0,0 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { copyToClipboard } from "@/lib/clipboard";
import { useAdaptivePolling } from "@/hooks/use-adaptive-polling";
import { Input } from "@/components/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/tooltip";
import {
ArrowLeft,
Check,
Copy,
Download,
Eye,
FileText,
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";
import {
asciicastToPlainText,
parseAsciicast,
} from "@/features/session-recording/asciicast";
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 buildTextFilename(log: SessionLogRecord): string {
return buildFilename(log).replace(/\.(cast|guac|log)$/, ".txt");
}
async function extractPlainText(
log: SessionLogRecord,
blob: Blob,
): Promise<string | null> {
if (log.format === "guacamole") return null;
const source = await blob.text();
if (log.format === "text") return source;
return asciicastToPlainText(parseAsciicast(source));
}
function SectionHeader({ label, count }: { label: string; count: number }) {
return (
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/20">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 flex-1">
{label}
</span>
<span className="text-[10px] font-semibold text-muted-foreground/40 bg-muted/60 px-1.5 py-0.5">
{count}
</span>
</div>
);
}
function LogRow({
log,
onView,
onDownload,
onDownloadText,
onDelete,
}: {
log: SessionLogRecord;
onView: () => void;
onDownload: () => void;
onDownloadText: () => void;
onDelete: () => void;
}) {
const { t } = useTranslation();
const hostLabel = log.hostName ?? log.hostIp ?? `Host ${log.hostId}`;
return (
<div className="group flex items-center gap-2.5 px-3 py-2.5 border-b border-border/40 last:border-b-0 hover:bg-muted/40 transition-colors">
<div className="shrink-0 flex items-center justify-center size-7 bg-muted/60 text-muted-foreground">
<ScrollText className="size-3.5" />
</div>
<div className="flex flex-col flex-1 min-w-0 gap-0.5">
<span className="text-xs font-semibold truncate text-foreground">
{hostLabel}
</span>
<span className="text-[10px] text-muted-foreground/60 truncate">
{formatDate(log.startedAt)}
{" · "}
{(log.protocol ?? "ssh").toUpperCase()}
{log.username ? ` · ${log.username}` : ""}
{" · "}
{formatDuration(log.duration)}
{" · "}
{formatBytes(log.sizeBytes)}
</span>
</div>
<TooltipProvider>
<div className="shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onView}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<Eye className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left">View log</TooltipContent>
</Tooltip>
{log.format === "asciicast" && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onDownloadText}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<FileText className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left">
{t("sessionLogs.downloadAsText")}
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onDownload}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<Download className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left">Download</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onDelete}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-colors"
>
<X className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left">Delete</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
);
}
export function SessionLogsPanel() {
const { t } = useTranslation();
const [logs, setLogs] = useState<SessionLogRecord[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("");
const [viewLog, setViewLog] = useState<SessionLogRecord | null>(null);
const [viewContent, setViewContent] = useState<string>("");
const [viewBlob, setViewBlob] = useState<Blob | null>(null);
const [viewText, setViewText] = useState<string | null>(null);
const [viewLoading, setViewLoading] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<SessionLogRecord | null>(
null,
);
const [deleting, setDeleting] = useState(false);
const [copied, setCopied] = useState(false);
const [retentionDays, setRetentionDays] = useState<number | null>(null);
const [savingRetention, setSavingRetention] = useState(false);
const logsRef = useRef(logs);
logsRef.current = logs;
const load = useCallback(
async (initial = false) => {
if (initial) setLoading(true);
try {
const fresh = await getSessionLogs();
const changed =
logsRef.current.length !== fresh.length ||
logsRef.current.some((log, i) => log.id !== fresh[i]?.id);
if (changed) {
logsRef.current = fresh;
setLogs(fresh);
}
return changed;
} catch (error) {
if (initial) {
toast.error(t("sessionLogs.loadError"));
return false;
}
throw error;
} finally {
if (initial) setLoading(false);
}
},
[t],
);
useEffect(() => {
void load(true);
getSessionRecordingRetention()
.then(setRetentionDays)
.catch(() => setRetentionDays(null));
}, [load]);
useAdaptivePolling(
() => load(false),
{
minIntervalMs: 5_000,
maxIntervalMs: 30_000,
stablePollsPerStep: 3,
},
true,
{ runImmediately: 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);
setViewText(null);
setViewLoading(true);
try {
if (log.format === "text") {
const text = await getSessionLogContent(log.id);
setViewContent(text);
setViewText(text);
} else {
const blob = await getSessionLogBlob(log.id);
setViewBlob(blob);
setViewText(await extractPlainText(log, blob));
}
} catch {
toast.error(t("sessionLogs.loadError"));
} finally {
setViewLoading(false);
}
};
const downloadBlob = (blob: Blob, filename: string) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
const handleDownload = async (log: SessionLogRecord) => {
try {
downloadBlob(await getSessionLogBlob(log.id), buildFilename(log));
} catch {
toast.error(t("sessionLogs.loadError"));
}
};
const handleDownloadText = async (log: SessionLogRecord) => {
try {
const text =
log.id === viewLog?.id && viewText != null
? viewText
: await extractPlainText(log, await getSessionLogBlob(log.id));
if (text == null) {
toast.error(t("sessionLogs.loadError"));
return;
}
downloadBlob(
new Blob([text], { type: "text/plain" }),
buildTextFilename(log),
);
} 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 (!viewText) return;
const ok = await copyToClipboard(viewText);
if (ok) {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} else {
toast.error(t("common.copyFailed"));
}
};
if (loading) {
return (
<div className="flex items-center justify-center flex-1 p-8">
<Loader2 className="size-5 text-muted-foreground animate-spin" />
</div>
);
}
// Inline log viewer
if (viewLog) {
const hostLabel =
viewLog.hostName ?? viewLog.hostIp ?? `Host ${viewLog.hostId}`;
return (
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
{/* Viewer header */}
<div className="flex items-center gap-2 px-2 py-2 border-b border-border/60 bg-muted/20 shrink-0">
<button
onClick={() => setViewLog(null)}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<ArrowLeft className="size-3.5" />
</button>
<div className="flex flex-col flex-1 min-w-0">
<span className="text-xs font-semibold truncate text-foreground">
{hostLabel}
</span>
<span className="text-[10px] text-muted-foreground/50">
{formatDate(viewLog.startedAt)}
{` · ${viewLog.protocol.toUpperCase()}`}
{viewLog.duration != null
? ` · ${formatDuration(viewLog.duration)}`
: ""}
{" · "}
{formatBytes(viewLog.sizeBytes)}
</span>
</div>
<TooltipProvider>
<div className="flex items-center gap-0.5 shrink-0">
{viewText != null && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleCopy}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
{copied ? (
<Check className="size-3 text-green-500" />
) : (
<Copy className="size-3" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="left">
{copied
? t("sessionLogs.copied")
: t("sessionLogs.copyContent")}
</TooltipContent>
</Tooltip>
)}
{viewLog.format === "asciicast" && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => handleDownloadText(viewLog)}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<FileText className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left">
{t("sessionLogs.downloadAsText")}
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => handleDownload(viewLog)}
className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
>
<Download className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left">
{t("sessionLogs.downloadLog")}
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
{/* Log content */}
<div className="flex-1 min-h-0 overflow-y-auto">
{viewLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="size-4 animate-spin text-muted-foreground" />
</div>
) : viewBlob ? (
<SessionRecordingPlayer log={viewLog} blob={viewBlob} />
) : (
<pre className="p-3 text-[11px] font-mono whitespace-pre-wrap break-all text-foreground/80 leading-relaxed">
{viewContent || "(empty)"}
</pre>
)}
</div>
</div>
);
}
return (
<div className="flex flex-col flex-1 min-h-0">
<div className="flex-1 overflow-y-auto">
{retentionDays != null && (
<div className="flex items-center gap-2 border-b border-border/60 px-3 py-2 text-[10px] text-muted-foreground">
<span className="flex-1">Recording retention</span>
<Input
type="number"
min={1}
max={3650}
value={retentionDays}
onChange={(event) => setRetentionDays(Number(event.target.value))}
className="h-7 w-20 text-xs"
aria-label="Recording retention days"
/>
<span>days</span>
<button
type="button"
disabled={savingRetention}
onClick={async () => {
setSavingRetention(true);
try {
await setSessionRecordingRetention(retentionDays);
toast.success("Recording retention updated");
} catch {
toast.error("Failed to update recording retention");
} finally {
setSavingRetention(false);
}
}}
className="flex size-7 items-center justify-center hover:bg-muted disabled:opacity-50"
aria-label="Save recording retention"
>
{savingRetention ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Save className="size-3" />
)}
</button>
</div>
)}
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center flex-1 gap-3 p-6 text-center py-16">
<div className="size-10 bg-muted/40 flex items-center justify-center">
<ScrollText className="size-5 text-muted-foreground/30" />
</div>
<div className="flex flex-col gap-1">
<span className="text-sm font-semibold text-muted-foreground/60">
{t("sessionLogs.noLogs")}
</span>
<span className="text-xs text-muted-foreground/40">
{t("sessionLogs.noLogsDesc")}
</span>
</div>
</div>
) : (
<>
<div className="relative px-3 py-2 border-b border-border/60">
<Search className="absolute left-5.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/50 pointer-events-none" />
<Input
placeholder={t("sessionLogs.filterByHost")}
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="pl-8 h-7 text-xs"
/>
</div>
{filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-10 text-center">
<span className="text-xs text-muted-foreground/50">
No results for &quot;{filter}&quot;
</span>
</div>
) : (
<div className="flex flex-col">
<SectionHeader
label={t("sessionLogs.title")}
count={filtered.length}
/>
{filtered.map((log) => (
<LogRow
key={log.id}
log={log}
onView={() => handleView(log)}
onDownload={() => handleDownload(log)}
onDownloadText={() => handleDownloadText(log)}
onDelete={() => setDeleteTarget(log)}
/>
))}
</div>
)}
</>
)}
</div>
{/* Inline delete confirmation - positioned against the relative parent in AppShell */}
{deleteTarget && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
<div className="bg-popover border border-border shadow-xl w-full max-w-xs flex flex-col gap-4 p-4">
<p className="text-sm text-foreground">
{t("sessionLogs.confirmDelete")}
</p>
<p className="text-xs text-muted-foreground">
{deleteTarget.hostName ??
deleteTarget.hostIp ??
`Session ${deleteTarget.id}`}
{" · "}
{formatDate(deleteTarget.startedAt)}
</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setDeleteTarget(null)}
className="px-3 py-1.5 text-xs border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
{t("common.cancel")}
</button>
<button
onClick={handleDelete}
disabled={deleting}
className="px-3 py-1.5 text-xs bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors flex items-center gap-1.5"
>
{deleting && <Loader2 className="size-3 animate-spin" />}
{t("sessionLogs.deleteLog")}
</button>
</div>
</div>
</div>
)}
</div>
);
}
-9
View File
@@ -5,7 +5,6 @@ import {
KeyRound,
LayoutPanelLeft,
Plug,
ScrollText,
Server,
Settings,
User,
@@ -113,14 +112,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
promotable: true,
rightDockable: true,
},
{
id: "session-logs",
icon: ScrollText,
labelKey: "nav.sessionLogs",
separatorAfter: true,
promotable: true,
rightDockable: true,
},
{
id: "split-screen",
icon: LayoutPanelLeft,
@@ -1,20 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseAsciicast } from "../../../features/session-recording/asciicast";
describe("parseAsciicast", () => {
it("parses terminal dimensions and timed input/output", () => {
const recording = parseAsciicast(
'{"version":2,"width":120,"height":30}\n[0.1,"o","hello"]\n[1.5,"i","ls\\r"]\n[2,"r","100x40"]\n',
);
expect(recording.width).toBe(120);
expect(recording.height).toBe(30);
expect(recording.duration).toBe(2);
expect(recording.events).toHaveLength(3);
});
it("rejects unsupported recording versions", () => {
expect(() => parseAsciicast('{"version":1}\n')).toThrow(
"Unsupported asciicast version",
);
});
});
+4 -9
View File
@@ -50,7 +50,8 @@ describe("RAIL_ITEMS", () => {
// from this one list.
// "sftp" moved to the file-manager plugin's own registerRailItem,
// "port-forwarding" to the tunnels plugin's, "serial" to the serial
// plugin's, and "collab" to the session-sharing plugin's.
// plugin's, "collab" to the session-sharing plugin's, and "session-logs"
// to the session-recording plugin's.
expect(RAIL_ITEMS.map((item) => item.id)).toEqual([
"hosts",
"credentials",
@@ -59,7 +60,6 @@ describe("RAIL_ITEMS", () => {
"quick-connect",
"ssh-tools",
"macros",
"session-logs",
"split-screen",
]);
});
@@ -81,7 +81,7 @@ describe("RAIL_ITEMS", () => {
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
.filter((item) => item.promotable)
.map((item) => item.id),
).toEqual(["termix-id", "ssh-tools", "macros", "session-logs"]);
).toEqual(["termix-id", "ssh-tools", "macros"]);
});
it("derives promotableIds from the promotable flag", () => {
@@ -118,12 +118,7 @@ describe("RAIL_ITEMS", () => {
});
it("only offers reference panels in the right dock", () => {
expect(rightDockableIds()).toEqual([
"connections",
"ssh-tools",
"macros",
"session-logs",
]);
expect(rightDockableIds()).toEqual(["connections", "ssh-tools", "macros"]);
});
});