Complete DDS training workflow and delivery package

This commit is contained in:
andreysk0304 2026-09-24 01:10:49 +03:00
commit 4c4b91064f
229 changed files with 11969 additions and 1024 deletions

View file

@ -10,7 +10,8 @@ import { useLogout, useMe } from "@/features/auth/useAuth";
import type { Role } from "@/shared/types/generated";
import {
useAudit, useBackups, useCreateBackup, useCreateUser, usePatchUser, useStatus, useUsers,
useAudit, useBackups, useCreateBackup, useCreateUser, useDiagnostics, usePatchUser, useStatus,
useUsers,
} from "./api";
const ROLE_LABELS: Record<Role, string> = {
@ -31,6 +32,8 @@ export function Admin() {
<button type="button" onClick={() => logout.mutate()}>Выйти</button>
</p>
<Status />
<Diagnostics />
<p><a href="/api/admin/config.xml">Скачать XML-конфигурацию рабочих мест</a></p>
<Users />
<Backups />
<Audit />
@ -38,6 +41,65 @@ export function Admin() {
);
}
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 (
<>
<h2>Нагрузка и диагностика</h2>
{metrics && (
<table className="grid">
<tbody>
<tr><th>CPU backend</th><td>{metrics.cpu_percent.toFixed(1)}% · {metrics.cpu_cores} ядер на сервере</td></tr>
<tr><th>Нагрузка сервера, 1 мин</th><td>{metrics.load_1m_percent === null ? "нет счётчика ОС" : `${metrics.load_1m_percent.toFixed(1)}%`}</td></tr>
<tr><th>Память backend</th><td>{bytes(metrics.rss_bytes)}</td></tr>
<tr><th>Свободная память сервера</th><td>{bytes(metrics.memory_available_bytes)} из {bytes(metrics.memory_total_bytes)}</td></tr>
<tr><th>Свободно на диске</th><td>{bytes(metrics.disk_free_bytes)} из {bytes(metrics.disk_total_bytes)}</td></tr>
<tr><th>Занятия</th><td>активно {metrics.active_sessions}; завершено за 24 ч {metrics.completed_sessions_24h}; восстановлено после запуска {metrics.restored_sessions}</td></tr>
<tr><th>Процесс</th><td>работает {Math.floor(metrics.uptime_seconds / 60)} мин; потоков {metrics.threads}</td></tr>
</tbody>
</table>
)}
{report.isError && <p className="warn">Диагностика недоступна: {String(report.error)}</p>}
<p><a href="/api/admin/diagnostics.json">Скачать отчёт об ошибках и сбоях JSON</a></p>
<h3>Сбои действий за 24 часа</h3>
{failedActions.length === 0 ? <p>Зафиксированных сбоев действий нет.</p> : (
<table className="grid">
<thead><tr><th>Когда</th><th>Действие</th><th>Кто</th><th>Подробности</th></tr></thead>
<tbody>{failedActions.map((event, index) => (
<tr key={`${event.at}-${index}`}>
<th>{new Date(event.at).toLocaleString("ru-RU")}</th>
<td>{event.action}</td><td>{event.actor}</td><td>{event.detail}</td>
</tr>
))}</tbody>
</table>
)}
<details>
<summary>Предупреждения процесса ({systemEvents.length})</summary>
{systemEvents.length === 0 ? <p>Предупреждений после запуска нет.</p> : (
<table className="grid">
<thead><tr><th>Когда</th><th>Уровень</th><th>Источник</th><th>Сообщение</th></tr></thead>
<tbody>{systemEvents.map((event, index) => (
<tr key={`${event.at}-${index}`}>
<th>{new Date(event.at).toLocaleString("ru-RU")}</th>
<td>{event.level}</td><td>{event.source}</td><td>{event.message}</td>
</tr>
))}</tbody>
</table>
)}
</details>
</>
);
}
function Status() {
const status = useStatus();
return (
@ -104,21 +166,22 @@ function Users() {
<tbody>
<tr>
<th>Логин</th>
<td><input value={form.login} onChange={(e) => setForm({ ...form, login: e.target.value })} /></td>
<td><input value={form.login} maxLength={80} onChange={(e) => setForm({ ...form, login: e.target.value })} /></td>
</tr>
<tr>
<th>ФИО</th>
<td><input value={form.full_name} onChange={(e) => setForm({ ...form, full_name: e.target.value })} /></td>
<td><input value={form.full_name} maxLength={120} onChange={(e) => setForm({ ...form, full_name: e.target.value })} /></td>
</tr>
<tr>
<th>Пароль</th>
<td>
<input
type="password"
maxLength={1024}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>{" "}
не короче восьми знаков
от восьми до 1024 знаков
</td>
</tr>
<tr>
@ -182,20 +245,24 @@ function Backups() {
function Audit() {
const [action, setAction] = useState("");
const audit = useAudit(action);
const [actor, setActor] = useState("");
const [offset, setOffset] = useState(0);
const audit = useAudit(action, actor, offset);
const pageSize = 200;
const rows = audit.data ?? [];
return (
<>
<h2>Журнал действий</h2>
<p>
Фильтр по действию:{" "}
<input value={action} placeholder="lesson.start" onChange={(e) => setAction(e.target.value)} />
Действие: <input value={action} placeholder="login.failed" onChange={(e) => { setAction(e.target.value); setOffset(0); }} />
{" "}Пользователь: <input value={actor} placeholder="логин" onChange={(e) => { setActor(e.target.value); setOffset(0); }} />
</p>
<table className="grid">
<thead>
<tr><th>Когда</th><th>Кто</th><th>Действие</th><th>Над чем</th><th>Подробности</th></tr>
</thead>
<tbody>
{(audit.data ?? []).map((row, index) => (
{rows.slice(0, pageSize).map((row, index) => (
<tr key={index}>
<th>{new Date(row.at).toLocaleString("ru-RU")}</th>
<td>{row.actor}</td>
@ -206,6 +273,11 @@ function Audit() {
))}
</tbody>
</table>
<p>
<button type="button" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Назад</button>{" "}
<span>Записи {rows.length ? offset + 1 : 0}–{offset + Math.min(rows.length, pageSize)}</span>{" "}
<button type="button" disabled={rows.length <= pageSize} onClick={() => setOffset(offset + pageSize)}>Раньше</button>
</p>
</>
);
}