1871 lines
146 KiB
HTML
1871 lines
146 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<script>
|
||
(function () {
|
||
try {
|
||
var root = document.documentElement;
|
||
root.setAttribute("data-theme", "ultra-dark");
|
||
root.classList.add("dark");
|
||
root.classList.remove("light");
|
||
} catch (e) {}
|
||
try {
|
||
document.documentElement.classList.add("wesp-admin-loading");
|
||
} catch (e2) {}
|
||
})();
|
||
</script>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WESP Admin</title>
|
||
<!-- Ant Design (MIT) + тема админки; файлы в /static/vendor/admin-ui/ -->
|
||
<link rel="stylesheet" href="/static/vendor/admin-ui/antd.min.css">
|
||
<link rel="stylesheet" href="/static/vendor/admin-ui/custom.min.css">
|
||
<link rel="stylesheet" href="/static/css/wesp-admin-panel.css?v=20260714">
|
||
<link rel="stylesheet" href="/static/css/wesp-logo-loader.css">
|
||
<link rel="stylesheet" href="/static/css/wesp-setup.css">
|
||
<link rel="stylesheet" href="/static/css/wesp-startup.css">
|
||
</head>
|
||
<body>
|
||
<div id="app" class="ant-layout ant-layout-has-sider" style="min-height: 100vh">
|
||
<aside
|
||
id="wespAdminSider"
|
||
class="ant-layout-sider ant-layout-sider-dark ant-layout-sider-has-trigger ant-layout-sider-collapsed wesp-admin-sider"
|
||
>
|
||
<div class="ant-layout-sider-children">
|
||
<div class="wesp-admin-brand">
|
||
<div class="wesp-admin-brand-title">WESP</div>
|
||
</div>
|
||
<!-- Иконки: встроенный SVG (контуры в духе открытых наборов Lucide/MIT, без внешних шрифтов) -->
|
||
<ul
|
||
role="menu"
|
||
class="ant-menu ant-menu-dark ant-menu-root ant-menu-inline"
|
||
style="border-right: none;"
|
||
>
|
||
<li class="ant-menu-item ant-menu-item-selected" role="menuitem" data-section="dashboard" title="Дашборд">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg></span>
|
||
<span class="wesp-menu-label">Дашборд</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="system" title="Система">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg></span>
|
||
<span class="wesp-menu-label">Система</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="auto-update" title="Обновления (Gitea)">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg></span>
|
||
<span class="wesp-menu-label">Обновления</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="sync" title="Синхронизация">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg></span>
|
||
<span class="wesp-menu-label">Синхронизация</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="network" title="Сеть: имя в LAN, mDNS, публичный URL">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg></span>
|
||
<span class="wesp-menu-label">Сеть</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="security" title="Безопасность">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/></svg></span>
|
||
<span class="wesp-menu-label">Безопасность</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="hardware" title="Железо">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg></span>
|
||
<span class="wesp-menu-label">Железо</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="diagnostics" title="Диагностика: сеть, безопасность, периферия">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></span>
|
||
<span class="wesp-menu-label">Диагностика</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="users" title="Пользователи">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>
|
||
<span class="wesp-menu-label">Пользователи</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="assistant" title="Локальный ИИ: чат, сводки, диагностика (без облака)">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></span>
|
||
<span class="wesp-menu-label">Ассистент</span>
|
||
</li>
|
||
<li class="ant-menu-item" role="menuitem" data-section="links" title="Переходы">
|
||
<span class="wesp-menu-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>
|
||
<span class="wesp-menu-label">Переходы</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div
|
||
class="ant-layout-sider-trigger"
|
||
id="wespAdminSiderTrigger"
|
||
role="button"
|
||
tabindex="0"
|
||
aria-controls="wespAdminSider"
|
||
aria-expanded="false"
|
||
title="Развернуть меню"
|
||
>
|
||
<span class="wesp-sider-trigger-icon" aria-hidden="true">▶</span>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="ant-layout" id="content-layout">
|
||
<main class="ant-layout-content" style="margin: 24px 16px; padding: 0; min-height: auto;">
|
||
<div class="wesp-admin-content-topbar">
|
||
<div class="wesp-admin-content-topbar-actions">
|
||
<a class="ant-btn" href="/recipes"><span>Назад в рецепты</span></a>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="refreshSummaryBtn">
|
||
<span>Обновить</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="wesp-admin-section is-active" data-section-content="dashboard">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-body">
|
||
<!-- Сетка метрик: две колонки md-12, по два круговых индикатора (ant-col-12). -->
|
||
<div class="ant-row" id="systemGaugesGrid" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-sm-24 ant-col-md-12" style="padding: 8px;">
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="text-center ant-col ant-col-12" style="padding: 8px;">
|
||
<div class="wesp-gauge" data-gauge="cpu">
|
||
<div class="wesp-gauge-ring wesp-gauge-ring--adm">
|
||
<svg class="wesp-gauge-svg" viewBox="0 0 100 100" aria-hidden="true">
|
||
<circle class="wesp-gauge-track" cx="50" cy="50" r="47" fill="none" stroke-width="6"/>
|
||
<circle class="wesp-gauge-fill wesp-gauge-fill--adm" cx="50" cy="50" r="47" fill="none" stroke-width="6"
|
||
stroke-linecap="round" transform="rotate(-90 50 50)"/>
|
||
</svg>
|
||
<div class="wesp-gauge-center">
|
||
<span class="wesp-gauge-pct" data-gauge-pct>—</span><span class="wesp-gauge-pct-suffix">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-gauge-caption wesp-gauge-caption--with-btn">
|
||
<span class="wesp-gauge-caption-text"><b>CPU</b> <span data-gauge-detail>—</span></span>
|
||
<button
|
||
type="button"
|
||
class="ant-btn ant-btn-circle ant-btn-sm wesp-gauge-history-btn"
|
||
id="btnDashHardwareChart"
|
||
title="load timeseries; retention per summary poll"
|
||
aria-label="Open load chart"
|
||
>
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="64 64 896 896" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"><path d="M536.1 273H488c-4.4 0-8 3.6-8 8v275.3c0 2.6 1.2 5 3.3 6.5l165.3 120.7c3.6 2.6 8.6 1.9 11.2-1.7l28.6-39c2.7-3.7 1.9-8.7-1.7-11.2L544.1 528.5V281c0-4.4-3.6-8-8-8zm219.8 75.2l156.8 38.3c5 1.2 9.9-2.6 9.9-7.7l.8-161.5c0-6.7-7.7-10.5-12.9-6.3L752.9 334.1a8 8 0 0 0 3 14.1zm167.7 301.1l-56.7-19.5a8 8 0 0 0-10.1 4.8c-1.9 5.1-3.9 10.1-6 15.1-17.8 42.1-43.3 80-75.9 112.5a353 353 0 0 1-112.5 75.9 352.18 352.18 0 0 1-137.7 27.8c-47.8 0-94.1-9.3-137.7-27.8a353 353 0 0 1-112.5-75.9c-32.5-32.5-58-70.4-75.9-112.5A353.44 353.44 0 0 1 171 512c0-47.8 9.3-94.2 27.8-137.8 17.8-42.1 43.3-80 75.9-112.5a353 353 0 0 1 112.5-75.9C430.6 167.3 477 158 524.8 158s94.1 9.3 137.7 27.8A353 353 0 0 1 775 261.7c10.2 10.3 19.8 21 28.6 32.3l59.8-46.8C784.7 146.6 662.2 81.9 524.6 82 285 82.1 92.6 276.7 95 516.4 97.4 751.9 288.9 942 524.8 942c185.5 0 343.5-117.6 403.7-282.3 1.5-4.2-.7-8.9-4.9-10.4z"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="text-center ant-col ant-col-12" style="padding: 8px;">
|
||
<div class="wesp-gauge" data-gauge="ram">
|
||
<div class="wesp-gauge-ring wesp-gauge-ring--adm">
|
||
<svg class="wesp-gauge-svg" viewBox="0 0 100 100" aria-hidden="true">
|
||
<circle class="wesp-gauge-track" cx="50" cy="50" r="47" fill="none" stroke-width="6"/>
|
||
<circle class="wesp-gauge-fill wesp-gauge-fill--adm" cx="50" cy="50" r="47" fill="none" stroke-width="6"
|
||
stroke-linecap="round" transform="rotate(-90 50 50)"/>
|
||
</svg>
|
||
<div class="wesp-gauge-center">
|
||
<span class="wesp-gauge-pct" data-gauge-pct>—</span><span class="wesp-gauge-pct-suffix">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-gauge-caption"><b>RAM</b> <span data-gauge-detail>—</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-sm-24 ant-col-md-12" style="padding: 8px;">
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="text-center ant-col ant-col-12" style="padding: 8px;">
|
||
<div class="wesp-gauge" data-gauge="swap">
|
||
<div class="wesp-gauge-ring wesp-gauge-ring--adm">
|
||
<svg class="wesp-gauge-svg" viewBox="0 0 100 100" aria-hidden="true">
|
||
<circle class="wesp-gauge-track" cx="50" cy="50" r="47" fill="none" stroke-width="6"/>
|
||
<circle class="wesp-gauge-fill wesp-gauge-fill--adm" cx="50" cy="50" r="47" fill="none" stroke-width="6"
|
||
stroke-linecap="round" transform="rotate(-90 50 50)"/>
|
||
</svg>
|
||
<div class="wesp-gauge-center">
|
||
<span class="wesp-gauge-pct" data-gauge-pct>—</span><span class="wesp-gauge-pct-suffix">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-gauge-caption"><b>swap</b> <span data-gauge-detail>—</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="text-center ant-col ant-col-12" style="padding: 8px;">
|
||
<div class="wesp-gauge" data-gauge="disk">
|
||
<div class="wesp-gauge-ring wesp-gauge-ring--adm">
|
||
<svg class="wesp-gauge-svg" viewBox="0 0 100 100" aria-hidden="true">
|
||
<circle class="wesp-gauge-track" cx="50" cy="50" r="47" fill="none" stroke-width="6"/>
|
||
<circle class="wesp-gauge-fill wesp-gauge-fill--adm" cx="50" cy="50" r="47" fill="none" stroke-width="6"
|
||
stroke-linecap="round" transform="rotate(-90 50 50)"/>
|
||
</svg>
|
||
<div class="wesp-gauge-center">
|
||
<span class="wesp-gauge-pct" data-gauge-pct>—</span><span class="wesp-gauge-pct-suffix">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-gauge-caption"><b>disk</b> <span data-gauge-detail>—</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-row wesp-dash-service-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-xl-12" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered wesp-dash-service-card">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">WESP</div>
|
||
<div class="ant-card-extra wesp-dash-status">
|
||
<span class="wesp-status-dot wesp-status-dot--ok" aria-hidden="true"></span>
|
||
<span id="wespRuntimeStatus">OK</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="wesp-dash-wesp-meta">
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">version</span>
|
||
<span class="ant-tag ant-tag-green" id="wespDashVersion">v—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">first_run</span>
|
||
<span class="wesp-dash-wesp-value" id="wespFirstLaunch">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">warranty_until</span>
|
||
<span class="wesp-dash-wesp-value" id="wespWarrantyUntil">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">warranty_days</span>
|
||
<span class="wesp-dash-wesp-value" id="wespWarrantyDays">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-xl-12" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered wesp-dash-service-card">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Ops</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="wesp-dash-manage-list">
|
||
<div class="wesp-dash-manage-row">
|
||
<div class="wesp-dash-manage-info">
|
||
<span class="wesp-dash-manage-title">log</span>
|
||
<span class="wesp-dash-manage-hint">tail; WESP_ADMIN_LOG_PATH</span>
|
||
</div>
|
||
<div class="wesp-dash-manage-actions">
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-primary" id="btnManageLog"><span>open</span></button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnManageLogDownload" title="GET /api/admin/server-log/download"><span>download</span></button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnManageClientLogs" title="Хвост лога с выбранного клиента (выгрузка /api/sync/client-log)"><span>client…</span></button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnManageClientLogsDownload" title="GET /api/admin/client-uploaded-logs/download"><span>client dl</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-dash-manage-row">
|
||
<div class="wesp-dash-manage-info">
|
||
<span class="wesp-dash-manage-title">config</span>
|
||
<span class="wesp-dash-manage-hint">rollup → section System</span>
|
||
</div>
|
||
<div class="wesp-dash-manage-actions">
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnManageConfig"><span>go</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-dash-manage-row">
|
||
<div class="wesp-dash-manage-info">
|
||
<span class="wesp-dash-manage-title">backup</span>
|
||
<span class="wesp-dash-manage-hint">SQLite→ZIP; restore writes *.restore-bak-*</span>
|
||
</div>
|
||
<div class="wesp-dash-manage-actions">
|
||
<input type="file" id="restoreSqliteFile" accept=".zip,application/zip" hidden />
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnManageBackup"><span>GET ZIP</span></button>
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-danger" id="btnManageRestore"><span>restore…</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-dash-manage-row wesp-dash-manage-row--service">
|
||
<div class="wesp-dash-manage-info">
|
||
<span class="wesp-dash-manage-title">service</span>
|
||
<span class="wesp-dash-manage-hint">WESP_ADMIN_STOP_CMD / WESP_ADMIN_RESTART_CMD</span>
|
||
</div>
|
||
<div class="wesp-dash-manage-actions">
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-danger" id="btnWespStop" title="needs WESP_ADMIN_STOP_CMD"><span>stop</span></button>
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-primary" id="btnWespRestart" title="needs WESP_ADMIN_RESTART_CMD"><span>restart</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-lg-12" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered wesp-traffic-card">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">nx throughput (instant)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="wesp-traffic-two-col">
|
||
<div class="wesp-traffic-col">
|
||
<div class="wesp-traffic-label">tx</div>
|
||
<div class="wesp-traffic-value"><span class="wesp-traffic-arrow">↑</span> <span id="netUpload">—</span></div>
|
||
</div>
|
||
<div class="wesp-traffic-col">
|
||
<div class="wesp-traffic-label">rx</div>
|
||
<div class="wesp-traffic-value"><span class="wesp-traffic-arrow">↓</span> <span id="netDownload">—</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-lg-12" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered wesp-traffic-card">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title" title="cumulative since boot (kernel counters)">nx cumulative</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="wesp-traffic-two-col">
|
||
<div class="wesp-traffic-col">
|
||
<div class="wesp-traffic-label">bytes tx</div>
|
||
<div class="wesp-traffic-value"><span class="wesp-traffic-icon" aria-hidden="true">☁↑</span> <span id="netVolumeSent">—</span></div>
|
||
</div>
|
||
<div class="wesp-traffic-col">
|
||
<div class="wesp-traffic-label">bytes rx</div>
|
||
<div class="wesp-traffic-value"><span class="wesp-traffic-icon" aria-hidden="true">☁↓</span> <span id="netVolumeRecv">—</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-xs-24" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">host addrs (L3)</div>
|
||
<div class="ant-card-extra">
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-icon-only wesp-ip-reveal-btn is-ip-hidden" id="btnToggleServerIp" aria-pressed="true" title="reveal / mask">
|
||
<span class="wesp-ip-reveal-btn__icon wesp-ip-reveal-btn__icon--show" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg></span>
|
||
<span class="wesp-ip-reveal-btn__icon wesp-ip-reveal-btn__icon--hide" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/></svg></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div id="serverIpList" class="wesp-server-ip-list wesp-ip-masked">—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-lg-12" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered" style="height: 100%;">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">loadavg 1/5/15</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div
|
||
id="loadAvgPills"
|
||
title="loadavg ≠ CPU%; compare to logical cores (CPU gauge)"
|
||
>—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-lg-12" style="padding: 8px;">
|
||
<div class="ant-card ant-card-bordered" style="height: 100%;">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">uptime · sockets</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body wesp-dash-meta-pills">
|
||
<span class="ant-tag ant-tag-green" id="pillHostUptime">host: —</span>
|
||
<span class="ant-tag ant-tag-green" id="pillWespUptime">wesp: —</span>
|
||
<span class="ant-tag ant-tag-green" id="pillConnections">TCP — · UDP —</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-row" style="margin: 16px -8px 0;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<div
|
||
class="ant-card ant-card-bordered ant-card-small wesp-admin-stat-card wesp-admin-stat-card--clickable"
|
||
data-stat-nav="users"
|
||
role="button"
|
||
tabindex="0"
|
||
title="users · Alt+click: SQLite"
|
||
>
|
||
<div class="ant-card-body" style="padding: 16px;">
|
||
<div style="opacity: 0.65; font-size: 13px;">users</div>
|
||
<div id="statUsers" style="font-size: 26px; font-weight: 600; margin-top: 6px;">-</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<div
|
||
class="ant-card ant-card-bordered ant-card-small wesp-admin-stat-card wesp-admin-stat-card--clickable"
|
||
data-stat-nav="recipes"
|
||
role="button"
|
||
tabindex="0"
|
||
title="recipes · Alt+click: SQLite"
|
||
>
|
||
<div class="ant-card-body" style="padding: 16px;">
|
||
<div style="opacity: 0.65; font-size: 13px;">recipes</div>
|
||
<div id="statRecipes" style="font-size: 26px; font-weight: 600; margin-top: 6px;">-</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<div
|
||
class="ant-card ant-card-bordered ant-card-small wesp-admin-stat-card wesp-admin-stat-card--clickable"
|
||
data-stat-nav="components"
|
||
role="button"
|
||
tabindex="0"
|
||
title="components · Alt+click: SQLite"
|
||
>
|
||
<div class="ant-card-body" style="padding: 16px;">
|
||
<div style="opacity: 0.65; font-size: 13px;">components</div>
|
||
<div id="statComponents" style="font-size: 26px; font-weight: 600; margin-top: 6px;">-</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<div
|
||
class="ant-card ant-card-bordered ant-card-small wesp-admin-stat-card wesp-admin-stat-card--clickable"
|
||
data-stat-nav="sync"
|
||
role="button"
|
||
tabindex="0"
|
||
title="sync · Alt+click: SQLite"
|
||
>
|
||
<div class="ant-card-body" style="padding: 16px;">
|
||
<div style="opacity: 0.65; font-size: 13px;">Sync queue, pending</div>
|
||
<div id="statQueuePending" style="font-size: 26px; font-weight: 600; margin-top: 6px;">-</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-card ant-card-bordered" style="margin-top: 16px;">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">instance rollup</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-md-12" style="padding: 8px;">
|
||
<div class="wesp-dash-summary-block-title">Runtime</div>
|
||
<div class="wesp-dash-wesp-meta">
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">CONFIG_MODE</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaConfigMode">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">DEBUG</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaDebug">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">version</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaVersion">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">sync.role</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaSyncRole">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">first_run</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaFirstLaunch">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">warranty_until</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaWarrantyUntil">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">warranty_days</span>
|
||
<span class="wesp-dash-wesp-value" id="dashMetaWarrantyDays">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-md-12" style="padding: 8px;">
|
||
<div class="wesp-dash-summary-block-title">DB row counts</div>
|
||
<div class="wesp-dash-wesp-meta">
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">users</span>
|
||
<span class="wesp-dash-wesp-value" id="dashCountUsers">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">recipes</span>
|
||
<span class="wesp-dash-wesp-value" id="dashCountRecipes">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">ingredients</span>
|
||
<span class="wesp-dash-wesp-value" id="dashCountIngredients">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">components</span>
|
||
<span class="wesp-dash-wesp-value" id="dashCountComponents">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">feeders</span>
|
||
<span class="wesp-dash-wesp-value" id="dashCountDispensers">—</span>
|
||
</div>
|
||
<div class="wesp-dash-wesp-row">
|
||
<span class="wesp-dash-wesp-label">feed_periods</span>
|
||
<span class="wesp-dash-wesp-value" id="dashCountPeriods">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="wesp-dash-summary-generated" id="dashMetaGenerated">—</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" id="wespSectionSystem" data-section-content="system">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Система</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="systemDataList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Базы данных</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="dbDataList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Киоск Pi — всё сразу</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">
|
||
Одна кнопка: <code>wesp.env</code> + <code>myscript.service</code> +
|
||
киоск Chromium без рабочего стола + перезагрузка.
|
||
Во время настройки — та же анимация, что на <code>/starting</code>.
|
||
</p>
|
||
<p class="wesp-sync-hint" id="kioskFullSetupHint">—</p>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="btnKioskFullSetup" title="env + киоск + reboot">
|
||
<span>Настроить киоск Pi</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Настройка Pi (systemd + wesp.env)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint wesp-sync-hint--muted">
|
||
Отдельные шаги — если нужно только env/systemd без киоска.
|
||
</p>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">
|
||
То же, что <code>install_fresh.sh</code>: <code>/etc/wesp/wesp.env</code>
|
||
(restart, OTA, автоперезагрузка, киоск) и <code>myscript.service</code>.
|
||
Нужен WESP от <strong>root</strong>. После применения служба перезапускается (~2 с).
|
||
</p>
|
||
<p class="wesp-sync-hint" id="piPlatformSetupHint">—</p>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="btnPiPlatformApply" title="wesp.env + systemd enable">
|
||
<span>Применить настройки Pi</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Радужный экран Pi (firmware)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">
|
||
Большой цветной квадрат <strong>до</strong> Plymouth — splash GPU в
|
||
<code>/boot/firmware/config.txt</code>. Добавляет <code>disable_splash=1</code> в секцию
|
||
<code>[all]</code> (нужен sudo).
|
||
</p>
|
||
<p class="wesp-sync-hint" id="piRainbowSplashHint">—</p>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="btnPiRainbowSplashDisable" title="disable_splash=1">
|
||
<span>Отключить радугу</span>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnPiRainbowSplashEnable" title="Убрать disable_splash из config.txt">
|
||
<span>Вернуть (как с завода)</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Заставка загрузки (Plymouth)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">
|
||
Анимация логотипа WESP при включении (800×600, тёмная тема), как в setup/login с тёмной темой.
|
||
Нужны <code>plymouth</code>, <code>python3-pil</code> и sudo для установки темы.
|
||
</p>
|
||
<p class="wesp-sync-hint" id="plymouthStatusHint">—</p>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="btnPlymouthInstall" title="Собрать кадры из logo2.png и установить тему wesp">
|
||
<span>Установить тему Plymouth</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Киоск: Chromium при входе</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">
|
||
Для первой настройки удобнее кнопка <strong>«Настроить киоск Pi»</strong> выше.
|
||
Chromium kiosk: сначала тёмный <code>/starting</code> с подписями, затем
|
||
<code>/scales</code>. Без перевода страницы (расширенные флаги Chromium).
|
||
WESP обычно работает от root (<code>myscript.service</code>) — запись в
|
||
<code>~/.config/autostart/</code> без пароля.
|
||
</p>
|
||
<p class="wesp-sync-hint" id="kioskBootStatusHint">—</p>
|
||
<label class="wesp-sync-hint" style="display:flex;align-items:center;gap:8px;margin-top:8px;">
|
||
<input type="checkbox" id="kioskBootHideDesktop" checked />
|
||
<span>Без рабочего стола (патч <code>/etc/xdg/labwc/autostart</code> — нужен root/WESP)</span>
|
||
</label>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="btnKioskBootEnable" title="chromium-kiosk.desktop">
|
||
<span>Включить киоск</span>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnKioskBootDisable" title="Удалить autostart">
|
||
<span>Выключить киоск</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Сброс данных</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">
|
||
Удаляет содержимое <code>data/</code> (БД, JSON настроек, бэкапы, клиентские каталоги).
|
||
Исходный код и статика не затрагиваются. После сброса перезапустите WESP и пройдите <code>/setup</code>.
|
||
</p>
|
||
<p class="wesp-sync-hint" id="factoryResetPreservedNote" hidden></p>
|
||
<div class="wesp-adm-list-wrap" style="margin: 12px 0;">
|
||
<ul id="factoryResetPreviewList" class="wesp-adm-list" role="list">
|
||
<li class="wesp-adm-list-item"><span class="wesp-adm-list-key">—</span><span class="wesp-adm-list-value">Загрузка…</span></li>
|
||
</ul>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-bottom: 12px;">
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label">
|
||
<input type="checkbox" id="factoryResetSafetyBackup" checked />
|
||
ZIP копия SQLite в data/backups/ перед сбросом
|
||
</label>
|
||
</div>
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label">
|
||
<input type="checkbox" id="factoryResetKeepLogs" />
|
||
Не удалять data/logs
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; align-items: flex-end; gap: 8px;">
|
||
<div class="wesp-sync-field" style="flex: 1; min-width: 200px;">
|
||
<label class="wesp-sync-label" for="factoryResetConfirmPhrase">Фраза подтверждения</label>
|
||
<input id="factoryResetConfirmPhrase" class="ant-input" type="text" autocomplete="off" spellcheck="false" placeholder="СБРОС ДАННЫХ" />
|
||
</div>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="btnFactoryResetPreview"><span>Обновить список</span></button>
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-danger" id="btnFactoryReset"><span>Сбросить данные…</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" id="wespSectionAutoUpdate" data-section-content="auto-update">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Обновления (Gitea)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint" id="auRuntimeHint"></p>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap">
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label"><input type="checkbox" id="auEnabled" /> Проверять обновления</label>
|
||
</div>
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="auCheckInterval">Интервал проверки (сек)</label>
|
||
<input id="auCheckInterval" class="ant-input" type="number" min="60" max="86400" step="1" style="max-width: 120px" />
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-top: 12px">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="auGiteaUrl">Gitea URL</label>
|
||
<input id="auGiteaUrl" class="ant-input" type="text" placeholder="https://git.example.com" autocomplete="off" />
|
||
</div>
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="auGiteaOwner">owner</label>
|
||
<input id="auGiteaOwner" class="ant-input" type="text" autocomplete="off" />
|
||
</div>
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="auGiteaRepo">repo</label>
|
||
<input id="auGiteaRepo" class="ant-input" type="text" autocomplete="off" />
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="margin-top: 12px">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="auRepositoryUrl">Полный URL репозитория (опционально)</label>
|
||
<input id="auRepositoryUrl" class="ant-input" type="text" placeholder="https://git.example.com/owner/repo" autocomplete="off" />
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--actions">
|
||
<label class="wesp-sync-label" for="auSave"> </label>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="auSave"><span>Сохранить</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-top: 16px">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="auGiteaToken">Токен API Gitea</label>
|
||
<input id="auGiteaToken" class="ant-input" type="password" autocomplete="new-password" placeholder="вставьте новый токен, чтобы записать" />
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-top: 12px">
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="auGiteaApiUser">Логин API (basic auth)</label>
|
||
<input id="auGiteaApiUser" class="ant-input" type="text" autocomplete="off" />
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="auGiteaApiPass">Пароль API</label>
|
||
<input id="auGiteaApiPass" class="ant-input" type="password" autocomplete="new-password" />
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-top: 12px; align-items: center">
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label"><input type="checkbox" id="auClearSecrets" /> Удалить секреты из <code>.secret/gitea_secrets.json</code></label>
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--actions">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="auSaveSecrets"><span>Сохранить секреты API</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="sync">
|
||
<div id="syncInitialSyncBanner" class="ant-alert ant-alert-info" style="display:none; margin-bottom: 12px;" role="status">
|
||
<span>Идёт первая синхронизация с сервером. Прогресс — по <strong>успешной записи в локальную БД</strong>, не только по получению пачек с сервера. Дождитесь завершения (флаг <code>first_bootstrap_done</code> в состоянии узла).</span>
|
||
</div>
|
||
<div id="syncInitialSyncProgressWrap" style="display:none; margin-bottom: 12px;" role="region" aria-label="Прогресс первой синхронизации">
|
||
<div class="wesp-sync-progress-bar" aria-hidden="true">
|
||
<div id="syncInitialSyncProgressFill" class="wesp-sync-progress-fill" style="width:0%"></div>
|
||
</div>
|
||
<p id="syncInitialSyncProgressText" class="wesp-sync-progress-text" style="margin:8px 0 0;font-size:13px;color:rgba(0,0,0,0.75);"></p>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap" id="syncDiagnosticsCard">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Диагностика синхронизации</div>
|
||
<div class="ant-card-extra">
|
||
<button type="button" class="ant-btn ant-btn-sm" id="syncDiagRefreshBtn" title="Обновить данные диагностики">
|
||
<span>Обновить</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint" style="padding: 12px 16px 0; margin: 0;" id="syncDiagHint">
|
||
Загрузка… Метрики runtime относятся к <strong>этому</strong> процессу воркера (см. блок Runtime).
|
||
</p>
|
||
<div class="wesp-adm-list-wrap" style="padding: 12px 16px;">
|
||
<ul id="syncDiagOverviewList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Runtime (process-local)</div>
|
||
<ul id="syncDiagRuntimeList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Очередь (серверная БД)</div>
|
||
<ul id="syncDiagQueueStatsList" class="wesp-adm-list" role="list"></ul>
|
||
<div id="syncDiagByTableWrap" class="wesp-sync-diag-table-wrap"></div>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Bootstrap / первая синхронизация</div>
|
||
<ul id="syncDiagBootstrapList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Push отчётов (локальная очередь на клиенте)</div>
|
||
<ul id="syncDiagReportPushList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Проблемные задачи</div>
|
||
<div id="syncDiagProblemsWrap" class="wesp-sync-diag-table-wrap"></div>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Конфликты</div>
|
||
<ul id="syncDiagConflictsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Consistency (digest)</div>
|
||
<ul id="syncDiagConsistencyList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-subcard">
|
||
<div class="wesp-sync-diag-subtitle">Клиенты и доставки</div>
|
||
<ul id="syncDiagClientsExtraList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-sync-diag-actions" style="padding: 12px 16px 16px;">
|
||
<div class="wesp-sync-diag-subtitle">Действия</div>
|
||
<p class="wesp-sync-hint">Опасные операции могут повлиять на очередь; подтверждайте осознанно.</p>
|
||
<div class="wesp-adm-list-actions" style="flex-wrap: wrap; gap: 8px;">
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-danger" id="syncActionRequeueStuck" title="processing дольше порога → pending">
|
||
<span>Requeue stuck</span>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="syncActionRestartLocal" title="Перезапуск sync_client в процессе">
|
||
<span>Restart local sync</span>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="syncActionRefreshRuntime" title="Снимок runtime воркера">
|
||
<span>Refresh runtime</span>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm ant-btn-primary" id="syncActionRetryPush" title="Фоновый sync_cycle (клиент)">
|
||
<span>Retry report push</span>
|
||
</button>
|
||
</div>
|
||
<p id="syncDiagActionMessage" class="wesp-sync-hint" style="margin-top:10px; min-height: 1.2em;" aria-live="polite"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Роль и сервер синхронизации</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div id="syncRuntimeNotices" class="wesp-sync-runtime-notices" style="display: none" aria-live="polite"></div>
|
||
<p class="wesp-sync-hint" id="syncConnectionHint"></p>
|
||
<div class="wesp-sync-settings-row">
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="syncSettingsRole">Роль узла</label>
|
||
<select id="syncSettingsRole" class="ant-input" style="max-width: 280px;">
|
||
<option value="server">Сервер</option>
|
||
<option value="client">Клиент</option>
|
||
<option value="other">Другое</option>
|
||
</select>
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<span class="wesp-sync-label" id="syncSettingsServerUrlLabel">Адрес сервера</span>
|
||
<div class="wesp-sync-url-combo" role="group" aria-labelledby="syncSettingsServerUrlLabel">
|
||
<select id="syncSettingsServerScheme" class="ant-input wesp-sync-scheme-select" aria-label="Протокол (http или https)">
|
||
<option value="https">https://</option>
|
||
<option value="http">http://</option>
|
||
</select>
|
||
<input id="syncSettingsServerHost" class="ant-input wesp-sync-host-input" type="text" placeholder="komton_srv_1.local" autocomplete="off" aria-label="IP или домен сервера" />
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--actions">
|
||
<label class="wesp-sync-label" for="syncSettingsSave"> </label>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="syncSettingsSave"><span>Сохранить</span></button>
|
||
</div>
|
||
</div>
|
||
<p class="wesp-sync-hint" style="margin-top: 14px;">Поля ниже пишутся в <code>data/sync_client_state.json</code> (полевой <code>sync_client.py</code>).</p>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-top: 10px">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="syncSettingsClientId">client_id (UUID)</label>
|
||
<input id="syncSettingsClientId" class="ant-input" type="text" placeholder="08ee387a-0522-4f16-afa7-49dfa578fdb9" autocomplete="off" spellcheck="false" />
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="syncSettingsClientName">client_name</label>
|
||
<input id="syncSettingsClientName" class="ant-input" type="text" placeholder="Sync Client" autocomplete="off" />
|
||
</div>
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="syncSettingsFirstBootstrapDone"> </label>
|
||
<label class="wesp-sync-label" style="display: flex; align-items: center; gap: 8px; margin: 0;">
|
||
<input type="checkbox" id="syncSettingsFirstBootstrapDone" />
|
||
first_bootstrap_done
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap" id="orchestratorSyncCard">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Orchestrator (VPS upstream)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">Pairing с оркестратором: код из ЛК предприятия → подтверждение на hub.</p>
|
||
<div class="wesp-sync-settings-row">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="orchSyncUpstreamUrl">Upstream URL</label>
|
||
<input id="orchSyncUpstreamUrl" class="ant-input" type="text" placeholder="https://orchestrator.example.com" autocomplete="off" />
|
||
</div>
|
||
<div class="wesp-sync-field">
|
||
<label class="wesp-sync-label" for="orchSyncHubSiteId">hub_site_id</label>
|
||
<input id="orchSyncHubSiteId" class="ant-input" type="text" autocomplete="off" spellcheck="false" />
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="orchSyncApiKey">API key</label>
|
||
<input id="orchSyncApiKey" class="ant-input" type="password" autocomplete="off" spellcheck="false" />
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--actions">
|
||
<label class="wesp-sync-label" for="orchSyncSave"> </label>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="orchSyncSave"><span>Сохранить</span></button>
|
||
</div>
|
||
</div>
|
||
<p id="orchSyncMessage" class="wesp-sync-hint" aria-live="polite"></p>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Очередь синхронизации</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="syncQueueList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Ошибки синхронизации</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint wesp-sync-errors-hint">Только ошибки валидации/очереди. Transport (TCP, DNS, timeout) — отфильтровано.</p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="syncErrorsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Подключённые клиенты</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="syncClientsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" id="wespSectionNetwork" data-section-content="network">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Этот узел</div>
|
||
<div class="ant-card-extra">
|
||
<button type="button" class="ant-btn ant-btn-sm" id="netSettingsRefreshBtn" title="GET /api/admin/network-settings">
|
||
<span>Обновить</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint" style="padding: 12px 16px 0; margin: 0;" id="networkSettingsHint">
|
||
Роутер настраивать не нужно, если все устройства в одной LAN без изоляции клиентов (guest Wi‑Fi).
|
||
</p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="networkDetectedList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Имя и адрес в LAN</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body wesp-network-settings-form">
|
||
<div class="wesp-network-form-loading" id="networkFormLoading" hidden aria-live="polite">
|
||
<span class="wesp-inline-spinner" aria-hidden="true"></span>
|
||
<span>Загрузка настроек сети…</span>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="netLocalHostname">Имя в LAN (mDNS)</label>
|
||
<input id="netLocalHostname" class="ant-input" type="text" placeholder="komton_srv_1.local" autocomplete="off" spellcheck="false" />
|
||
<p class="wesp-sync-hint" style="margin-top: 6px;">Пример для фермы: <code>komton_srv_1.local</code>, <code>komton_srv_2.local</code>…</p>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="flex-wrap: wrap; margin-top: 12px; align-items: flex-end">
|
||
<div class="wesp-sync-field wesp-sync-field--grow">
|
||
<label class="wesp-sync-label" for="netPublicBaseUrl">Публичный базовый URL</label>
|
||
<input id="netPublicBaseUrl" class="ant-input" type="text" placeholder="http://komton_srv_1.local" autocomplete="off" />
|
||
<p class="wesp-sync-hint" style="margin-top: 6px;">Start URL киоска и QR; пусто — автоопределение по запросу.</p>
|
||
</div>
|
||
<div class="wesp-sync-field wesp-sync-field--actions">
|
||
<label class="wesp-sync-label" for="netBuildUrlBtn"> </label>
|
||
<button type="button" class="ant-btn" id="netBuildUrlBtn"><span>Собрать из имени</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="wesp-sync-settings-row" style="margin-top: 16px; align-items: center">
|
||
<label class="wesp-hw-sim-switch" for="netMdnsEnabled">
|
||
<input type="checkbox" id="netMdnsEnabled" role="switch" aria-describedby="netMdnsStatusLabel" />
|
||
<span class="wesp-hw-sim-switch-ui" aria-hidden="true"></span>
|
||
<span id="netMdnsStatusLabel" class="wesp-hw-sim-status">mDNS: выкл.</span>
|
||
</label>
|
||
<p class="wesp-sync-hint" id="netMdnsHint" style="margin: 8px 0 0; flex: 1 1 100%;"></p>
|
||
</div>
|
||
<div class="wesp-security-actions" style="margin-top: 16px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="netSettingsSave"><span>Сохранить</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Журнал сети</div>
|
||
<div class="ant-card-extra">
|
||
<button type="button" class="ant-btn ant-btn-sm wesp-icon-btn" id="netEventsClearBtn" title="Очистить журнал" aria-label="Очистить журнал сети">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="netEventsRefreshBtn" title="Обновить журнал">
|
||
<span>Обновить</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint" style="padding: 12px 16px 0;">
|
||
Сохранение настроек, запуск/остановка mDNS, ошибки объявления имени в LAN.
|
||
</p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="networkEventsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="security">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Безопасность и киоск</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint wesp-security-hint" id="securitySettingsHint"></p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul class="wesp-adm-list wesp-security-list" role="list">
|
||
<li class="wesp-adm-list-item wesp-adm-list-section" role="separator"><div class="wesp-adm-list-section-text">MAC-адрес</div></li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<div class="wesp-adm-list-title">Ограничить доступ по списку MAC</div>
|
||
<div class="wesp-adm-list-desc">ACL по L2 MAC; только перечисленные интерфейсы.</div>
|
||
<p class="wesp-security-field-hint" id="secMacLockHint" hidden></p>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--toggle">
|
||
<label class="wesp-security-switch" aria-label="Ограничить по MAC">
|
||
<input type="checkbox" id="secMacLock" class="wesp-security-checkbox" />
|
||
<span class="wesp-security-switch-ui" aria-hidden="true"></span>
|
||
</label>
|
||
</div>
|
||
</li>
|
||
<li class="wesp-adm-list-item wesp-adm-list-item--multiline">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secAllowedMacs">Разрешённые MAC</label>
|
||
<div class="wesp-adm-list-desc">Список через запятую; формат EUI-48.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--full">
|
||
<textarea id="secAllowedMacs" class="ant-input wesp-security-input" rows="2" placeholder="aa:bb:cc:dd:ee:ff"></textarea>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="wesp-adm-list-item wesp-adm-list-section" role="separator"><div class="wesp-adm-list-section-text">Киоск</div></li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<div class="wesp-adm-list-title">Только сопряжённые терминалы</div>
|
||
<div class="wesp-adm-list-desc">Доступ к /scales и kiosk API только с cookie привязанного терминала или Start URL. Выключите для отладки без привязки.</div>
|
||
<p class="wesp-security-field-hint" id="secKioskEnforceHint" hidden></p>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--toggle">
|
||
<label class="wesp-security-switch" aria-label="Только сопряжённые терминалы">
|
||
<input type="checkbox" id="secKioskEnforce" class="wesp-security-checkbox" />
|
||
<span class="wesp-security-switch-ui" aria-hidden="true"></span>
|
||
</label>
|
||
</div>
|
||
</li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secKioskPairTtl">Срок legacy pair-token</label>
|
||
<div class="wesp-adm-list-desc">TTL старого одноразового pair-token (legacy), с [30; 86400]. Start URL — постоянная ссылка.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--field"><input type="number" id="secKioskPairTtl" class="ant-input wesp-security-input" min="30" max="86400" step="1" /></div>
|
||
</li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secKioskCookieDays">Срок cookie привязки</label>
|
||
<div class="wesp-adm-list-desc">Срок cookie привязки терминала, сут.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--field"><input type="number" id="secKioskCookieDays" class="ant-input wesp-security-input" min="1" max="36500" step="1" /></div>
|
||
</li>
|
||
|
||
<li class="wesp-adm-list-item wesp-adm-list-section" role="separator"><div class="wesp-adm-list-section-text">Калибровка и токен устройства</div></li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<div class="wesp-adm-list-title">Публичная калибровка по токену</div>
|
||
<div class="wesp-adm-list-desc">Калибровка без admin-session при валидном DEVICE_TOKEN в заголовке.</div>
|
||
<p class="wesp-security-field-hint" id="secCalibrationPublicHint" hidden></p>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--toggle">
|
||
<label class="wesp-security-switch" aria-label="Публичная калибровка">
|
||
<input type="checkbox" id="secCalibrationPublic" class="wesp-security-checkbox" />
|
||
<span class="wesp-security-switch-ui" aria-hidden="true"></span>
|
||
</label>
|
||
</div>
|
||
</li>
|
||
<li class="wesp-adm-list-item wesp-security-note-row">
|
||
<p class="wesp-security-list-note" id="secDeviceTokenNote">Токен не отображается. Новое значение или флаг сброса ниже.</p>
|
||
</li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secDeviceTokenHeader">HTTP-заголовок токена</label>
|
||
<div class="wesp-adm-list-desc">HTTP header для секрета устройства; default X-Device-Token.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--field"><input type="text" id="secDeviceTokenHeader" class="ant-input wesp-security-input" placeholder="X-Device-Token" autocomplete="off" /></div>
|
||
</li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secDeviceToken">Новый токен устройства</label>
|
||
<div class="wesp-adm-list-desc">Пусто = без смены. После commit не читается из API.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--field"><input type="password" id="secDeviceToken" class="ant-input wesp-security-input" placeholder="Не менять — пусто" autocomplete="new-password" /></div>
|
||
</li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secDeviceTokenClear">Сбросить токен устройства</label>
|
||
<div class="wesp-adm-list-desc">Обнуление секрета; публичная калибровка OFF до нового токена.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--toggle"><input type="checkbox" id="secDeviceTokenClear" class="wesp-security-checkbox" aria-label="Сбросить токен" /></div>
|
||
</li>
|
||
|
||
<li class="wesp-adm-list-item wesp-adm-list-section" role="separator"><div class="wesp-adm-list-section-text">Вход в веб-интерфейс</div></li>
|
||
<li class="wesp-adm-list-item">
|
||
<div class="wesp-adm-list-main">
|
||
<label class="wesp-adm-list-title wesp-adm-list-title--form" for="secSessionRememberDays">Срок «Запомнить меня»</label>
|
||
<div class="wesp-adm-list-desc">PERMANENT_SESSION / «Запомнить»: срок, сут.</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra wesp-adm-list-extra--field"><input type="number" id="secSessionRememberDays" class="ant-input wesp-security-input" min="1" max="3650" step="1" /></div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div class="wesp-security-actions">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="securitySettingsSave"><span>Сохранить</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="hardware">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap" id="wespHardwareHistoryCard">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Нагрузка (история)</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-hardware-chart-hint" id="wespHardwareHistoryHint">
|
||
fetching…
|
||
</p>
|
||
<div class="wesp-hardware-chart-wrap" id="wespHardwareHistoryWrap" hidden>
|
||
<svg id="wespHardwareHistorySvg" class="wesp-hardware-chart-svg" viewBox="0 0 640 220" preserveAspectRatio="xMidYMid meet" role="img" aria-label="График нагрузки"></svg>
|
||
<div class="wesp-hardware-chart-legend" id="wespHardwareHistoryLegend"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap" id="hwSimulationCard">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Симуляция весов</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="wesp-hw-sim-head">
|
||
<label class="wesp-hw-sim-switch" for="hwSimulationToggle">
|
||
<input type="checkbox" id="hwSimulationToggle" role="switch" aria-describedby="hwSimulationStatusLabel" />
|
||
<span class="wesp-hw-sim-switch-ui" aria-hidden="true"></span>
|
||
<span id="hwSimulationStatusLabel" class="wesp-hw-sim-status">Выключена · чтение HX711</span>
|
||
</label>
|
||
</div>
|
||
<div id="hwSimulationPanel" class="wesp-hw-sim-panel" hidden>
|
||
<p class="wesp-sync-hint wesp-hw-sim-hint">
|
||
Имитация показаний для киоска и калибровки без датчика. Текущий показ:
|
||
<strong id="hwSimWeightLive">0</strong> кг
|
||
</p>
|
||
<div class="wesp-hw-sim-weight-row">
|
||
<button type="button" class="ant-btn" id="hwSimWeightMinus" title="−1 кг" aria-label="Уменьшить на 1 кг"><span>−</span></button>
|
||
<input type="number" id="hwSimWeightInput" class="ant-input wesp-hw-sim-input" min="0" max="9999" step="1" inputmode="numeric" aria-label="Вес в килограммах" />
|
||
<span class="wesp-hw-sim-unit">кг</span>
|
||
<button type="button" class="ant-btn" id="hwSimWeightPlus" title="+1 кг" aria-label="Увеличить на 1 кг"><span>+</span></button>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="hwSimWeightApply"><span>Применить</span></button>
|
||
</div>
|
||
<label class="wesp-sync-label" for="hwSimWeightSlider">Ползунок (0–500 кг)</label>
|
||
<input type="range" id="hwSimWeightSlider" class="wesp-hw-sim-slider" min="0" max="500" step="1" value="0" aria-valuemin="0" aria-valuemax="500" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Железо и весы</div>
|
||
<div class="ant-card-extra">
|
||
<button type="button" class="ant-btn ant-btn-sm" id="hardwareRefreshBtn" title="GET /api/admin/hardware-status">
|
||
<span>Обновить</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint" id="hardwareStatusHint" style="margin-top: 0; margin-bottom: 12px;">обновление…</p>
|
||
<div class="wesp-diagnostics-actions">
|
||
<button type="button" class="ant-btn" id="hwHxSampleBtn"><span>HX711 sample</span></button>
|
||
<button type="button" class="ant-btn" id="hwGpioBlinkBtn"><span>GPIO blink ×3</span></button>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="hardwareHostList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="hardwareHostAlertsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="hardwarePeriphList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="hardwareScalesList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Журнал событий оборудования</div>
|
||
<div class="ant-card-extra">
|
||
<button type="button" class="ant-btn ant-btn-sm wesp-icon-btn" id="hardwareEventsClearBtn" title="Очистить журнал" aria-label="Очистить журнал событий">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint" style="padding: 12px 16px 0;">
|
||
HX711, GPIO, пороги CPU/RAM/диска. При замыкании или неверной обвязке смотри коды <code>hx711_read_error</code>.
|
||
</p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="hardwareEventsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="diagnostics">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper wesp-diag-summary-head">
|
||
<div>
|
||
<div class="ant-card-head-title">Сводка диагностики</div>
|
||
<p class="wesp-sync-hint wesp-diag-summary-head-sub">
|
||
Топология узла и LAN; эвристический аудит безопасности; периферия (GPIO BCM, АЦП HX711).
|
||
</p>
|
||
</div>
|
||
<button type="button" class="ant-btn" id="diagRefreshReportBtn" title="GET /api/admin/diagnostics/report">
|
||
<span>Обновить отчёт</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint wesp-diagnostics-hint" id="diagNodeHint"></p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="diagOverviewList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Сеть: сервер и клиенты</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">L3 path асимметричен. Полная картина: прогон на server- и client-узле.</p>
|
||
<div class="wesp-diagnostics-actions">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="diagRunPrimaryBtn"><span>network-run: primary</span></button>
|
||
<button type="button" class="ant-btn" id="diagRunPrimaryTraceBtn"><span>L3 trace (modal)</span></button>
|
||
<button type="button" class="ant-btn" id="diagRunInternetBtn"><span>HTTP probe (WAN)</span></button>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="diagPrimaryTargetsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap wesp-diagnostics-results-wrap">
|
||
<ul id="diagNetworkResultsList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Дополнительные точки LAN</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint">Персистенция в network diagnostics JSON на данном хосте; до trace — save.</p>
|
||
<div id="diagExtraTargetsRows" class="wesp-diagnostics-targets"></div>
|
||
<div class="wesp-diagnostics-actions">
|
||
<button type="button" class="ant-btn" id="diagAddExtraTargetBtn"><span>+ target row</span></button>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="diagSaveExtraTargetsBtn"><span>PATCH extra_targets</span></button>
|
||
<button type="button" class="ant-btn" id="diagRunExtraTargetsBtn"><span>network-run: extra</span></button>
|
||
</div>
|
||
<p class="wesp-sync-hint wesp-diagnostics-path" id="diagSettingsPath"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Безопасность и секреты</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<p class="wesp-sync-hint">
|
||
Эвристика: только булевы индикаторы (OK / Ошибка) и текст рекомендаций.
|
||
Список ниже заполняется после <strong>GET /api/admin/diagnostics/report</strong> (раздел «Диагностика» в меню слева).
|
||
</p>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="diagSecurityList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Железо и весы</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<div class="wesp-diagnostics-actions">
|
||
<button type="button" class="ant-btn" id="diagHxSampleBtn"><span>HX711 sample</span></button>
|
||
<button type="button" class="ant-btn" id="diagGpioBlinkBtn"><span>GPIO blink ×3</span></button>
|
||
</div>
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="diagHardwareList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="users">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Добавить пользователя</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<form id="createUserForm" class="ant-form ant-form-inline">
|
||
<div class="ant-form-item" style="margin-right: 12px; margin-bottom: 8px;">
|
||
<input id="newUserLogin" class="ant-input" type="text" placeholder="Логин" required style="width: 200px;">
|
||
</div>
|
||
<div class="ant-form-item" style="margin-right: 12px; margin-bottom: 8px;">
|
||
<input id="newUserPassword" class="ant-input" type="password" placeholder="Пароль (мин. 8 симв., буквы и цифры)" required style="width: 220px;" minlength="8">
|
||
</div>
|
||
<div class="ant-form-item" style="margin-right: 12px; margin-bottom: 8px;">
|
||
<label class="ant-checkbox-wrapper">
|
||
<span class="ant-checkbox">
|
||
<input id="newUserIsSuper" type="checkbox" class="ant-checkbox-input">
|
||
<span class="ant-checkbox-inner"></span>
|
||
</span>
|
||
<span>Суперпользователь</span>
|
||
</label>
|
||
</div>
|
||
<div class="ant-form-item" style="margin-bottom: 8px;">
|
||
<button type="submit" class="ant-btn ant-btn-primary"><span>Создать</span></button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card ant-card-bordered">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Список пользователей</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body" style="padding: 0;">
|
||
<div class="wesp-adm-list-wrap">
|
||
<ul id="usersList" class="wesp-adm-list" role="list"></ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="assistant">
|
||
<div class="ant-card ant-card-bordered wesp-admin-card-gap wesp-admin-block">
|
||
<div class="ant-card-head">
|
||
<div class="ant-card-head-wrapper">
|
||
<div class="ant-card-head-title">Локальный ассистент</div>
|
||
<div class="ant-card-extra">
|
||
<span class="ant-tag" id="wespAssistantStatusBadge">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ant-card-body">
|
||
<p class="wesp-sync-hint" style="margin: 0 0 8px;">
|
||
Доступно только суперпользователю. Запросы к модели <strong>не отправляются в облако</strong> — только на ваш
|
||
локальный сервер (<code class="wesp-admin-code-inline" id="wespAssistantLlmUrlHint">127.0.0.1</code>,
|
||
OpenAI-совместимый API, чаще всего <code class="wesp-admin-code-inline">llama-server</code>).
|
||
</p>
|
||
<p class="wesp-sync-hint" style="margin: 0 0 10px; max-width: 820px; padding: 10px 12px; border-radius: 8px; background: rgba(128, 128, 128, 0.1);">
|
||
<strong>Как узнать, что с системой:</strong> включите ниже <strong>«Чат с контекстом БД»</strong> — в диалог идут среда и PRAGMA по файлам SQLite (<strong>не</strong> строки таблиц раздатчиков/рецептов; полный дамп — <strong>«Снимок диагностики»</strong>).
|
||
Отчёты/склад за даты — <strong>«Сводка за период»</strong>.
|
||
Ещё: дашборд, журналы, <a href="/db_check">Проверка БД</a>.
|
||
</p>
|
||
<ul class="wesp-assistant-hint-list">
|
||
<li>Это обычная генерация текста, <strong>не режим «размышления»</strong> (как у отдельных облачных моделей): ответ появляется сразу после обработки запроса.</li>
|
||
<li><strong>Чат</strong> — по умолчанию лёгкий (без БД в промпте). Галка «Чат с контекстом БД» — среда + PRAGMA по .sqlite, <strong>без строк справочников</strong> (тяжелее по токенам). <strong>Сводка за период</strong> — отчёты/склад. Полный дамп ORM — «Снимок диагностики». При включённых <strong>инструментах</strong> модель может вызывать slim-SQL (только SELECT, сначала preview) и анализ логов; выполнение SELECT — только в режиме «Выполнять SELECT».</li>
|
||
<li>Каталог файлов моделей (GGUF): переменная <code class="wesp-admin-code-inline">WESP_ASSISTANT_DIR</code>.</li>
|
||
</ul>
|
||
<div class="wesp-assistant-settings-block" style="margin-bottom: 16px; padding: 12px 14px; border-radius: 8px; border: 1px solid rgba(128, 128, 128, 0.22);">
|
||
<div class="wesp-assistant-subtitle" style="margin-top: 0;">Управление ИИ</div>
|
||
<div style="margin-bottom: 10px;">
|
||
<label class="wesp-sync-label" style="display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||
<input type="checkbox" id="wespAssistantAdminLlmEnabled" />
|
||
<span><strong>Включить ИИ</strong> (чат, сводки, ping, инструменты)</span>
|
||
</label>
|
||
<p class="wesp-sync-hint" id="wespAssistantAdminLlmEnabledHint" style="margin: 6px 0 0; max-width: 720px; display: none;"></p>
|
||
<p class="wesp-sync-hint" style="margin: 6px 0 0; max-width: 720px;">
|
||
Флаг <code class="wesp-admin-code-inline">admin_llm_enabled</code> →
|
||
<code class="wesp-admin-code-inline">data/wesp_security_settings.json</code>;
|
||
env <code class="wesp-admin-code-inline">WESP_ADMIN_LLM_ENABLED</code> имеет приоритет.
|
||
При выключении API <code class="wesp-admin-code-inline">/api/admin/llm/*</code> возвращает 503.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<label class="wesp-sync-label" style="display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||
<input type="checkbox" id="wespAssistantLlmAutostart" />
|
||
<span>Автозапуск <code class="wesp-admin-code-inline">llama-server</code> при старте WESP</span>
|
||
</label>
|
||
<p class="wesp-sync-hint" id="wespAssistantLlmAutostartHint" style="margin: 6px 0 0; max-width: 720px; display: none;"></p>
|
||
<p class="wesp-sync-hint" style="margin: 6px 0 0; max-width: 720px;">
|
||
Ключ <code class="wesp-admin-code-inline">llm_autostart</code> в том же JSON; env
|
||
<code class="wesp-admin-code-inline">WESP_LLM_AUTOSTART</code>. Действует после перезапуска WESP
|
||
(если ИИ включён и есть GGUF в <code class="wesp-admin-code-inline">WESP_ASSISTANT_DIR/models</code>).
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom: 12px;">
|
||
<label class="wesp-sync-label" style="display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||
<input type="checkbox" id="wespAssistantChatDbContext" />
|
||
<span>Чат с контекстом БД (среда + PRAGMA SQLite; без строк таблиц)</span>
|
||
</label>
|
||
<p class="wesp-sync-hint" id="wespAssistantChatDbContextHint" style="margin: 6px 0 0; max-width: 720px; display: none;"></p>
|
||
<p class="wesp-sync-hint" style="margin: 6px 0 0; max-width: 720px;">
|
||
Ключ <code class="wesp-admin-code-inline">llm_chat_db_context</code> в
|
||
<code class="wesp-admin-code-inline">data/wesp_security_settings.json</code>.
|
||
Переменная <code class="wesp-admin-code-inline">WESP_LLM_CHAT_INCLUDE_DB_CONTEXT</code> в окружении имеет приоритет. Полный дамп таблиц по-прежнему только в «Снимок диагностики».
|
||
</p>
|
||
</div>
|
||
<div style="margin-bottom: 12px;">
|
||
<label class="wesp-sync-label" style="display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||
<input type="checkbox" id="wespAssistantLlmToolsEnabled" />
|
||
<span>Инструменты в чате (slim-SQL, логи, сводка лога)</span>
|
||
</label>
|
||
<p class="wesp-sync-hint" id="wespAssistantLlmToolsEnabledHint" style="margin: 6px 0 0; max-width: 720px; display: none;"></p>
|
||
<p class="wesp-sync-hint" style="margin: 6px 0 0; max-width: 720px;">
|
||
Ключ <code class="wesp-admin-code-inline">llm_tools_enabled</code> в
|
||
<code class="wesp-admin-code-inline">data/wesp_security_settings.json</code>.
|
||
Переменная <code class="wesp-admin-code-inline">WESP_LLM_TOOLS_ENABLED</code> в окружении имеет приоритет.
|
||
</p>
|
||
</div>
|
||
<div style="margin-bottom: 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;">
|
||
<button type="button" class="ant-btn" id="wespAssistantRefreshStatusBtn"><span>Проверить состояние</span></button>
|
||
<button type="button" class="ant-btn" id="wespAssistantRefreshLogBtn"><span>Обновить журнал ИИ</span></button>
|
||
</div>
|
||
|
||
<div
|
||
id="wespAssistantLlmLogTerminal"
|
||
class="wesp-admin-activity-terminal wesp-assistant-llm-log"
|
||
aria-label="Журнал вызовов ИИ"
|
||
style="margin-bottom: 20px;"
|
||
>
|
||
<div class="wesp-admin-activity-terminal__chrome">
|
||
<span class="wesp-admin-activity-terminal__dots" aria-hidden="true"></span>
|
||
<span class="wesp-admin-activity-terminal__title">wesp-llm · journal</span>
|
||
<span class="wesp-admin-activity-terminal__badge" id="wespAssistantLlmLogBadge">7d · JSONL</span>
|
||
</div>
|
||
<div
|
||
id="wespAssistantLlmLog"
|
||
class="wesp-admin-activity-terminal__body"
|
||
role="log"
|
||
aria-live="polite"
|
||
></div>
|
||
</div>
|
||
|
||
<div class="wesp-assistant-subtitle" id="wespAssistantChatSectionTitle">Диалог с моделью</div>
|
||
<p class="wesp-sync-hint" style="margin: 0 0 8px;">Пока идёт запрос, в ленте показывается «Генерируется ответ…». Можно отправить сочетанием <kbd>Enter</kbd> (новая строка — <kbd>Shift+Enter</kbd>).</p>
|
||
<div
|
||
id="wespAssistantChatLog"
|
||
class="wesp-admin-modal-pre wesp-assistant-chat-log"
|
||
role="log"
|
||
aria-live="polite"
|
||
></div>
|
||
<textarea
|
||
id="wespAssistantChatInput"
|
||
class="ant-input"
|
||
rows="3"
|
||
placeholder="Напишите вопрос…"
|
||
style="width: 100%; max-width: 640px; margin-bottom: 8px;"
|
||
></textarea>
|
||
<div class="wesp-assistant-chat-toolbar" style="margin-bottom: 12px; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; max-width: 720px;">
|
||
<label class="wesp-sync-label" style="display: inline-flex; align-items: center; gap: 6px;">
|
||
<span>SQL к БД:</span>
|
||
<select id="wespAssistantSqlMode" class="ant-input" title="preview — только EXPLAIN; execute — чтение строк SELECT (read-only)">
|
||
<option value="preview" selected>только план (preview)</option>
|
||
<option value="execute">выполнять SELECT</option>
|
||
</select>
|
||
</label>
|
||
<label class="wesp-sync-label" style="display: inline-flex; align-items: center; gap: 6px;">
|
||
<input type="checkbox" id="wespAssistantToolTraceCb" checked />
|
||
<span>Показать trace инструментов</span>
|
||
</label>
|
||
</div>
|
||
<div style="margin-bottom: 20px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="wespAssistantChatSendBtn"><span>Отправить</span></button>
|
||
<button type="button" class="ant-btn" id="wespAssistantChatClearBtn"><span>Очистить диалог</span></button>
|
||
</div>
|
||
<div class="wesp-assistant-subtitle" style="margin-top: 4px;">Trace инструментов (последний ответ)</div>
|
||
<pre id="wespAssistantToolTrace" class="wesp-admin-modal-pre wesp-assistant-tool-trace" style="margin-top: 6px; max-height: 220px; overflow: auto;">—</pre>
|
||
|
||
<div class="wesp-assistant-subtitle">Сводка по данным из базы</div>
|
||
<p class="wesp-sync-hint" style="margin: 0 0 8px;">Модель получает выгрузку отчётов/склада за выбранный период (тяжелее обычного чата).</p>
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px; align-items: flex-end;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-8" style="padding: 8px;">
|
||
<label class="wesp-admin-field-label" for="wespAssistantDateFrom">Период с</label>
|
||
<input type="date" id="wespAssistantDateFrom" class="ant-input" />
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-8" style="padding: 8px;">
|
||
<label class="wesp-admin-field-label" for="wespAssistantDateTo">Период по</label>
|
||
<input type="date" id="wespAssistantDateTo" class="ant-input" />
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-8" style="padding: 8px;">
|
||
<label class="wesp-admin-field-label" for="wespAssistantFocus">Что включить</label>
|
||
<select id="wespAssistantFocus" class="ant-input" title="both — отчёты и склад; deviations — только отклонения; stock — склад">
|
||
<option value="both" selected>отчёты и склад (both)</option>
|
||
<option value="deviations">только отклонения (deviations)</option>
|
||
<option value="stock">только склад (stock)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 8px; flex-wrap: wrap; gap: 8px; display: flex; align-items: center;">
|
||
<button type="button" class="ant-btn" id="wespAssistantPingBtn" title="Короткий запрос к модели без чтения БД — проверка цепочки WESP → llama-server"><span>Проверка связи (без БД)</span></button>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="wespAssistantSummaryBtn"><span>Сводка за период</span></button>
|
||
<button type="button" class="ant-btn" id="wespAssistantDiagnosticsBtn" title="GET /api/admin/llm/diagnostics — полный JSON и текст для модели"><span>Снимок диагностики</span></button>
|
||
</div>
|
||
|
||
<div class="wesp-assistant-subtitle" style="margin-top: 16px;">Результат проверки / сводки / диагностики</div>
|
||
<pre id="wespAssistantOutput" class="wesp-admin-modal-pre wesp-assistant-output-pre" style="margin-top: 8px;"></pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="wesp-admin-section" data-section-content="links">
|
||
<div class="ant-row" style="margin-left: -8px; margin-right: -8px;">
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<a class="ant-card ant-card-bordered ant-card-hoverable" href="/db_check" style="display: block; color: inherit;">
|
||
<div class="ant-card-body">Проверка БД</div>
|
||
</a>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<a class="ant-card ant-card-bordered ant-card-hoverable" href="/scales" style="display: block; color: inherit;">
|
||
<div class="ant-card-body">Весы</div>
|
||
</a>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<a class="ant-card ant-card-bordered ant-card-hoverable" href="/calibration" style="display: block; color: inherit;">
|
||
<div class="ant-card-body">Калибровка</div>
|
||
</a>
|
||
</div>
|
||
<div class="ant-col ant-col-xs-24 ant-col-sm-12 ant-col-lg-6" style="padding: 8px;">
|
||
<a class="ant-card ant-card-bordered ant-card-hoverable" href="/recipes" style="display: block; color: inherit;">
|
||
<div class="ant-card-body">Рецепты</div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div
|
||
id="adminActivityTerminal"
|
||
class="wesp-admin-activity-terminal"
|
||
aria-label="Activity: UI JSONL + wesp.log tail, 72h"
|
||
>
|
||
<div class="wesp-admin-activity-terminal__chrome">
|
||
<span class="wesp-admin-activity-terminal__dots" aria-hidden="true"></span>
|
||
<span class="wesp-admin-activity-terminal__title">wesp-admin · activity</span>
|
||
<span class="wesp-admin-activity-terminal__badge">72h · ui + llm + wesp.log</span>
|
||
</div>
|
||
<div
|
||
id="adminActivityLog"
|
||
class="wesp-admin-activity-terminal__body"
|
||
role="log"
|
||
aria-live="polite"
|
||
aria-relevant="additions"
|
||
></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ant Design: ant-modal-mask + ant-modal-wrap + ant-modal + ant-modal-content -->
|
||
<div id="adminModal" class="wesp-admin-modal" hidden>
|
||
<div class="ant-modal-mask" id="adminModalMask"></div>
|
||
<div
|
||
class="ant-modal-wrap"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="adminModalTitle"
|
||
tabindex="-1"
|
||
>
|
||
<div class="ant-modal wesp-admin-log-modal">
|
||
<div class="ant-modal-content">
|
||
<button type="button" class="ant-modal-close" id="adminModalClose" aria-label="Закрыть">
|
||
<span class="ant-modal-close-x">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</span>
|
||
</button>
|
||
<div class="ant-modal-header">
|
||
<div class="ant-modal-title" id="adminModalTitle">Журнал</div>
|
||
</div>
|
||
<div class="ant-modal-body">
|
||
<pre id="adminModalBody" class="wesp-admin-modal-pre"></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="userModulesModal" class="wesp-admin-modal" hidden>
|
||
<div class="ant-modal-mask" id="userModulesModalMask"></div>
|
||
<div
|
||
class="ant-modal-wrap"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="userModulesModalTitle"
|
||
tabindex="-1"
|
||
>
|
||
<div class="ant-modal wesp-admin-user-modules-modal">
|
||
<div class="ant-modal-content">
|
||
<button type="button" class="ant-modal-close" id="userModulesModalClose" aria-label="Закрыть">
|
||
<span class="ant-modal-close-x">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</span>
|
||
</button>
|
||
<div class="ant-modal-header">
|
||
<div class="ant-modal-title" id="userModulesModalTitle">Доступ к модулям</div>
|
||
</div>
|
||
<div class="ant-modal-body">
|
||
<p class="wesp-sync-hint" style="margin: 0 0 12px;">
|
||
Пользователь: <strong id="userModulesModalLogin">—</strong>
|
||
</p>
|
||
<ul class="wesp-adm-list" role="list">
|
||
<li class="wesp-adm-list-item wesp-adm-list-item--multiline">
|
||
<div class="wesp-adm-list-main">
|
||
<div class="wesp-adm-list-title">Lab</div>
|
||
<div class="wesp-adm-list-desc">Zootech: рационы, нутриенты, авторацион</div>
|
||
</div>
|
||
<div class="wesp-adm-list-extra">
|
||
<label class="ant-checkbox-wrapper">
|
||
<span class="ant-checkbox">
|
||
<input id="userModuleLabAccess" type="checkbox" class="ant-checkbox-input">
|
||
<span class="ant-checkbox-inner"></span>
|
||
</span>
|
||
<span>Включён</span>
|
||
</label>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
<p id="userModuleLabSuperHint" class="wesp-sync-hint" hidden style="margin: 12px 0 0;">
|
||
У суперпользователя модуль Lab доступен всегда.
|
||
</p>
|
||
</div>
|
||
<div class="ant-modal-footer">
|
||
<button type="button" class="ant-btn" id="userModulesModalCancel"><span>Отмена</span></button>
|
||
<button type="button" class="ant-btn ant-btn-primary" id="userModulesModalSave"><span>Сохранить</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tracerouteModal" class="wesp-admin-modal" hidden>
|
||
<div class="ant-modal-mask" id="tracerouteModalMask"></div>
|
||
<div
|
||
class="ant-modal-wrap"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="tracerouteModalTitle"
|
||
tabindex="-1"
|
||
>
|
||
<div class="ant-modal wesp-admin-traceroute-modal">
|
||
<div class="ant-modal-content">
|
||
<button type="button" class="ant-modal-close" id="tracerouteModalClose" aria-label="Закрыть">
|
||
<span class="ant-modal-close-x">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</span>
|
||
</button>
|
||
<div class="ant-modal-header">
|
||
<div class="ant-modal-title" id="tracerouteModalTitle">L3 path analysis</div>
|
||
</div>
|
||
<div class="ant-modal-body wesp-admin-traceroute-modal-body">
|
||
<p class="wesp-sync-hint" style="margin: 0 0 8px;">
|
||
Хосты: <strong>1 строка = 1 FQDN/IP</strong>. Backend: <code class="wesp-admin-code-inline">traceroute</code>/<code class="wesp-admin-code-inline">tracepath</code> (или mtr); парсинг stdout.
|
||
</p>
|
||
<textarea
|
||
id="tracerouteHostsInput"
|
||
class="ant-input"
|
||
rows="6"
|
||
style="width: 100%; font-family: ui-monospace, monospace; font-size: 12px; resize: vertical;"
|
||
placeholder="192.168.0.1 8.8.8.8"
|
||
></textarea>
|
||
<div class="wesp-diagnostics-actions" style="margin-top: 12px;">
|
||
<button type="button" class="ant-btn ant-btn-primary" id="tracerouteRunBtn"><span>POST traceroute-by-hosts</span></button>
|
||
<button type="button" class="ant-btn" id="tracerouteFillPrimaryBtn"><span>fill primary_targets</span></button>
|
||
<button type="button" class="ant-btn" id="tracerouteClearHostsBtn"><span>clear</span></button>
|
||
</div>
|
||
<div id="tracerouteModalResults" class="wesp-admin-traceroute-results"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="adminSqliteModal" class="wesp-admin-modal" hidden>
|
||
<div class="ant-modal-mask" id="adminSqliteModalMask"></div>
|
||
<div
|
||
class="ant-modal-wrap"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="adminSqliteModalTitle"
|
||
tabindex="-1"
|
||
>
|
||
<div class="ant-modal wesp-admin-sqlite-modal">
|
||
<div class="ant-modal-content">
|
||
<button type="button" class="ant-modal-close" id="adminSqliteModalClose" aria-label="Закрыть">
|
||
<span class="ant-modal-close-x">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</span>
|
||
</button>
|
||
<div class="ant-modal-header">
|
||
<div class="ant-modal-title" id="adminSqliteModalTitle">НЕ НАКОСЯЧ</div>
|
||
</div>
|
||
<div class="ant-modal-body wesp-admin-sqlite-modal-body wesp-db-browser-body">
|
||
<p class="wesp-db-browser__intro">
|
||
SQLite по bind: дерево таблиц. Commit → запись в файлы на сервере; операции необратимы без бэкапа.
|
||
</p>
|
||
<div class="wesp-db-browser__toolbar">
|
||
<button type="button" class="ant-btn ant-btn-sm" id="adminDbRefreshTreeBtn">
|
||
<span>Обновить дерево</span>
|
||
</button>
|
||
<button type="button" class="ant-btn ant-btn-sm" id="adminDbRefreshGridBtn">
|
||
<span>Обновить таблицу</span>
|
||
</button>
|
||
</div>
|
||
<p id="adminSqliteError" class="wesp-sqlite-editor__error" aria-live="polite"></p>
|
||
<div class="wesp-db-browser-layout">
|
||
<div class="wesp-db-tree-panel">
|
||
<div class="wesp-db-tree-panel-title">Каталог таблиц</div>
|
||
<ul id="adminDbTree" class="wesp-db-tree" role="tree"></ul>
|
||
</div>
|
||
<div class="wesp-db-grid-panel">
|
||
<div id="adminDbGridMeta" class="wesp-db-grid-meta"></div>
|
||
<div id="adminDbPager" class="wesp-db-pager"></div>
|
||
<div id="adminDbGridWrap" class="wesp-db-grid-wrap"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Скелетоны сразу после разметки (модуль грузится позже и не успевает до первого кадра) -->
|
||
<script>
|
||
(function () {
|
||
try {
|
||
document.documentElement.classList.add("wesp-admin-loading");
|
||
} catch (e1) {}
|
||
var app = document.getElementById("app");
|
||
if (!app) return;
|
||
var seen = new Set();
|
||
function add(el) {
|
||
if (!el || seen.has(el)) return;
|
||
seen.add(el);
|
||
el.classList.add("wesp-admin-block");
|
||
for (var i = 0; i < el.children.length; i++) {
|
||
if (el.children[i].classList.contains("wesp-admin-skel-overlay")) return;
|
||
}
|
||
var ov = document.createElement("div");
|
||
ov.className = "wesp-admin-skel-overlay";
|
||
ov.setAttribute("aria-hidden", "true");
|
||
el.appendChild(ov);
|
||
}
|
||
add(app.querySelector(".wesp-admin-sider"));
|
||
add(app.querySelector("main.ant-layout-content .wesp-admin-content-topbar"));
|
||
var cards = app.querySelectorAll("main.ant-layout-content .ant-card");
|
||
for (var j = 0; j < cards.length; j++) add(cards[j]);
|
||
window.__WESP_ADMIN_SKEL_ARMED_AT__ = performance.now();
|
||
})();
|
||
</script>
|
||
<script src="/static/js/wesp-user-messages.js?v=1"></script>
|
||
<script type="module" src="/static/js/pages/admin-panel.js?v=20260714"></script>
|
||
<link href="/static/css/wesp-update-banner.css" rel="stylesheet">
|
||
<script src="/static/js/wesp-dialog.js"></script>
|
||
<script src="/static/js/wesp-update-notifier.js"></script>
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
if (window.WespUpdateNotifier && window.WespDialog) {
|
||
WespUpdateNotifier.init({ dialog: window.WespDialog });
|
||
}
|
||
});
|
||
</script>
|
||
<div id="kioskFullSetupOverlay" class="wesp-kiosk-setup-overlay" hidden aria-hidden="true">
|
||
<div class="wesp-kiosk-setup-overlay__screen wesp-startup-screen">
|
||
<div class="wesp-startup__stage">
|
||
<div class="wesp-setup__intro" id="kioskFullSetupIntro"></div>
|
||
<p class="wesp-startup__quip" id="kioskFullSetupQuip" aria-live="polite"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|