mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
feat(time): server TZ display; optional SSH host sync via root pw
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -224,6 +224,7 @@ loginForm.addEventListener("submit", async (ev) => {
|
||||
loginPassword.value = "";
|
||||
showApp();
|
||||
await loadProtocols();
|
||||
await loadTimeSyncCaps();
|
||||
await loadClients();
|
||||
} catch (e) {
|
||||
loginError.textContent = String(e.message || e);
|
||||
@@ -269,6 +270,23 @@ const clockFmt = new Intl.DateTimeFormat("ru-RU", {
|
||||
timeStyle: "medium",
|
||||
});
|
||||
|
||||
/** Часовой пояс строки «Сервер» (IANA), как в /api/server-time */
|
||||
let serverDisplayTz = "UTC";
|
||||
/** @type {Intl.DateTimeFormat | null} */
|
||||
let serverTzFmtCached = null;
|
||||
|
||||
function buildServerTzFmt(tz) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium",
|
||||
timeZone: tz,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
let clockTickId = null;
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
@@ -276,7 +294,7 @@ let clockServerPollId = null;
|
||||
|
||||
/** Метка UTC сервера (мс) по последнему ответу API */
|
||||
let serverAnchorUtcMs = /** @type {number | null} */ (null);
|
||||
/** Date.now() в момент установки якоря (компенсация сети только между опросами) */
|
||||
/** Date.now() в момент установки якоря */
|
||||
let serverAnchorWallMs = 0;
|
||||
|
||||
function browserTimeZoneLabel() {
|
||||
@@ -296,8 +314,8 @@ function tickServerClockDisplay() {
|
||||
const estimatedUtcMs = serverAnchorUtcMs + (Date.now() - serverAnchorWallMs);
|
||||
const d = new Date(estimatedUtcMs);
|
||||
clockServerEl.dateTime = d.toISOString();
|
||||
const tz = browserTimeZoneLabel();
|
||||
clockServerEl.textContent = tz ? `${clockFmt.format(d)} · ${tz}` : clockFmt.format(d);
|
||||
const fmt = serverTzFmtCached || clockFmt;
|
||||
clockServerEl.textContent = `${fmt.format(d)} · ${serverDisplayTz}`;
|
||||
}
|
||||
|
||||
async function refreshServerClock() {
|
||||
@@ -310,9 +328,13 @@ async function refreshServerClock() {
|
||||
}
|
||||
serverAnchorUtcMs = parsed;
|
||||
serverAnchorWallMs = Date.now();
|
||||
serverDisplayTz =
|
||||
typeof t.timeZone === "string" && t.timeZone.trim() ? t.timeZone.trim() : "UTC";
|
||||
serverTzFmtCached = buildServerTzFmt(serverDisplayTz);
|
||||
tickServerClockDisplay();
|
||||
} catch {
|
||||
serverAnchorUtcMs = null;
|
||||
serverTzFmtCached = null;
|
||||
clockServerEl.dateTime = "";
|
||||
clockServerEl.textContent = "—";
|
||||
}
|
||||
@@ -341,6 +363,8 @@ function stopClocks() {
|
||||
}
|
||||
serverAnchorUtcMs = null;
|
||||
serverAnchorWallMs = 0;
|
||||
serverTzFmtCached = null;
|
||||
serverDisplayTz = "UTC";
|
||||
clockServerEl.dateTime = "";
|
||||
clockLocalEl.dateTime = "";
|
||||
clockServerEl.textContent = "—";
|
||||
@@ -355,6 +379,44 @@ function startClocks() {
|
||||
clockServerPollId = setInterval(() => void refreshServerClock(), 30_000);
|
||||
}
|
||||
|
||||
async function loadTimeSyncCaps() {
|
||||
const hint = document.querySelector("#sync-host-hint");
|
||||
const btn = document.querySelector("#sync-host-time");
|
||||
try {
|
||||
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.`;
|
||||
}
|
||||
if (btn) btn.disabled = !c.hostTimeSync;
|
||||
} catch {
|
||||
if (hint) hint.textContent = "";
|
||||
if (btn) btn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector("#sync-host-time")?.addEventListener("click", async () => {
|
||||
const inp = document.querySelector("#sync-root-pw");
|
||||
const pw = inp && typeof inp.value === "string" ? inp.value : "";
|
||||
if (!pw.trim()) {
|
||||
setStatus("Введите пароль root на хосте.", true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setStatus("Синхронизация времени хоста…", false);
|
||||
await api("/api/sync-host-time", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ rootPassword: pw, unixMs: Date.now() }),
|
||||
});
|
||||
inp.value = "";
|
||||
setStatus("Запрос выполнен. Проверьте строку «Сервер».", false);
|
||||
void refreshServerClock();
|
||||
} catch (e) {
|
||||
setStatus(String(e.message || e), true);
|
||||
}
|
||||
});
|
||||
|
||||
const dtRu = new Intl.DateTimeFormat("ru-RU", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
@@ -545,6 +607,7 @@ async function boot() {
|
||||
if (ok) {
|
||||
showApp();
|
||||
await loadProtocols();
|
||||
await loadTimeSyncCaps();
|
||||
await loadClients();
|
||||
} else {
|
||||
showLogin();
|
||||
|
||||
@@ -67,11 +67,20 @@
|
||||
<time id="clock-local" class="clock-time">—</time>
|
||||
</div>
|
||||
<p class="clock-hint muted">
|
||||
Сервер показывается в вашем часовом поясе (как браузер). «Сверить» — заново запросить момент с VPS.
|
||||
«Сервер» — время в часовом поясе VPS (подпись справа). «Браузер» — ваш компьютер. «Сверить» — заново запросить момент с сервера.
|
||||
</p>
|
||||
<button type="button" id="clock-sync" class="btn small ghost clock-sync-btn">
|
||||
Сверить время
|
||||
</button>
|
||||
<details class="clock-host-sync">
|
||||
<summary>Синхронизировать часы VPS с этим компьютером</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">
|
||||
Выставить время хоста как у браузера
|
||||
</button>
|
||||
</details>
|
||||
</div>
|
||||
<button type="button" id="refresh" class="btn primary">Обновить</button>
|
||||
</section>
|
||||
|
||||
@@ -268,6 +268,43 @@ h1 {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.clock-host-sync {
|
||||
margin-top: 0.65rem;
|
||||
padding-top: 0.55rem;
|
||||
border-top: 1px solid var(--line);
|
||||
max-width: 26rem;
|
||||
}
|
||||
|
||||
.clock-host-sync summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.84rem;
|
||||
color: var(--accent, #38bdf8);
|
||||
}
|
||||
|
||||
.clock-sync-hint {
|
||||
margin: 0.45rem 0 0.5rem;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.clock-sync-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 22rem;
|
||||
margin-top: 0.35rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: #0a0f16;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.clock-sync-submit {
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user