feat(webui): sync host clock and timezone from browser
Adds POST /api/system/time-sync so the dashboard's "Sync with this browser" button can set the host's date, time, and IANA timezone from the client's browser — useful for appliances with no NTP/network access to keep their own clock straight. Disables NTP before writing the manual time so it doesn't get immediately overwritten. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b2b3f86c8d
commit
3ec7ca08da
@@ -1483,6 +1483,61 @@ func (h *handler) handleAPISystemShutdown(w http.ResponseWriter, r *http.Request
|
|||||||
writeJSON(w, map[string]string{"status": "shutting down"})
|
writeJSON(w, map[string]string{"status": "shutting down"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// timezoneNameRE matches IANA timezone identifiers like "Europe/Moscow" or "UTC".
|
||||||
|
var timezoneNameRE = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+)*$`)
|
||||||
|
|
||||||
|
func validTimezoneName(tz string) bool {
|
||||||
|
if tz == "" || !timezoneNameRE.MatchString(tz) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := os.Stat(filepath.Join("/usr/share/zoneinfo", tz))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAPISystemTimeSync sets the host's timezone and wall-clock time from
|
||||||
|
// values supplied by the client's browser (used when the appliance has no
|
||||||
|
// network/NTP access to keep its own clock in sync).
|
||||||
|
func (h *handler) handleAPISystemTimeSync(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
EpochMS int64 `json:"epoch_ms"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.EpochMS <= 0 {
|
||||||
|
writeError(w, http.StatusBadRequest, "epoch_ms required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
|
||||||
|
if req.Timezone != "" {
|
||||||
|
if !validTimezoneName(req.Timezone) {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid timezone")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b, err := exec.Command("timedatectl", "set-timezone", req.Timezone).CombinedOutput(); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "set-timezone failed: "+strings.TrimSpace(string(b)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&out, "timezone set to %s\n", req.Timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual time only sticks if NTP sync is off.
|
||||||
|
_ = exec.Command("timedatectl", "set-ntp", "false").Run()
|
||||||
|
|
||||||
|
sec := req.EpochMS / 1000
|
||||||
|
if b, err := exec.Command("date", "-u", "-s", "@"+strconv.FormatInt(sec, 10)).CombinedOutput(); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "set-time failed: "+strings.TrimSpace(string(b)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.WriteString("system clock synced\n")
|
||||||
|
|
||||||
|
writeJSON(w, map[string]string{"status": "ok", "output": out.String()})
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tools ─────────────────────────────────────────────────────────────────────
|
// ── Tools ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
var standardTools = []string{
|
var standardTools = []string{
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
|||||||
mux.HandleFunc("POST /api/system/install-to-ram", h.handleAPIInstallToRAM)
|
mux.HandleFunc("POST /api/system/install-to-ram", h.handleAPIInstallToRAM)
|
||||||
mux.HandleFunc("POST /api/system/reboot", h.handleAPISystemReboot)
|
mux.HandleFunc("POST /api/system/reboot", h.handleAPISystemReboot)
|
||||||
mux.HandleFunc("POST /api/system/shutdown", h.handleAPISystemShutdown)
|
mux.HandleFunc("POST /api/system/shutdown", h.handleAPISystemShutdown)
|
||||||
|
mux.HandleFunc("POST /api/system/time-sync", h.handleAPISystemTimeSync)
|
||||||
|
|
||||||
// Preflight
|
// Preflight
|
||||||
mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)
|
mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)
|
||||||
|
|||||||
Reference in New Issue
Block a user