fix(ui): separate server vs browser TZ; highlight sync from device

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-14 17:08:07 +03:00
parent dfae0d8256
commit 40d236c228
6 changed files with 147 additions and 22 deletions

View File

@@ -8,6 +8,7 @@ const logoutBtn = document.querySelector("#logout");
const refreshBtn = document.querySelector("#refresh");
const clockServerEl = document.querySelector("#clock-server");
const clockLocalEl = document.querySelector("#clock-local");
const clockZoneDiffEl = document.querySelector("#clock-zone-diff");
const clockSyncBtn = document.querySelector("#clock-sync");
const rowsEl = document.querySelector("#rows");
const statusEl = document.querySelector("#status");
@@ -320,7 +321,9 @@ function tickServerClockDisplay() {
async function refreshServerClock() {
try {
const t = await api("/api/server-time");
const tz = browserTimeZoneLabel();
const q = tz ? `?browserTz=${encodeURIComponent(tz)}` : "";
const t = await api(`/api/server-time${q}`);
const iso = typeof t.iso === "string" ? t.iso : "";
const parsed = new Date(iso).getTime();
if (!iso || Number.isNaN(parsed)) {
@@ -332,11 +335,28 @@ async function refreshServerClock() {
typeof t.timeZone === "string" && t.timeZone.trim() ? t.timeZone.trim() : "UTC";
serverTzFmtCached = buildServerTzFmt(serverDisplayTz);
tickServerClockDisplay();
if (clockZoneDiffEl) {
const hint = typeof t.zoneCompareHint === "string" ? t.zoneCompareHint.trim() : "";
if (hint) {
clockZoneDiffEl.textContent = hint;
clockZoneDiffEl.classList.remove("hidden");
clockZoneDiffEl.classList.toggle("clock-zone-diff--accent", t.zoneSame === false);
} else {
clockZoneDiffEl.textContent = "";
clockZoneDiffEl.classList.add("hidden");
clockZoneDiffEl.classList.remove("clock-zone-diff--accent");
}
}
} catch {
serverAnchorUtcMs = null;
serverTzFmtCached = null;
clockServerEl.dateTime = "";
clockServerEl.textContent = "—";
if (clockZoneDiffEl) {
clockZoneDiffEl.textContent = "";
clockZoneDiffEl.classList.add("hidden");
clockZoneDiffEl.classList.remove("clock-zone-diff--accent");
}
}
}
@@ -369,6 +389,11 @@ function stopClocks() {
clockLocalEl.dateTime = "";
clockServerEl.textContent = "—";
clockLocalEl.textContent = "—";
if (clockZoneDiffEl) {
clockZoneDiffEl.textContent = "";
clockZoneDiffEl.classList.add("hidden");
clockZoneDiffEl.classList.remove("clock-zone-diff--accent");
}
}
function startClocks() {
@@ -386,8 +411,8 @@ async function loadTimeSyncCaps() {
const c = await api("/api/time-sync-capabilities");
if (hint) {
hint.textContent = c.hostTimeSync
? `Через SSH на root@${c.sshHost}. Часовой пояс строки «Сервер»: ${c.serverClockTimeZone}. Пароль не сохраняется.`
: `Авто-синхронизация по SSH недоступна (или TIME_SYNC_DISABLED). Пояс «Сервер»: ${c.serverClockTimeZone}. Задайте DISPLAY_TZ или TZ для контейнера панели — см. README.`;
? `Записывается UTC-момент с этого устройства на хост по SSH (root@${c.sshHost}). Пояс строки «Сервер»: ${c.serverClockTimeZone}. Пароль не сохраняется.`
: `Авто-синхронизация по SSH недоступна (или TIME_SYNC_DISABLED). Пояс «Сервер»: ${c.serverClockTimeZone}. Задайте TZ контейнера панели при необходимости — см. README.`;
}
if (btn) btn.disabled = !c.hostTimeSync;
} catch {
@@ -404,13 +429,13 @@ document.querySelector("#sync-host-time")?.addEventListener("click", async () =>
return;
}
try {
setStatus("Синхронизация времени хоста…", false);
setStatus("Беру время с этого устройства и отправляю на хост…", false);
await api("/api/sync-host-time", {
method: "POST",
body: JSON.stringify({ rootPassword: pw, unixMs: Date.now() }),
});
inp.value = "";
setStatus("Запрос выполнен. Проверьте строку «Сервер».", false);
setStatus("Готово: часы хоста выставлены по вашему устройству (UTC). Проверьте строки времени.", false);
void refreshServerClock();
} catch (e) {
setStatus(String(e.message || e), true);

View File

@@ -58,27 +58,28 @@
<select id="proto-select" class="proto-select" aria-label="Инстанс AmneziaWG"></select>
</div>
<div class="clock-strip" role="status" aria-live="polite">
<div class="clock-row">
<span class="muted clock-kind">Сервер</span>
<div class="clock-row clock-row--server">
<span class="muted clock-kind">Сервер <span class="clock-kind-sub">(часы VPS / контейнера)</span></span>
<time id="clock-server" class="clock-time"></time>
</div>
<div class="clock-row">
<span class="muted clock-kind">Браузер</span>
<div class="clock-row clock-row--your-place">
<span class="muted clock-kind">Ваше место <span class="clock-kind-sub">(браузер)</span></span>
<time id="clock-local" class="clock-time"></time>
</div>
<p id="clock-zone-diff" class="clock-zone-diff muted hidden" role="note"></p>
<p class="clock-hint muted">
«Сервер» — время в часовом поясе VPS (подпись справа). «Браузер» — ваш компьютер. «Сверить» — заново запросить момент с сервера.
Цифры разнятся, если пояса разные — это нормально для одного момента UTC. «Сверить» подтягивает время с VPS.
</p>
<button type="button" id="clock-sync" class="btn small ghost clock-sync-btn">
Сверить время
</button>
<details class="clock-host-sync">
<summary>Синхронизировать часы VPS с этим компьютером</summary>
<summary class="clock-sync-summary">Синхронизация: выставить часы <strong>хоста VPS</strong> под <strong>ваше устройство</strong></summary>
<p id="sync-host-hint" class="muted clock-sync-hint"></p>
<label for="sync-root-pw">Пароль root на хосте (не сохраняется)</label>
<input id="sync-root-pw" type="password" autocomplete="off" class="clock-sync-input">
<button type="button" id="sync-host-time" class="btn small primary clock-sync-submit">
Выставить время хоста как у браузера
Взять время с этого устройства и записать на хост VPS
</button>
</details>
</div>

View File

@@ -227,6 +227,42 @@ h1 {
max-width: 22rem;
}
.clock-row--server .clock-kind-sub,
.clock-row--your-place .clock-kind-sub {
font-weight: 400;
opacity: 0.82;
font-size: 0.72rem;
}
.clock-row--your-place {
padding: 0.35rem 0.55rem;
margin: 0.15rem -0.35rem 0;
border-radius: 10px;
border-left: 3px solid #22d3ee;
background: rgba(56, 189, 248, 0.07);
}
.clock-zone-diff {
margin: 0.5rem 0 0;
font-size: 0.78rem;
line-height: 1.45;
max-width: 26rem;
}
.clock-zone-diff--accent {
color: #a5f3fc;
padding: 0.45rem 0.55rem;
margin-top: 0.45rem;
border-radius: 8px;
border: 1px solid rgba(34, 211, 238, 0.35);
background: rgba(56, 189, 248, 0.08);
}
.clock-sync-summary strong {
color: var(--text);
font-weight: 650;
}
.clock-strip {
display: flex;
flex-direction: column;
@@ -247,7 +283,9 @@ h1 {
}
.clock-kind {
flex: 0 0 4.25rem;
flex: 0 0 auto;
min-width: 7rem;
max-width: 11rem;
}
.clock-time {