/** * @file MemberTmuxCard.tsx * @description The Users page's tmux card (one Linux user per account only, * `GET /api/users/tmux`): each member's own tmux server — the version it runs * beside the installed one, its sessions, whether an agent of theirs is * working — so an admin sees who still runs an old tmux after an upgrade. * An admin restarts one member's server, or every idle outdated one at once * (`POST /api/users/:id/tmux-restart`, `/api/users/tmux-restart-idle`), each * time with their own password again; a restart ends that member's panes, * and the next start runs the installed tmux. An analyst only reads. * @author Nguyễn Ngọc Trí Vĩ */ import { useCallback, useEffect, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { RefreshCw, RotateCcw, TerminalSquare } from "lucide-react"; import { api, type MemberTmux, type MemberTmuxRow } from "../../lib/api"; import { formatDateTime } from "../../lib/format"; import { toast } from "../../lib/toast"; import { isSubmitEnter } from "../../lib/keys"; import { useSpin } from "../../hooks/useSpin"; import { ConfirmModal } from "../ConfirmModal"; function errorText(t: TFunction<"users">, e: unknown): string { const err = e as Error & { code?: string }; return err.code && t(`tmux.errors.${err.code}`, { defaultValue: "" }) ? t(`tmux.errors.${err.code}`) : err.message; } const BADGE = "badge ml-1.5 py-0 text-[10px]"; const WARN = `${BADGE} bg-status-warning/10 text-status-warning border-status-warning/20`; const DANGER = `${BADGE} bg-status-danger/10 text-status-danger border-status-danger/20`; export function MemberTmuxCard({ isAdmin }: { isAdmin: boolean }) { const { t } = useTranslation("users"); const [data, setData] = useState(null); const [error, setError] = useState(null); // A member row = restart that one; "idle" = every idle outdated member. const [restarting, setRestarting] = useState(null); const load = useCallback( () => api.users .tmux() .then((r) => { setData(r); setError(null); }) .catch((e: Error) => setError(e.message)), [] ); const [spinning, refresh] = useSpin(load); useEffect(() => { void load(); }, [load]); if (!data?.available) return null; const rows = data.users.filter((u) => u.running); const idleOutdated = rows.filter((u) => u.outdated && u.busy === 0); const th = "px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"; return (

{t("tmux.title")}

{t("tmux.description", { version: data.installed ?? "?" })}

{isAdmin && idleOutdated.length > 0 && ( )}
{!data.matched && (

{t("tmux.notMatched")}

)} {error && (

{error}

)} {rows.length === 0 ? (

{t("tmux.empty")}

) : (
{isAdmin && } {rows.map((u) => ( {isAdmin && ( )} ))}
{t("tmux.user")} {t("tmux.version")} {t("tmux.sessions")} {t("tmux.agents")}{t("actions")}
{u.username} {u.stuck ? "?" : (u.version ?? "?")} {u.stuck && {t("tmux.stuck")}} {u.outdated && {t("tmux.outdated")}} {u.startedAt && (
{t("tmux.since", { at: formatDateTime(new Date(u.startedAt).toISOString()), })}
)}
{u.sessions ?? "—"} {u.busy > 0 ? t("tmux.working", { count: u.busy }) : t("tmux.idle")}
)} {restarting && ( setRestarting(null)} onDone={(msg) => { toast.success(msg); setRestarting(null); void load(); }} /> )}
); } function RestartDialog({ target, idle, onClose, onDone, }: { target: MemberTmuxRow | "idle"; idle: MemberTmuxRow[]; onClose: () => void; onDone: (message: string) => void; }) { const { t } = useTranslation("users"); const [current, setCurrent] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const one = target === "idle" ? null : target; const submit = async () => { if (!current || busy) return; setBusy(true); setError(null); try { if (one) { await api.users.tmuxRestart(one.id, current); onDone(t("tmux.done", { name: one.username })); } else { const r = await api.users.tmuxRestartIdle(current); // A server that would not stop needs root: say which, the rest went. if (r.failed.length) toast.error(r.failed.map((f) => f.message).join("\n")); onDone(t("tmux.doneIdle", { names: r.restarted.join(", ") || "—" })); } } catch (e) { setError(errorText(t, e)); } finally { setBusy(false); } }; return ( 0 ? "tmux.restartHintBusy" : "tmux.restartHint", { name: one.username, count: one.sessions ?? 0, }) : t("tmux.restartIdleHint", { names: idle.map((u) => u.username).join(", ") }) } confirmLabel={t("tmux.restartButton")} cancelLabel={t("common:cancel")} busy={busy} disabled={!current} onConfirm={() => void submit()} onCancel={onClose} >
{ if (isSubmitEnter(e)) { e.preventDefault(); void submit(); } }} > setCurrent(e.target.value)} autoComplete="current-password" aria-label={t("yourPassword")} /> {error && (

{error}

)}
); } function Field({ label, hint, children }: { label: string; hint?: string; children: ReactNode }) { return ( ); }