// АРМ администратора: учётные записи, состояние стенда, журнал аудита, копии. // // Того, чего администратору нельзя по ТЗ, здесь нет вовсе — ни правки оценок, // ни вмешательства в занятие. Запрет выражен отсутствием кнопки, а не // disabled-атрибутом (lct-24). import { useState } from "react"; import { useLogout, useMe } from "@/features/auth/useAuth"; import type { Role } from "@/shared/types/generated"; import { useAudit, useBackups, useCreateBackup, useCreateUser, useDiagnostics, usePatchUser, useStatus, useUsers, } from "./api"; const ROLE_LABELS: Record = { admin: "Администратор", instructor: "Преподаватель", trainee: "Обучающийся", }; export function Admin() { const me = useMe(); const logout = useLogout(); return (

АРМ администратора

{me.data?.full_name}{" "}

Скачать XML-конфигурацию рабочих мест

); } function bytes(value: number | null) { if (value === null) return "нет данных ОС"; if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(1)} ГБ`; return `${Math.round(value / 1024 ** 2)} МБ`; } function Diagnostics() { const report = useDiagnostics(); const metrics = report.data?.metrics; const systemEvents = report.data?.recent_system_events ?? []; const failedActions = report.data?.failed_actions_24h ?? []; return ( <>

Нагрузка и диагностика

{metrics && (
CPU backend{metrics.cpu_percent.toFixed(1)}% · {metrics.cpu_cores} ядер на сервере
Нагрузка сервера, 1 мин{metrics.load_1m_percent === null ? "нет счётчика ОС" : `${metrics.load_1m_percent.toFixed(1)}%`}
Память backend{bytes(metrics.rss_bytes)}
Свободная память сервера{bytes(metrics.memory_available_bytes)} из {bytes(metrics.memory_total_bytes)}
Свободно на диске{bytes(metrics.disk_free_bytes)} из {bytes(metrics.disk_total_bytes)}
Занятияактивно {metrics.active_sessions}; завершено за 24 ч {metrics.completed_sessions_24h}; восстановлено после запуска {metrics.restored_sessions}
Процессработает {Math.floor(metrics.uptime_seconds / 60)} мин; потоков {metrics.threads}
)} {report.isError &&

Диагностика недоступна: {String(report.error)}

}

Скачать отчёт об ошибках и сбоях JSON

Сбои действий за 24 часа

{failedActions.length === 0 ?

Зафиксированных сбоев действий нет.

: ( {failedActions.map((event, index) => ( ))}
КогдаДействиеКтоПодробности
{new Date(event.at).toLocaleString("ru-RU")} {event.action}{event.actor}{event.detail}
)}
Предупреждения процесса ({systemEvents.length}) {systemEvents.length === 0 ?

Предупреждений после запуска нет.

: ( {systemEvents.map((event, index) => ( ))}
КогдаУровеньИсточникСообщение
{new Date(event.at).toLocaleString("ru-RU")} {event.level}{event.source}{event.message}
)}
); } function Status() { const status = useStatus(); return ( <>

Состояние стенда

{(status.data ?? []).map((item) => ( ))}
{item.name} {item.detail}
); } function Users() { const users = useUsers(); const create = useCreateUser(); const patch = usePatchUser(); const [form, setForm] = useState({ login: "", full_name: "", password: "", role: "trainee" as Role, service: "" }); return ( <>

Учётные записи

{(users.data ?? []).map((user) => ( ))}
ЛогинФИОРольСлужбаСостояние
{user.login} {user.full_name} {ROLE_LABELS[user.role]} {user.service ?? "—"}

Завести запись

{ event.preventDefault(); create.mutate( { ...form, service: form.service || null }, { onSuccess: () => setForm({ ...form, login: "", full_name: "", password: "" }) }, ); }} > {/* Служба осмысленна только у обучающегося: по ней в его ленту попадают профильные события (docs/spec/DATASET.md). */} {form.role === "trainee" && ( )}
Логин setForm({ ...form, login: e.target.value })} />
ФИО setForm({ ...form, full_name: e.target.value })} />
Пароль setForm({ ...form, password: e.target.value })} />{" "} от восьми до 1024 знаков
Роль
Служба ДДС setForm({ ...form, service: e.target.value })} />

{create.isError && Логин уже занят}

); } function Backups() { const backups = useBackups(); const create = useCreateBackup(); return ( <>

Резервные копии

{create.isError && {String(create.error)}}

{(backups.data ?? []).map((item) => ( ))}
{item.name} {Math.round(item.size_bytes / 1024)} КБ {new Date(item.at).toLocaleString("ru-RU")}
{(backups.data ?? []).length === 0 &&

Копий пока нет.

} ); } function Audit() { const [action, setAction] = useState(""); const [actor, setActor] = useState(""); const [offset, setOffset] = useState(0); const audit = useAudit(action, actor, offset); const pageSize = 200; const rows = audit.data ?? []; return ( <>

Журнал действий

Действие: { setAction(e.target.value); setOffset(0); }} /> {" "}Пользователь: { setActor(e.target.value); setOffset(0); }} />

{rows.slice(0, pageSize).map((row, index) => ( ))}
КогдаКтоДействиеНад чемПодробности
{new Date(row.at).toLocaleString("ru-RU")} {row.actor} {row.action} {row.object_id ?? "—"} {row.detail}

{" "} Записи {rows.length ? offset + 1 : 0}–{offset + Math.min(rows.length, pageSize)}{" "}

); }