Files
Seth a6b3f039d8 Initial commit — Seth Calendar & Decimal Time clock site
Pages: /, /simple, /decimal, /seth, /calendar, /astro, /convert, /timegov
Features: Seth Calendar (10×36 + holidays), decimal time, moon phases,
astronomy (sun/moon), bidirectional time converter, Seth date display,
leap day split cell in calendar grid.
2026-03-08 22:32:38 +00:00

92 lines
2.6 KiB
JavaScript

const zoneSelect = document.getElementById("zoneSelect");
const zoneLabel = document.getElementById("zoneLabel");
const offsetLabel = document.getElementById("offsetLabel");
const utcLabel = document.getElementById("utcLabel");
const dateLine = document.getElementById("dateLine");
const digital = document.getElementById("digital");
const zones = [
"America/New_York",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"America/Anchorage",
"Pacific/Honolulu",
"UTC"
];
let serverOffsetMs = 0;
let selectedZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "America/New_York";
if (!zones.includes(selectedZone)) {
selectedZone = "America/New_York";
}
for (const zone of zones) {
const option = document.createElement("option");
option.value = zone;
option.textContent = zone;
if (zone === selectedZone) {
option.selected = true;
}
zoneSelect.appendChild(option);
}
zoneSelect.addEventListener("change", () => {
selectedZone = zoneSelect.value;
zoneLabel.textContent = selectedZone;
});
async function syncWithServer() {
const start = Date.now();
try {
const response = await fetch(`/api/time?ts=${start}`, { cache: "no-store" });
const end = Date.now();
const data = await response.json();
const rtt = end - start;
const estimatedServerAtEnd = data.epoch_ms + rtt / 2;
serverOffsetMs = estimatedServerAtEnd - end;
offsetLabel.textContent = `Synchronized (${serverOffsetMs >= 0 ? "+" : ""}${serverOffsetMs.toFixed(0)} ms, RTT ${rtt} ms)`;
} catch (_error) {
offsetLabel.textContent = "Sync failed";
}
}
function formatParts(date, zone) {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: zone,
hour12: false,
year: "numeric",
month: "long",
day: "2-digit",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
return formatter.formatToParts(date).reduce((accumulator, part) => {
if (part.type !== "literal") {
accumulator[part.type] = part.value;
}
return accumulator;
}, {});
}
function render() {
const now = new Date(Date.now() + serverOffsetMs);
const parts = formatParts(now, selectedZone);
const centiseconds = String(Math.floor(now.getMilliseconds() / 10)).padStart(2, "0");
dateLine.textContent = `${parts.weekday}, ${parts.month} ${parts.day}, ${parts.year}`;
digital.textContent = `${parts.hour}:${parts.minute}:${parts.second}.${centiseconds}`;
zoneLabel.textContent = selectedZone;
utcLabel.textContent = now.toISOString().replace("T", " ").replace("Z", " UTC");
requestAnimationFrame(render);
}
syncWithServer();
setInterval(syncWithServer, 20000);
render();