From 3f63cb246b2d973fe175ddbe4e34cca34ab4d8e4 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sun, 25 Sep 2022 19:38:28 +0800 Subject: [PATCH] [WIP] Handle timezone offset for timeRange --- server/model/maintenance.js | 41 +++++++++++++---- .../maintenance-socket-handler.js | 17 ++++--- server/util-server.js | 46 +++++++++++++++++++ src/languages/en.js | 2 +- src/pages/EditMaintenance.vue | 21 +++++---- src/util.js | 10 ++-- src/util.ts | 5 +- 7 files changed, 110 insertions(+), 32 deletions(-) diff --git a/server/model/maintenance.js b/server/model/maintenance.js index a27a358b1..3b07d5f7c 100644 --- a/server/model/maintenance.js +++ b/server/model/maintenance.js @@ -4,17 +4,19 @@ let timezone = require("dayjs/plugin/timezone"); dayjs.extend(utc); dayjs.extend(timezone); const { BeanModel } = require("redbean-node/dist/bean-model"); -const { parseVueDatePickerTimeFormat, parseTimeFormatFromVueDatePicker } = require("../../src/util"); +const { parseTimeObject, parseTimeFromTimeObject } = require("../../src/util"); const { isArray } = require("chart.js/helpers"); +const { timeObjectToUTC, timeObjectToLocal } = require("../util-server"); class Maintenance extends BeanModel { /** * Return an object that ready to parse to JSON for public * Only show necessary data to public + * @param {string} timezone If not specified, the timeRange will be in UTC * @returns {Object} */ - async toPublicJSON() { + async toPublicJSON(timezone = null) { let dateTimeRange = []; if (this.start_datetime) { @@ -33,11 +35,21 @@ class Maintenance extends BeanModel { } let timeRange = []; - let startTime = parseVueDatePickerTimeFormat(this.start_time); + let startTime = parseTimeObject(this.start_time); timeRange.push(startTime); - let endTime = parseVueDatePickerTimeFormat(this.end_time); + let endTime = parseTimeObject(this.end_time); timeRange.push(endTime); + // Apply timezone offset + if (timezone) { + if (this.start_time) { + timeObjectToLocal(startTime, timezone); + } + if (this.end_time) { + timeObjectToLocal(endTime, timezone); + } + } + let obj = { id: this.id, title: this.title, @@ -65,17 +77,28 @@ class Maintenance extends BeanModel { /** * Return an object that ready to parse to JSON + * @param {string} timezone If not specified, the timeRange will be in UTC * @returns {Object} */ - async toJSON() { - return this.toPublicJSON(); + async toJSON(timezone = null) { + return this.toPublicJSON(timezone); } - static jsonToBean(bean, obj) { + static jsonToBean(bean, obj, timezone) { if (obj.id) { bean.id = obj.id; } + // Apply timezone offset to timeRange, as it cannot apply automatically. + if (timezone) { + if (obj.timeRange[0]) { + timeObjectToUTC(obj.timeRange[0], timezone); + if (obj.timeRange[1]) { + timeObjectToUTC(obj.timeRange[1], timezone); + } + } + } + bean.title = obj.title; bean.description = obj.description; bean.strategy = obj.strategy; @@ -98,8 +121,8 @@ class Maintenance extends BeanModel { } } - bean.start_time = parseTimeFormatFromVueDatePicker(obj.timeRange[0]); - bean.end_time = parseTimeFormatFromVueDatePicker(obj.timeRange[1]); + bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]); + bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]); bean.weekdays = JSON.stringify(obj.weekdays); bean.days_of_month = JSON.stringify(obj.daysOfMonth); diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js index df4a9a35e..604f07bd1 100644 --- a/server/socket-handlers/maintenance-socket-handler.js +++ b/server/socket-handlers/maintenance-socket-handler.js @@ -5,6 +5,11 @@ const apicache = require("../modules/apicache"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const Maintenance = require("../model/maintenance"); const server = UptimeKumaServer.getInstance(); +const dayjs = require("dayjs"); +const utc = require("dayjs/plugin/utc"); +let timezone = require("dayjs/plugin/timezone"); +dayjs.extend(utc); +dayjs.extend(timezone); /** * Handlers for Maintenance @@ -12,13 +17,13 @@ const server = UptimeKumaServer.getInstance(); */ module.exports.maintenanceSocketHandler = (socket) => { // Add a new maintenance - socket.on("addMaintenance", async (maintenance, callback) => { + socket.on("addMaintenance", async (maintenance, timezone, callback) => { try { checkLogin(socket); log.debug("maintenance", maintenance); - let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance); + let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance, timezone); bean.user_id = socket.userID; let maintenanceID = await R.store(bean); @@ -39,7 +44,7 @@ module.exports.maintenanceSocketHandler = (socket) => { }); // Edit a maintenance - socket.on("editMaintenance", async (maintenance, callback) => { + socket.on("editMaintenance", async (maintenance, timezone, callback) => { try { checkLogin(socket); @@ -49,7 +54,7 @@ module.exports.maintenanceSocketHandler = (socket) => { throw new Error("Permission denied."); } - Maintenance.jsonToBean(bean, maintenance); + Maintenance.jsonToBean(bean, maintenance, timezone); await R.store(bean); @@ -138,7 +143,7 @@ module.exports.maintenanceSocketHandler = (socket) => { } }); - socket.on("getMaintenance", async (maintenanceID, callback) => { + socket.on("getMaintenance", async (maintenanceID, timezone, callback) => { try { checkLogin(socket); @@ -151,7 +156,7 @@ module.exports.maintenanceSocketHandler = (socket) => { callback({ ok: true, - maintenance: await bean.toJSON(), + maintenance: await bean.toJSON(timezone), }); } catch (e) { diff --git a/server/util-server.js b/server/util-server.js index 1517bcfe8..cc5e478d1 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -21,6 +21,11 @@ const { rfc2865: { file, attributes }, }, } = require("node-radius-utils"); +const dayjs = require("dayjs"); +const utc = require("dayjs/plugin/utc"); +let timezone = require("dayjs/plugin/timezone"); +dayjs.extend(utc); +dayjs.extend(timezone); // From ping-lite exports.WIN = /^win/.test(process.platform); @@ -645,3 +650,44 @@ module.exports.send403 = (res, msg = "") => { "msg": msg, }); }; + +function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) { + // e.g. +08:00 + let offsetString = dayjs().tz(timezone).format("Z"); + let hours = parseInt(offsetString.substring(1, 3)); + let minutes = parseInt(offsetString.substring(4, 6)); + + if ( + (timeObjectToUTC && offsetString.startsWith("+")) || + (!timeObjectToUTC && offsetString.startsWith("-")) + ) { + hours *= -1; + minutes *= -1; + } + + obj.hours += hours; + obj.minutes += minutes; + + // Handle out of bound + if (obj.hours < 0) { + obj.hours += 24; + } else if (obj.hours > 24) { + obj.hours -= 24; + } + + if (obj.minutes < 0) { + obj.minutes += 24; + } else if (obj.minutes > 24) { + obj.minutes -= 24; + } + + return obj; +} + +module.exports.timeObjectToUTC = (obj, timezone) => { + return timeObjectConvertTimezone(obj, timezone, true); +}; + +module.exports.timeObjectToLocal = (obj, timezone) => { + return timeObjectConvertTimezone(obj, timezone, false); +}; diff --git a/src/languages/en.js b/src/languages/en.js index d729d3a53..e77a31f41 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -607,7 +607,7 @@ export default { recurringInterval: "Interval", "Recurring": "Recurring", strategyManual: "Active/Inactive Manually", - warningTimezone: "It is NOT your current browser's timezone. It is your server's timezone.", + warningTimezone: "It is using your current Device/PC's timezone.", weekdayShortMon: "Mon", weekdayShortTue: "Tue", weekdayShortWed: "Wed", diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue index 3d8ab838d..640962d7d 100644 --- a/src/pages/EditMaintenance.vue +++ b/src/pages/EditMaintenance.vue @@ -109,7 +109,6 @@ :monthChangeOnScroll="false" :minDate="minDate" format="yyyy-MM-dd HH:mm" - utc="preserve" /> @@ -185,8 +184,8 @@ @@ -201,7 +200,7 @@ :monthChangeOnScroll="false" :minDate="minDate" :enableTimePicker="false" - utc="preserve" + :utc="true" /> @@ -357,6 +356,9 @@ export default { }, methods: { init() { + // Use browser's timezone! + let timezone = dayjs.tz.guess(); + this.affectedMonitors = []; this.selectedStatusPages = []; @@ -380,10 +382,8 @@ export default { daysOfMonth: [], }; } else if (this.isEdit) { - this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => { + this.$root.getSocket().emit("getMaintenance", this.$route.params.id, timezone, (res) => { if (res.ok) { - res.maintenance.start_date = this.$root.datetimeFormat(res.maintenance.start_date, "YYYY-MM-DDTHH:mm"); - res.maintenance.end_date = this.$root.datetimeFormat(res.maintenance.end_date, "YYYY-MM-DDTHH:mm"); this.maintenance = res.maintenance; this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => { @@ -441,8 +441,11 @@ export default { this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date); */ + // Use browser's timezone! + let timezone = dayjs.tz.guess(); + if (this.isAdd) { - this.$root.addMaintenance(this.maintenance, async (res) => { + this.$root.addMaintenance(this.maintenance, timezone, async (res) => { if (res.ok) { await this.addMonitorMaintenance(res.maintenanceID, async () => { await this.addMaintenanceStatusPage(res.maintenanceID, () => { @@ -459,7 +462,7 @@ export default { }); } else { - this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => { + this.$root.getSocket().emit("editMaintenance", this.maintenance, timezone, async (res) => { if (res.ok) { await this.addMonitorMaintenance(res.maintenanceID, async () => { await this.addMaintenanceStatusPage(res.maintenanceID, () => { diff --git a/src/util.js b/src/util.js index 73f5369d0..15427cddb 100644 --- a/src/util.js +++ b/src/util.js @@ -7,7 +7,7 @@ // Backend uses the compiled file util.js // Frontend uses util.ts Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseTimeFormatFromVueDatePicker = exports.parseVueDatePickerTimeFormat = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; +exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; const _dayjs = require("dayjs"); const dayjs = _dayjs; exports.isDev = process.env.NODE_ENV === "development"; @@ -314,7 +314,7 @@ exports.getMaintenanceRelativeURL = getMaintenanceRelativeURL; * @param {string} time E.g. 12:00 * @returns object */ -function parseVueDatePickerTimeFormat(time) { +function parseTimeObject(time) { if (!time) { return { hours: 0, @@ -335,11 +335,11 @@ function parseVueDatePickerTimeFormat(time) { } return obj; } -exports.parseVueDatePickerTimeFormat = parseVueDatePickerTimeFormat; +exports.parseTimeObject = parseTimeObject; /** * @returns string e.g. 12:00 */ -function parseTimeFormatFromVueDatePicker(obj) { +function parseTimeFromTimeObject(obj) { if (!obj) { return obj; } @@ -350,4 +350,4 @@ function parseTimeFormatFromVueDatePicker(obj) { } return result; } -exports.parseTimeFormatFromVueDatePicker = parseTimeFormatFromVueDatePicker; +exports.parseTimeFromTimeObject = parseTimeFromTimeObject; diff --git a/src/util.ts b/src/util.ts index 92da0fd56..cb51250b9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -348,7 +348,7 @@ export function getMaintenanceRelativeURL(id: string) { * @param {string} time E.g. 12:00 * @returns object */ -export function parseVueDatePickerTimeFormat(time: string) { +export function parseTimeObject(time: string) { if (!time) { return { hours: 0, @@ -376,7 +376,7 @@ export function parseVueDatePickerTimeFormat(time: string) { /** * @returns string e.g. 12:00 */ -export function parseTimeFormatFromVueDatePicker(obj : any) { +export function parseTimeFromTimeObject(obj : any) { if (!obj) { return obj; } @@ -391,3 +391,4 @@ export function parseTimeFormatFromVueDatePicker(obj : any) { return result; } +