/** * @file MemberTmux.tsx * @description Each member's own tmux server as part of the Administration * page's users table (one Linux user per account only, `GET /api/users/tmux`, * `useMemberTmux`): a `TmuxCell` per row — the version it runs, a badge when * older than the installed one, its sessions, whether an agent of theirs is * working — and `MemberTmuxBar` above the table, 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 { 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 { 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`; /** `GET /api/users/tmux`; `available` false without one Linux user per account. */ export function useMemberTmux(enabled = true) { const [data, setData] = useState(null); const reload = useCallback( () => api.users .tmux() .then(setData) .catch(() => setData(null)), [] ); useEffect(() => { if (enabled) void reload(); }, [enabled, reload]); return { data, reload }; } /** One member's tmux server as a users-table cell: version, badges, sessions, agents. */ export function TmuxCell({ row }: { row?: MemberTmuxRow }) { const { t } = useTranslation("users"); if (!row?.running) return —; return (
{row.stuck ? "?" : (row.version ?? "?")} {row.stuck && {t("tmux.stuck")}} {row.outdated && {t("tmux.outdated")}}
{t("tmux.sessionsCount", { count: row.sessions ?? 0 })} ·{" "} {row.busy > 0 ? t("tmux.working", { count: row.busy }) : t("tmux.idle")}
); } /** * Above the users table: the installed tmux, the not-yet-installed tmux-member * warning, and (an admin) "Restart N idle members". Nothing without S3. */ export function MemberTmuxBar({ data, isAdmin, onRestart, }: { data: MemberTmux | null; isAdmin: boolean; onRestart: (target: "idle") => void; }) { const { t } = useTranslation("users"); if (!data?.available) return null; const idle = data.users.filter((u) => u.running && u.outdated && u.busy === 0); return (
{t("tmux.description", { version: data.installed ?? "?" })} {!data.matched && {t("tmux.notMatched")}} {isAdmin && idle.length > 0 && ( )}
); } /** An admin's restart of one member's tmux, or of every idle outdated one. */ export function TmuxRestartDialog({ target, data, onClose, onDone, }: { target: MemberTmuxRow | "idle"; data: MemberTmux | null; onClose: () => void; onDone: (message: string) => void; }) { const idle = (data?.users ?? []).filter((u) => u.running && u.outdated && u.busy === 0); return ; } 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 ( ); }