mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
feat(ui): show server and browser date/time in toolbar
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,8 @@ const loginError = document.querySelector("#login-error");
|
|||||||
|
|
||||||
const logoutBtn = document.querySelector("#logout");
|
const logoutBtn = document.querySelector("#logout");
|
||||||
const refreshBtn = document.querySelector("#refresh");
|
const refreshBtn = document.querySelector("#refresh");
|
||||||
|
const clockServerEl = document.querySelector("#clock-server");
|
||||||
|
const clockLocalEl = document.querySelector("#clock-local");
|
||||||
const rowsEl = document.querySelector("#rows");
|
const rowsEl = document.querySelector("#rows");
|
||||||
const statusEl = document.querySelector("#status");
|
const statusEl = document.querySelector("#status");
|
||||||
const peerCountEl = document.querySelector("#peer-count");
|
const peerCountEl = document.querySelector("#peer-count");
|
||||||
@@ -129,6 +131,7 @@ dtOk.addEventListener("click", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function showLogin() {
|
function showLogin() {
|
||||||
|
stopClocks();
|
||||||
loginGate.classList.remove("hidden");
|
loginGate.classList.remove("hidden");
|
||||||
loginGate.setAttribute("aria-hidden", "false");
|
loginGate.setAttribute("aria-hidden", "false");
|
||||||
appRoot.classList.add("hidden");
|
appRoot.classList.add("hidden");
|
||||||
@@ -138,6 +141,7 @@ function showApp() {
|
|||||||
loginGate.classList.add("hidden");
|
loginGate.classList.add("hidden");
|
||||||
loginGate.setAttribute("aria-hidden", "true");
|
loginGate.setAttribute("aria-hidden", "true");
|
||||||
appRoot.classList.remove("hidden");
|
appRoot.classList.remove("hidden");
|
||||||
|
startClocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(text, isErr) {
|
function setStatus(text, isErr) {
|
||||||
@@ -212,6 +216,60 @@ refreshBtn.addEventListener("click", () => {
|
|||||||
loadClients();
|
loadClients();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clockFmt = new Intl.DateTimeFormat("ru-RU", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @type {ReturnType<typeof setInterval> | null} */
|
||||||
|
let clockTickId = null;
|
||||||
|
/** @type {ReturnType<typeof setInterval> | 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", {
|
const dtRu = new Intl.DateTimeFormat("ru-RU", {
|
||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
@@ -374,6 +432,7 @@ async function loadClients() {
|
|||||||
wgShowEl.textContent = data.wgShow || "";
|
wgShowEl.textContent = data.wgShow || "";
|
||||||
renderRows(data.clients);
|
renderRows(data.clients);
|
||||||
setStatus("", false);
|
setStatus("", false);
|
||||||
|
void refreshServerClock();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = String(e.message || e);
|
const msg = String(e.message || e);
|
||||||
if (msg.includes("Unauthorized")) {
|
if (msg.includes("Unauthorized")) {
|
||||||
|
|||||||
@@ -53,6 +53,16 @@
|
|||||||
|
|
||||||
<section class="toolbar">
|
<section class="toolbar">
|
||||||
<div class="pill"><span class="dot ok"></span><span id="proto-label">Протокол: AmneziaWG</span></div>
|
<div class="pill"><span class="dot ok"></span><span id="proto-label">Протокол: AmneziaWG</span></div>
|
||||||
|
<div class="clock-strip" role="status" aria-live="polite">
|
||||||
|
<div class="clock-row">
|
||||||
|
<span class="muted clock-kind">Сервер</span>
|
||||||
|
<time id="clock-server" class="clock-time">—</time>
|
||||||
|
</div>
|
||||||
|
<div class="clock-row">
|
||||||
|
<span class="muted clock-kind">Браузер</span>
|
||||||
|
<time id="clock-local" class="clock-time">—</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="button" id="refresh" class="btn primary">Обновить</button>
|
<button type="button" id="refresh" class="btn primary">Обновить</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,35 @@ h1 {
|
|||||||
flex-wrap: wrap;
|
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 {
|
.pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
18
server.js
18
server.js
@@ -344,6 +344,24 @@ app.get("/api/session", (req, res) => {
|
|||||||
res.json({ ok: true });
|
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) => {
|
app.post("/api/login", (req, res) => {
|
||||||
const pw = req.body?.password;
|
const pw = req.body?.password;
|
||||||
if (typeof pw !== "string" || !pw) {
|
if (typeof pw !== "string" || !pw) {
|
||||||
|
|||||||
Reference in New Issue
Block a user