diff --git a/admin-web/static/app.js b/admin-web/static/app.js index c11b4ab..a95b128 100644 --- a/admin-web/static/app.js +++ b/admin-web/static/app.js @@ -11,6 +11,10 @@ const i18n = { navLogs: "Logs", navSettings: "Settings", refresh: "Refresh", + autoRefresh: "Auto refresh every 5 seconds", + autoRefreshOn: "Auto refresh is on", + autoRefreshOff: "Auto refresh is off", + autoRefreshOffShort: "off", themeDark: "Dark", themeLight: "Light", metricMode: "Mode", @@ -234,6 +238,10 @@ const i18n = { navLogs: "Логи", navSettings: "Настройки", refresh: "Обновить", + autoRefresh: "Автообновление каждые 5 секунд", + autoRefreshOn: "Автообновление включено", + autoRefreshOff: "Автообновление выключено", + autoRefreshOffShort: "выкл", themeDark: "Тёмная", themeLight: "Светлая", metricMode: "Режим", @@ -469,11 +477,15 @@ const state = { backupSchedule: null, qrLink: "", pendingUsers: new Set(), + refreshingAll: false, + autoRefreshEnabled: localStorage.getItem("gotelegram-auto-refresh") !== "0", }; const t = (key) => (i18n[state.lang] && i18n[state.lang][key]) || i18n.en[key] || key; const trafficRanges = ["15m", "1h", "24h", "month"]; +const AUTO_REFRESH_MS = 5000; +let autoRefreshTimer = null; const fmtBytes = (value = 0) => { const units = ["B", "KB", "MB", "GB", "TB"]; @@ -527,6 +539,34 @@ const addEvent = (title, detail = "") => { renderEvents(); }; +function updateAutoRefreshToggle() { + const button = $("#autoRefreshToggle"); + if (!button) return; + button.classList.toggle("active", state.autoRefreshEnabled); + button.setAttribute("aria-pressed", String(state.autoRefreshEnabled)); + button.title = state.autoRefreshEnabled ? t("autoRefreshOn") : t("autoRefreshOff"); + const label = button.querySelector(".auto-refresh-state"); + if (label) label.textContent = state.autoRefreshEnabled ? "5s" : t("autoRefreshOffShort"); +} + +function syncAutoRefreshTimer() { + if (autoRefreshTimer) { + clearInterval(autoRefreshTimer); + autoRefreshTimer = null; + } + if (!state.autoRefreshEnabled) return; + autoRefreshTimer = setInterval(() => { + refreshAll().catch((err) => toast(err.message)); + }, AUTO_REFRESH_MS); +} + +function setAutoRefresh(enabled) { + state.autoRefreshEnabled = Boolean(enabled); + localStorage.setItem("gotelegram-auto-refresh", state.autoRefreshEnabled ? "1" : "0"); + updateAutoRefreshToggle(); + syncAutoRefreshTimer(); +} + async function api(path, options = {}) { const headers = { "Accept": "application/json", @@ -564,6 +604,7 @@ function applyI18n() { updateUserTrafficControls(); renderBackupSchedule(); updatePageTitle(); + updateAutoRefreshToggle(); } function setTheme(theme) { @@ -1271,6 +1312,8 @@ function renderConfig() { } async function refreshAll() { + if (state.refreshingAll) return; + state.refreshingAll = true; const btn = $("#refreshBtn"); btn.disabled = true; try { @@ -1305,6 +1348,8 @@ async function refreshAll() { toast(err.message); } finally { btn.disabled = false; + state.refreshingAll = false; + updateAutoRefreshToggle(); } } @@ -1684,6 +1729,7 @@ document.addEventListener("submit", (eventObj) => { }); $("#refreshBtn").addEventListener("click", refreshAll); +$("#autoRefreshToggle").addEventListener("click", () => setAutoRefresh(!state.autoRefreshEnabled)); $("#languageSelect").addEventListener("change", (eventObj) => setLanguage(eventObj.target.value)); $("#promoClose").addEventListener("click", () => { $("#promoModal").hidden = true; @@ -1703,6 +1749,7 @@ window.addEventListener("hashchange", () => setPage((location.hash || "#dashboar setPage((location.hash || "#dashboard").slice(1), false); setTheme(state.theme); renderEvents(); +syncAutoRefreshTimer(); refreshAll(); loadLogs(); maybeShowPromo(); diff --git a/admin-web/static/index.html b/admin-web/static/index.html index 67dce75..69eb042 100644 --- a/admin-web/static/index.html +++ b/admin-web/static/index.html @@ -11,7 +11,7 @@ document.documentElement.dataset.theme = theme; }()); - +
@@ -54,6 +54,11 @@ +
@@ -388,6 +393,6 @@ - + diff --git a/admin-web/static/styles.css b/admin-web/static/styles.css index 34948a3..580772d 100644 --- a/admin-web/static/styles.css +++ b/admin-web/static/styles.css @@ -228,6 +228,65 @@ h2 { flex-wrap: wrap; } +.auto-refresh-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 42px; + padding: 7px 10px; + border: 1px solid var(--line); + background: var(--panel-strong); + color: var(--text); +} + +.auto-refresh-toggle:hover { + transform: translateY(-1px); +} + +.auto-refresh-icon { + color: var(--blue); + font-size: 17px; + line-height: 1; +} + +.auto-refresh-track { + position: relative; + display: inline-flex; + align-items: center; + width: 38px; + height: 22px; + border-radius: 999px; + background: color-mix(in srgb, var(--muted) 28%, transparent); + transition: background .16s ease; +} + +.auto-refresh-track span { + position: absolute; + left: 3px; + width: 16px; + height: 16px; + border-radius: 999px; + background: var(--panel); + box-shadow: 0 2px 8px rgba(15, 23, 42, .18); + transition: transform .16s ease; +} + +.auto-refresh-toggle.active .auto-refresh-track { + background: color-mix(in srgb, var(--green) 58%, transparent); +} + +.auto-refresh-toggle.active .auto-refresh-track span { + transform: translateX(16px); +} + +.auto-refresh-state { + min-width: 22px; + color: var(--muted); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + .language-select { width: 78px; min-width: 78px; diff --git a/tests/test_admin_features.py b/tests/test_admin_features.py index 5095f7f..91b45d6 100644 --- a/tests/test_admin_features.py +++ b/tests/test_admin_features.py @@ -199,6 +199,19 @@ class AdminFeatureTests(unittest.TestCase): self.assertIn('class="keys-list" id="usersTable"', index) self.assertNotIn('class="keys-table"', index) + def test_topbar_has_five_second_auto_refresh_toggle(self): + app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8") + styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8") + index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8") + + self.assertIn('id="autoRefreshToggle"', index) + self.assertIn('data-i18n-title="autoRefresh"', index) + self.assertIn("gotelegram-auto-refresh", app_js) + self.assertIn("AUTO_REFRESH_MS = 5000", app_js) + self.assertIn("setInterval", app_js) + self.assertIn("clearInterval", app_js) + self.assertIn(".auto-refresh-toggle", styles) + if __name__ == "__main__": unittest.main()