v2.5.0: improve admin domain language logs

This commit is contained in:
Виталий Литвинов
2026-04-24 22:59:21 +03:00
parent 9c74a0d00f
commit 3b075a7ed7
7 changed files with 235 additions and 20 deletions

View File

@@ -109,8 +109,18 @@ const i18n = {
darkTheme: "Dark",
configMode: "Mode",
configDomain: "Domain",
configSiteStatus: "Site check",
configTemplate: "Template",
configVersion: "Version",
siteOk: "Site 200 OK",
siteHttp: "Site HTTP",
siteMissing: "Domain is not configured",
siteInvalid: "Invalid domain",
siteError: "Site check failed",
siteNotChecked: "Site check pending",
logsLines: "lines",
logsNoData: "No log lines",
languageSaved: "Language saved",
pageDashboardTitle: "Dashboard",
pageDashboardKicker: "Local Admin",
pageTrafficTitle: "Traffic",
@@ -231,8 +241,18 @@ const i18n = {
darkTheme: "Тёмная",
configMode: "Режим",
configDomain: "Домен",
configSiteStatus: "Проверка сайта",
configTemplate: "Шаблон",
configVersion: "Версия",
siteOk: "Сайт 200 OK",
siteHttp: "Сайт HTTP",
siteMissing: "Домен не настроен",
siteInvalid: "Некорректный домен",
siteError: "Проверка сайта не прошла",
siteNotChecked: "Проверка сайта ожидает",
logsLines: "строк",
logsNoData: "Строк логов нет",
languageSaved: "Язык сохранён",
pageDashboardTitle: "Обзор",
pageDashboardKicker: "Локальная админка",
pageTrafficTitle: "Трафик",
@@ -333,7 +353,7 @@ function applyI18n() {
el.placeholder = t(el.dataset.i18nPlaceholder);
});
$("#themeToggle").textContent = state.theme === "dark" ? t("themeLight") : t("themeDark");
$("#languageBadge").textContent = state.lang.toUpperCase();
$("#languageSelect").value = state.lang;
$("#settingsLanguage").textContent = state.lang === "ru" ? "Русский" : "English";
$("#settingsTheme").textContent = state.theme === "dark" ? t("darkTheme") : t("lightTheme");
updatePageTitle();
@@ -347,6 +367,26 @@ function setTheme(theme) {
if (state.overview) drawTrafficChart(state.overview.stats_history || []);
}
async function setLanguage(lang) {
const previous = state.lang;
state.lang = lang === "ru" ? "ru" : "en";
applyI18n();
try {
const data = await api("/api/settings/language", {
method: "POST",
body: JSON.stringify({ language: state.lang }),
});
state.lang = data.language === "ru" ? "ru" : "en";
applyI18n();
toast(t("languageSaved"));
await refreshAll();
} catch (err) {
state.lang = previous;
applyI18n();
toast(err.message);
}
}
function setPage(page, push = true) {
const next = $(`[data-page="${page}"]`) ? page : "dashboard";
state.page = next;
@@ -445,6 +485,31 @@ function renderRuntime() {
`).join("") : "";
}
function siteStatusText(site = {}) {
if (!site.host) return t("siteMissing");
if (site.error === "invalid_domain") return t("siteInvalid");
if (site.ok) return t("siteOk");
if (site.checked && site.http_code) return `${t("siteHttp")} ${site.http_code}`;
if (site.error) return t("siteError");
return t("siteNotChecked");
}
function siteStatusClass(site = {}) {
if (site.ok) return "ok";
if (!site.host || !site.checked) return "warn";
return "error";
}
function renderSiteStatus() {
const cfg = state.overview?.config || {};
const site = state.overview?.site_status || {};
$("#metricDomain").textContent = site.host || cfg.domain || cfg.mask_host || "--";
const statusEl = $("#siteStatus");
statusEl.textContent = siteStatusText(site);
statusEl.className = `metric-status ${siteStatusClass(site)}`;
statusEl.title = site.url || "";
}
function renderOverview() {
const data = state.overview;
if (!data) return;
@@ -455,7 +520,7 @@ function renderOverview() {
$("#sidebarBind").textContent = `${bind.host || "127.0.0.1"}:${bind.port || 1984}`;
$("#settingsBind").textContent = `${bind.host || "127.0.0.1"}:${bind.port || 1984}`;
$("#metricMode").textContent = cfg.mode || "--";
$("#metricDomain").textContent = cfg.domain || cfg.mask_host || "--";
renderSiteStatus();
$("#metricUsers").textContent = data.users_count ?? 0;
$("#metricProxyTraffic").textContent = fmtBytes(stats.proxy_bytes);
$("#metricProxyPackets").textContent = `${stats.proxy_pkts || 0} ${t("packets")}`;
@@ -593,9 +658,11 @@ function renderEvents() {
function renderConfig() {
const cfg = state.overview?.config || {};
const site = state.overview?.site_status || {};
const items = [
[t("configMode"), cfg.mode || "--"],
[t("configDomain"), cfg.domain || cfg.mask_host || "--"],
[t("configSiteStatus"), siteStatusText(site)],
[t("configTemplate"), cfg.template_id || cfg.template || "--"],
[t("configVersion"), state.overview?.version || "--"],
[t("bindAddress"), `${state.overview?.admin_bind?.host || "127.0.0.1"}:${state.overview?.admin_bind?.port || 1984}`],
@@ -660,13 +727,20 @@ async function loadLogs() {
const service = $("#logService").value;
const btn = $("#loadLogsBtn");
btn.disabled = true;
$("#logsMeta").textContent = "";
$("#logsBox").textContent = t("loading");
try {
const logs = await api(`/api/logs?service=${encodeURIComponent(service)}`);
const payload = await api(`/api/logs?service=${encodeURIComponent(service)}`);
if ($("#logService").value === service) {
$("#logsBox").textContent = logs;
const structured = payload && typeof payload === "object";
const text = typeof payload === "string" ? payload : (payload?.text || "");
const lines = structured ? (payload.line_count ?? text.split("\n").filter(Boolean).length) : text.split("\n").filter(Boolean).length;
const stateText = structured ? (payload.ok ? "OK" : `exit ${payload.exit_code ?? "?"}`) : "OK";
$("#logsMeta").textContent = `${service} · ${lines} ${t("logsLines")} · ${stateText}`;
$("#logsBox").textContent = text || t("logsNoData");
}
} catch (err) {
$("#logsMeta").textContent = "";
$("#logsBox").textContent = err.message;
} finally {
btn.disabled = false;
@@ -766,6 +840,7 @@ $("#addUserForm").addEventListener("submit", (eventObj) => {
});
$("#refreshBtn").addEventListener("click", refreshAll);
$("#languageSelect").addEventListener("change", (eventObj) => setLanguage(eventObj.target.value));
$("#createBackupBtn").addEventListener("click", createBackup);
$("#loadLogsBtn").addEventListener("click", loadLogs);
$("#repairStatsBtn").addEventListener("click", repairStats);

View File

@@ -11,7 +11,7 @@
document.documentElement.dataset.theme = theme;
}());
</script>
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin3">
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin4">
</head>
<body>
<div class="app-shell">
@@ -48,7 +48,10 @@
<small id="lastRefresh">--</small>
</div>
<div class="top-actions">
<span class="pill" id="languageBadge">EN</span>
<select id="languageSelect" class="language-select" aria-label="Language">
<option value="en">EN</option>
<option value="ru">RU</option>
</select>
<button id="themeToggle" class="ghost" type="button">Theme</button>
<button id="refreshBtn" type="button" data-i18n="refresh">Refresh</button>
</div>
@@ -61,6 +64,7 @@
<span data-i18n="metricMode">Mode</span>
<strong id="metricMode">--</strong>
<small id="metricDomain">--</small>
<small id="siteStatus" class="metric-status">--</small>
</article>
<article class="metric-card accent-green">
<span data-i18n="metricKeys">Keys</span>
@@ -219,6 +223,7 @@
<button id="loadLogsBtn" type="button" data-i18n="loadLogs">Load</button>
</div>
</div>
<div id="logsMeta" class="logs-meta"></div>
<pre id="logsBox" class="logs"></pre>
</div>
</section>
@@ -264,6 +269,6 @@
</div>
<div id="toast" class="toast"></div>
<script src="/app.js?v=2.5.0-admin3" type="module"></script>
<script src="/app.js?v=2.5.0-admin4" type="module"></script>
</body>
</html>

View File

@@ -209,6 +209,12 @@ h2 {
flex-wrap: wrap;
}
.language-select {
width: 78px;
min-width: 78px;
font-weight: 800;
}
.icon-btn {
width: 42px;
padding: 0;
@@ -321,6 +327,33 @@ h2 {
line-height: 1.05;
}
.metric-status {
display: inline-flex;
align-items: center;
width: fit-content;
margin-top: 7px;
border-radius: 8px;
padding: 4px 8px;
background: var(--panel-strong);
font-size: 12px;
font-weight: 800;
}
.metric-status.ok {
background: color-mix(in srgb, var(--green) 16%, transparent);
color: var(--green);
}
.metric-status.warn {
background: color-mix(in srgb, var(--amber) 18%, transparent);
color: var(--amber);
}
.metric-status.error {
background: color-mix(in srgb, var(--red) 16%, transparent);
color: var(--red);
}
.grid-two {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(320px, .9fr);
@@ -566,6 +599,14 @@ td small {
overflow-wrap: anywhere;
}
.logs-meta {
min-height: 24px;
margin: -2px 0 8px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.logs {
min-height: 460px;
max-height: calc(100vh - 260px);