Merge branch 'master' into copilot/remove-password-complexity-requirements
This commit is contained in:
commit
95b97f8c95
@ -165,7 +165,7 @@ class Database {
|
||||
* Read the database config
|
||||
* @throws {Error} If the config is invalid
|
||||
* @typedef {string|undefined} envString
|
||||
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
|
||||
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString, socketPath:envString}} Database config
|
||||
*/
|
||||
static readDBConfig() {
|
||||
let dbConfig;
|
||||
@ -185,7 +185,7 @@ class Database {
|
||||
|
||||
/**
|
||||
* @typedef {string|undefined} envString
|
||||
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
|
||||
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString, socketPath:envString}} dbConfig the database configuration that should be written
|
||||
* @returns {void}
|
||||
*/
|
||||
static writeDBConfig(dbConfig) {
|
||||
@ -284,6 +284,7 @@ class Database {
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
socketPath: dbConfig.socketPath,
|
||||
...(dbConfig.ssl
|
||||
? {
|
||||
ssl: {
|
||||
@ -309,6 +310,7 @@ class Database {
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.dbName,
|
||||
socketPath: dbConfig.socketPath,
|
||||
timezone: "Z",
|
||||
typeCast: function (field, next) {
|
||||
if (field.type === "DATETIME") {
|
||||
|
||||
@ -1505,24 +1505,46 @@ class Monitor extends BeanModel {
|
||||
|
||||
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
const heartbeatJSON = await bean.toJSONAsync({ decodeResponse: true });
|
||||
const monitorData = [{ id: monitor.id, active: monitor.active, name: monitor.name }];
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
}
|
||||
|
||||
// Also provide the time in server timezone
|
||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
heartbeatJSON["localDateTime"] = dayjs
|
||||
.utc(heartbeatJSON["time"])
|
||||
.tz(heartbeatJSON["timezone"])
|
||||
.format(SQL_DATETIME_FORMAT);
|
||||
|
||||
// Calculate downtime tracking information when service comes back up
|
||||
// This makes downtime information available to all notification providers
|
||||
if (bean.status === UP && monitor.id) {
|
||||
try {
|
||||
const lastDownHeartbeat = await R.getRow(
|
||||
"SELECT time FROM heartbeat WHERE monitor_id = ? AND status = ? ORDER BY time DESC LIMIT 1",
|
||||
[monitor.id, DOWN]
|
||||
);
|
||||
|
||||
if (lastDownHeartbeat && lastDownHeartbeat.time) {
|
||||
heartbeatJSON["lastDownTime"] = lastDownHeartbeat.time;
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't calculate downtime, just continue without it
|
||||
// Silently fail to avoid disrupting notification sending
|
||||
log.debug(
|
||||
"monitor",
|
||||
`[${monitor.name}] Could not calculate downtime information: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
const heartbeatJSON = await bean.toJSONAsync({ decodeResponse: true });
|
||||
const monitorData = [{ id: monitor.id, active: monitor.active, name: monitor.name }];
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
}
|
||||
|
||||
// Also provide the time in server timezone
|
||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
heartbeatJSON["localDateTime"] = dayjs
|
||||
.utc(heartbeatJSON["time"])
|
||||
.tz(heartbeatJSON["timezone"])
|
||||
.format(SQL_DATETIME_FORMAT);
|
||||
|
||||
await Notification.send(
|
||||
JSON.parse(notification.config),
|
||||
msg,
|
||||
|
||||
@ -56,6 +56,8 @@ class Discord extends NotificationProvider {
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
let addess = this.extractAddress(monitorJSON);
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
const wentOfflineTimestamp = Math.floor(new Date(heartbeatJSON["time"]).getTime() / 1000);
|
||||
|
||||
let discorddowndata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [
|
||||
@ -76,6 +78,11 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "Went Offline",
|
||||
// F for full date/time
|
||||
value: `<t:${wentOfflineTimestamp}:F>`,
|
||||
},
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
@ -104,6 +111,14 @@ class Discord extends NotificationProvider {
|
||||
await axios.post(webhookUrl.toString(), discorddowndata, config);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
const backOnlineTimestamp = Math.floor(new Date(heartbeatJSON["time"]).getTime() / 1000);
|
||||
let downtimeDuration = null;
|
||||
let wentOfflineTimestamp = null;
|
||||
if (heartbeatJSON["lastDownTime"]) {
|
||||
wentOfflineTimestamp = Math.floor(new Date(heartbeatJSON["lastDownTime"]).getTime() / 1000);
|
||||
downtimeDuration = this.formatDuration(backOnlineTimestamp - wentOfflineTimestamp);
|
||||
}
|
||||
|
||||
let discordupdata = {
|
||||
username: discordDisplayName,
|
||||
embeds: [
|
||||
@ -124,10 +139,23 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
...(wentOfflineTimestamp
|
||||
? [
|
||||
{
|
||||
name: "Went Offline",
|
||||
// F for full date/time
|
||||
value: `<t:${wentOfflineTimestamp}:F>`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(downtimeDuration
|
||||
? [
|
||||
{
|
||||
name: "Downtime Duration",
|
||||
value: downtimeDuration,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(heartbeatJSON["ping"] != null
|
||||
? [
|
||||
{
|
||||
@ -162,6 +190,32 @@ class Discord extends NotificationProvider {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as human-readable string (e.g., "1h 23m", "45m 30s")
|
||||
* TODO: Update below to `Intl.DurationFormat("en", { style: "short" }).format(duration)` once we are on a newer node version
|
||||
* @param {number} timeInSeconds The time in seconds to format a duration for
|
||||
* @returns {string} The formatted duration
|
||||
*/
|
||||
formatDuration(timeInSeconds) {
|
||||
const hours = Math.floor(timeInSeconds / 3600);
|
||||
const minutes = Math.floor((timeInSeconds % 3600) / 60);
|
||||
const seconds = timeInSeconds % 60;
|
||||
|
||||
const durationParts = [];
|
||||
if (hours > 0) {
|
||||
durationParts.push(`${hours}h`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
durationParts.push(`${minutes}m`);
|
||||
}
|
||||
if (seconds > 0 && hours === 0) {
|
||||
// Only show seconds if less than an hour
|
||||
durationParts.push(`${seconds}s`);
|
||||
}
|
||||
|
||||
return durationParts.length > 0 ? durationParts.join(" ") : "0s";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Discord;
|
||||
|
||||
@ -102,6 +102,7 @@ class SetupDatabase {
|
||||
dbConfig.dbName = process.env.UPTIME_KUMA_DB_NAME;
|
||||
dbConfig.username = getEnvOrFile("UPTIME_KUMA_DB_USERNAME");
|
||||
dbConfig.password = getEnvOrFile("UPTIME_KUMA_DB_PASSWORD");
|
||||
dbConfig.socketPath = process.env.UPTIME_KUMA_DB_SOCKET?.trim();
|
||||
dbConfig.ssl = getEnvOrFile("UPTIME_KUMA_DB_SSL")?.toLowerCase() === "true";
|
||||
dbConfig.ca = getEnvOrFile("UPTIME_KUMA_DB_CA");
|
||||
Database.writeDBConfig(dbConfig);
|
||||
@ -160,6 +161,7 @@ class SetupDatabase {
|
||||
runningSetup: this.runningSetup,
|
||||
needSetup: this.needSetup,
|
||||
isEnabledEmbeddedMariaDB: this.isEnabledEmbeddedMariaDB(),
|
||||
isEnabledMariaDBSocket: process.env.UPTIME_KUMA_DB_SOCKET?.trim().length > 0,
|
||||
});
|
||||
});
|
||||
|
||||
@ -202,16 +204,22 @@ class SetupDatabase {
|
||||
|
||||
// External MariaDB
|
||||
if (dbConfig.type === "mariadb") {
|
||||
if (!dbConfig.hostname) {
|
||||
response.status(400).json("Hostname is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
// If socketPath is provided and not empty, validate it
|
||||
if (process.env.UPTIME_KUMA_DB_SOCKET?.trim().length > 0) {
|
||||
dbConfig.socketPath = process.env.UPTIME_KUMA_DB_SOCKET.trim();
|
||||
} else {
|
||||
// socketPath not provided, hostname and port are required
|
||||
if (!dbConfig.hostname) {
|
||||
response.status(400).json("Hostname is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbConfig.port) {
|
||||
response.status(400).json("Port is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
if (!dbConfig.port) {
|
||||
response.status(400).json("Port is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dbConfig.dbName) {
|
||||
@ -241,6 +249,7 @@ class SetupDatabase {
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.dbName,
|
||||
socketPath: dbConfig.socketPath,
|
||||
...(dbConfig.ssl
|
||||
? {
|
||||
ssl: {
|
||||
|
||||
@ -1371,6 +1371,7 @@
|
||||
"None (Successful Connection)": "None (Successful Connection)",
|
||||
"expectedTlsAlertDescription": "Select the TLS alert you expect the server to return. Use {code} to verify mTLS endpoints reject connections without client certificates. See {link} for details.",
|
||||
"TLS Alert Spec": "RFC 8446",
|
||||
"mariadbSocketPathDetectedHelptext": "Connecting to the database as specified via the {0} environment variable.",
|
||||
"Expand All Groups": "Expand All Groups",
|
||||
"Collapse All Groups": "Collapse All Groups"
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
</div>
|
||||
|
||||
<template v-if="dbConfig.type === 'mariadb'">
|
||||
<div class="form-floating mt-3 short">
|
||||
<div v-if="!isProvidedMariaDBSocket" class="form-floating mt-3 short">
|
||||
<input
|
||||
id="floatingInput"
|
||||
v-model="dbConfig.hostname"
|
||||
@ -90,11 +90,19 @@
|
||||
<label for="floatingInput">{{ $t("Hostname") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3 short">
|
||||
<div v-if="!isProvidedMariaDBSocket" class="form-floating mt-3 short">
|
||||
<input id="floatingInput" v-model="dbConfig.port" type="text" class="form-control" required />
|
||||
<label for="floatingInput">{{ $t("Port") }}</label>
|
||||
</div>
|
||||
|
||||
<div v-if="isProvidedMariaDBSocket" class="mt-1 short text-start">
|
||||
<i18n-t keypath="mariadbSocketPathDetectedHelptext" tag="div" class="form-text">
|
||||
<code>UPTIME_KUMA_DB_SOCKET</code>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<hr v-if="isProvidedMariaDBSocket" class="mt-3 mb-2 short" />
|
||||
|
||||
<div class="form-floating mt-3 short">
|
||||
<input
|
||||
id="floatingInput"
|
||||
@ -198,6 +206,9 @@ export default {
|
||||
disabledButton() {
|
||||
return this.dbConfig.type === undefined || this.info.runningSetup;
|
||||
},
|
||||
isProvidedMariaDBSocket() {
|
||||
return this.info.isEnabledMariaDBSocket;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
let res = await axios.get("/setup-database-info");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user