diff --git a/public/app.js b/public/app.js index 5a5869a..b30762d 100644 --- a/public/app.js +++ b/public/app.js @@ -6,6 +6,8 @@ const loginError = document.querySelector("#login-error"); const logoutBtn = document.querySelector("#logout"); const refreshBtn = document.querySelector("#refresh"); +const clockServerEl = document.querySelector("#clock-server"); +const clockLocalEl = document.querySelector("#clock-local"); const rowsEl = document.querySelector("#rows"); const statusEl = document.querySelector("#status"); const peerCountEl = document.querySelector("#peer-count"); @@ -129,6 +131,7 @@ dtOk.addEventListener("click", async () => { }); function showLogin() { + stopClocks(); loginGate.classList.remove("hidden"); loginGate.setAttribute("aria-hidden", "false"); appRoot.classList.add("hidden"); @@ -138,6 +141,7 @@ function showApp() { loginGate.classList.add("hidden"); loginGate.setAttribute("aria-hidden", "true"); appRoot.classList.remove("hidden"); + startClocks(); } function setStatus(text, isErr) { @@ -212,6 +216,60 @@ refreshBtn.addEventListener("click", () => { loadClients(); }); +const clockFmt = new Intl.DateTimeFormat("ru-RU", { + dateStyle: "medium", + timeStyle: "medium", +}); + +/** @type {ReturnType | null} */ +let clockTickId = null; +/** @type {ReturnType | null} */ +let clockServerPollId = null; + +async function refreshServerClock() { + try { + const t = await api("/api/server-time"); + const iso = typeof t.iso === "string" ? t.iso : ""; + clockServerEl.dateTime = iso; + const tz = typeof t.timeZone === "string" ? t.timeZone : ""; + const formatted = typeof t.formatted === "string" ? t.formatted : ""; + clockServerEl.textContent = + formatted.trim() ? `${formatted}${tz ? ` · ${tz}` : ""}` : "—"; + } catch { + clockServerEl.dateTime = ""; + clockServerEl.textContent = "—"; + } +} + +function tickLocalClock() { + const n = new Date(); + clockLocalEl.dateTime = n.toISOString(); + clockLocalEl.textContent = clockFmt.format(n); +} + +function stopClocks() { + if (clockTickId !== null) { + clearInterval(clockTickId); + clockTickId = null; + } + if (clockServerPollId !== null) { + clearInterval(clockServerPollId); + clockServerPollId = null; + } + clockServerEl.dateTime = ""; + clockLocalEl.dateTime = ""; + clockServerEl.textContent = "—"; + clockLocalEl.textContent = "—"; +} + +function startClocks() { + stopClocks(); + tickLocalClock(); + void refreshServerClock(); + clockTickId = setInterval(tickLocalClock, 1000); + clockServerPollId = setInterval(() => void refreshServerClock(), 30_000); +} + const dtRu = new Intl.DateTimeFormat("ru-RU", { dateStyle: "short", timeStyle: "short", @@ -374,6 +432,7 @@ async function loadClients() { wgShowEl.textContent = data.wgShow || ""; renderRows(data.clients); setStatus("", false); + void refreshServerClock(); } catch (e) { const msg = String(e.message || e); if (msg.includes("Unauthorized")) { diff --git a/public/index.html b/public/index.html index 316e781..6a5d742 100644 --- a/public/index.html +++ b/public/index.html @@ -53,6 +53,16 @@
Протокол: AmneziaWG
+
+
+ Сервер + +
+
+ Браузер + +
+
diff --git a/public/styles.css b/public/styles.css index 6672e11..c415659 100644 --- a/public/styles.css +++ b/public/styles.css @@ -205,6 +205,35 @@ h1 { flex-wrap: wrap; } +.clock-strip { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 0.45rem 0.75rem; + border-radius: 12px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.03); + font-size: 0.82rem; + line-height: 1.35; +} + +.clock-row { + display: flex; + align-items: baseline; + gap: 0.5rem; + flex-wrap: wrap; +} + +.clock-kind { + flex: 0 0 4.25rem; +} + +.clock-time { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-size: 0.84rem; + color: var(--text); +} + .pill { display: inline-flex; align-items: center; diff --git a/server.js b/server.js index 9e1e4f8..fc03afe 100644 --- a/server.js +++ b/server.js @@ -344,6 +344,24 @@ app.get("/api/session", (req, res) => { res.json({ ok: true }); }); +app.get("/api/server-time", requireAuth, (_req, res) => { + const now = new Date(); + let timeZone = "UTC"; + try { + timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || timeZone; + } catch { + /* ignore */ + } + res.json({ + iso: now.toISOString(), + formatted: now.toLocaleString("ru-RU", { + dateStyle: "medium", + timeStyle: "medium", + }), + timeZone, + }); +}); + app.post("/api/login", (req, res) => { const pw = req.body?.password; if (typeof pw !== "string" || !pw) {