Merge branch 'master' into copilot/update-float-fields-precision

This commit is contained in:
copilot-swe-agent[bot] 2026-01-11 12:41:08 +00:00
commit 5e4eec3f10
11 changed files with 263 additions and 18 deletions

View File

@ -1,36 +1,31 @@
<sub> To keep reviews fast and effective, please make sure youve [read our pull request guidelines](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)</sub>
# Summary
## 📝 Summary of changes done and why they are done
In this pull request, the following changes are made:
<!-- Provide a clear summary of the purpose and scope of this pull request-->
## 📋 Related issues
- Foobar was changed to FooFoo, because ...
<!--Please link any GitHub issues or tasks that this pull request addresses-->
- Relates to #issue-number <!--this links related the issue-->
- Resolves #issue-number <!--this auto-closes the issue-->
## 📄 Checklist
<details>
<summary>Please follow this checklist to avoid unnecessary back and forth (click to expand)</summary>
- [ ] ⚠️ If there are Breaking change (a fix or feature that alters existing functionality in a way that could cause issues) I have called them out
- [ ] 🧠 I have disclosed any use of LLMs/AI in this contribution and reviewed all generated content.
I understand that I am responsible for and able to explain every line of code I submit.
- [ ] 🔍 My code adheres to the style guidelines of this project.
- [ ] ⚠️ My changes generate no new warnings.
- [ ] 🛠️ I have reviewed and tested my code.
- [ ] 🔍 Any UI changes adhere to visual style of this project.
- [ ] 🛠️ I have self-reviewed and self-tested my code to ensure it works as expected.
- [ ] 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
- [ ] 🤖 I added or updated automated tests where appropriate.
- [ ] 📄 Documentation updates are included (if applicable).
- [ ] 🔒 I have considered potential security impacts and mitigated risks.
- [ ] 🧰 Dependency updates are listed and explained.
- [ ] ⚠️ CI passes and is green.
</details>
## 📷 Screenshots or Visual Changes
## Screenshots for Visual Changes
<!--
If this pull request introduces visual changes, please provide the following details.

View File

@ -0,0 +1,38 @@
name: New contributor message
on:
# Safety
# This workflow uses pull_request_target so it can run with write permissions on first-time contributor PRs.
# It is safe because it does not check out or execute any code from the pull request and
# only uses the pinned, trusted actions/first-interaction action
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened]
branches:
- master
permissions:
pull-requests: write
jobs:
build:
if: github.repository == 'louislam/uptime-kuma'
name: Hello new contributor
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |
Hello! Thank you for your contribution 👋
As this is your first contribution, please be sure to check out our
[Pull Request guidelines](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma).
In particular:
- Mark your PR as Draft while youre still making changes
- Mark it as Ready for review once its fully ready 🟢
If you have any design or process questions, feel free to ask them right here in this pull request - unclear documentation is a bug too.
Thanks for lending a paw to Uptime Kuma 🐻

View File

@ -175,8 +175,8 @@ You can mention me if you ask a question on the subreddit.
### Create Pull Requests
We DO NOT accept all types of pull requests and do not want to waste your time. Please be sure that you have read and follow pull request rules:
[CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)
Pull requests are awesome.
To keep reviews fast and effective, please make sure youve [read our pull request guidelines](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma).
### Test Pull Requests

View File

@ -284,6 +284,14 @@ class Database {
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
...(dbConfig.ssl
? {
ssl: {
rejectUnauthorized: true,
...(dbConfig.ca && dbConfig.ca.trim() !== "" ? { ca: [dbConfig.ca] } : {}),
},
}
: {}),
});
// Set to true, so for example "uptime.kuma", becomes `uptime.kuma`, not `uptime`.`kuma`
@ -309,6 +317,14 @@ class Database {
}
return next();
},
...(dbConfig.ssl
? {
ssl: {
rejectUnauthorized: true,
...(dbConfig.ca && dbConfig.ca.trim() !== "" ? { ca: [dbConfig.ca] } : {}),
},
}
: {}),
},
pool: mariadbPoolConfig,
};

View File

@ -108,14 +108,49 @@ class NotificationProvider {
* @throws {any} The error specified
*/
throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
let msg = error && error.message ? error.message : String(error);
if (error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data);
if (error && error.code) {
msg += ` (code=${error.code})`;
}
if (error && error.response && error.response.status) {
msg += ` (HTTP ${error.response.status}${error.response.statusText ? " " + error.response.statusText : ""})`;
}
if (error && error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += " " + error.response.data;
} else {
try {
msg += " " + JSON.stringify(error.response.data);
} catch (e) {
msg += " " + String(error.response.data);
}
}
}
// Expand AggregateError to show underlying causes
let agg = null;
if (error && error.name === "AggregateError" && Array.isArray(error.errors)) {
agg = error;
} else if (error && error.cause && error.cause.name === "AggregateError" && Array.isArray(error.cause.errors)) {
agg = error.cause;
}
if (agg) {
let causes = agg.errors
.map((e) => {
let m = e && e.message ? e.message : String(e);
if (e && e.code) {
m += ` (code=${e.code})`;
}
return m;
})
.join("; ");
msg += " - caused by: " + causes;
} else if (error && error.cause && error.cause.message) {
msg += " - cause: " + error.cause.message;
}
throw new Error(msg);

View File

@ -102,6 +102,8 @@ 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.ssl = getEnvOrFile("UPTIME_KUMA_DB_SSL")?.toLowerCase() === "true";
dbConfig.ca = getEnvOrFile("UPTIME_KUMA_DB_CA");
Database.writeDBConfig(dbConfig);
}
}
@ -239,6 +241,14 @@ class SetupDatabase {
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.dbName,
...(dbConfig.ssl
? {
ssl: {
rejectUnauthorized: true,
...(dbConfig.ca && dbConfig.ca.trim() !== "" ? { ca: [dbConfig.ca] } : {}),
},
}
: {}),
});
await connection.execute("SELECT 1");
connection.end();

View File

@ -157,6 +157,16 @@ exports.pingAsync = function (
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT
) {
try {
const url = new URL(`http://${destAddr}`);
destAddr = url.hostname;
if (destAddr.startsWith("[") && destAddr.endsWith("]")) {
destAddr = destAddr.slice(1, -1);
}
} catch (e) {
// ignore
}
return new Promise((resolve, reject) => {
ping.promise
.probe(destAddr, {

View File

@ -6,6 +6,10 @@
"setupDatabaseSQLite": "A simple database file, recommended for small-scale deployments. Prior to v2.0.0, Uptime Kuma used SQLite as the default database.",
"settingUpDatabaseMSG": "Setting up the database. It may take a while, please be patient.",
"dbName": "Database Name",
"enableSSL": "Enable SSL/TLS",
"mariadbUseSSLHelptext": "Enable to use a encrypted connection to your database. Required for most cloud databases.",
"mariadbCaCertificateLabel": "CA Certificate",
"mariadbCaCertificateHelptext": "Paste the CA Cert in PEM format to use with self-signed certificates. Leave blank if your database uses a certificate signed by a public CA.",
"Settings": "Settings",
"Dashboard": "Dashboard",
"Help": "Help",

View File

@ -121,6 +121,42 @@
<input id="floatingInput" v-model="dbConfig.dbName" type="text" class="form-control" required />
<label for="floatingInput">{{ $t("dbName") }}</label>
</div>
<div class="mt-3 short text-start">
<div class="form-check form-switch ps-0" style="height: auto; display: block; padding: 0">
<div class="d-flex align-items-center">
<input
id="sslCheck"
v-model="dbConfig.ssl"
type="checkbox"
role="switch"
class="form-check-input ms-0 me-2"
style="float: none"
/>
<label class="form-check-label fw-bold" for="sslCheck">
{{ $t("enableSSL") }}
<span class="fw-normal text-muted" style="font-size: 0.9em">
({{ $t("Optional") }})
</span>
</label>
</div>
<div class="form-text mt-1">
{{ $t("mariadbUseSSLHelptext") }}
</div>
</div>
</div>
<div v-if="dbConfig.ssl" class="form-floating mt-3 short">
<textarea
id="caInput"
v-model="dbConfig.ca"
class="form-control"
placeholder="-----BEGIN CERTIFICATE-----"
style="height: 120px"
></textarea>
<label for="caInput">{{ $t("mariadbCaCertificateLabel") }}</label>
<div class="form-text">{{ $t("mariadbCaCertificateHelptext") }}</div>
</div>
</template>
<button class="btn btn-primary mt-4 short" type="submit" :disabled="disabledButton">
@ -148,6 +184,8 @@ export default {
username: "",
password: "",
dbName: "kuma",
ssl: false,
ca: "",
},
info: {
needSetup: false,
@ -253,6 +291,14 @@ export default {
}
}
.form-check {
height: calc(3.5rem + 2px);
padding: 0;
display: flex;
align-items: center;
justify-content: space-around;
}
.short {
width: 300px;
}

View File

@ -0,0 +1,34 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const NotificationProvider = require("../../../server/notification-providers/notification-provider");
describe("NotificationProvider.throwGeneralAxiosError()", () => {
const provider = new NotificationProvider();
test("expands AggregateError causes", () => {
let err1 = new Error("connect ECONNREFUSED 127.0.0.1:443");
err1.code = "ECONNREFUSED";
let err2 = new Error("connect ECONNREFUSED ::1:443");
err2.code = "ECONNREFUSED";
let aggErr = new AggregateError([err1, err2], "AggregateError");
assert.throws(() => provider.throwGeneralAxiosError(aggErr), {
message: /^AggregateError - caused by: .+/,
});
});
test("expands AggregateError wrapped in error.cause", () => {
let innerErr = new Error("connect ETIMEDOUT 10.0.0.1:443");
innerErr.code = "ETIMEDOUT";
let aggErr = new AggregateError([innerErr], "AggregateError");
let outerErr = new Error("Request failed");
outerErr.cause = aggErr;
assert.throws(() => provider.throwGeneralAxiosError(outerErr), {
message: /^Request failed - caused by: .+/,
});
});
});

View File

@ -0,0 +1,57 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { pingAsync } = require("../../server/util-server");
describe("Server Utilities: pingAsync", () => {
test("should convert IDN domains to Punycode before pinging", async () => {
const idnDomain = "münchen.de";
const punycodeDomain = "xn--mnchen-3ya.de";
await assert.rejects(pingAsync(idnDomain, false, 1, "", true, 56, 1, 1), (err) => {
if (err.message.includes("Parameter string not correctly encoded")) {
assert.fail("Ping failed with encoding error: IDN was not converted");
}
assert.ok(
err.message.includes(punycodeDomain),
`Error message should contain the Punycode domain "${punycodeDomain}". Got: ${err.message}`
);
return true;
});
});
test("should strip brackets from IPv6 addresses before pinging", async () => {
const ipv6WithBrackets = "[2606:4700:4700::1111]";
const ipv6Raw = "2606:4700:4700::1111";
await assert.rejects(pingAsync(ipv6WithBrackets, true, 1, "", true, 56, 1, 1), (err) => {
assert.strictEqual(
err.message.includes(ipv6WithBrackets),
false,
"Error message should not contain brackets"
);
// Allow either the IP in the message (local) OR "Network is unreachable"
const containsIP = err.message.includes(ipv6Raw);
const isUnreachable =
err.message.includes("Network is unreachable") || err.message.includes("Network unreachable");
// macOS error when IPv6 stack is missing
const isMacOSError = err.message.includes("nodename nor servname provided");
assert.ok(
containsIP || isUnreachable || isMacOSError,
`Ping failed correctly, but error message format was unexpected.\nGot: "${err.message}"\nExpected to contain IP "${ipv6Raw}" OR be a standard network error.`
);
return true;
});
});
test("should handle standard ASCII domains correctly", async () => {
const domain = "invalid-domain.test";
await assert.rejects(pingAsync(domain, false, 1, "", true, 56, 1, 1), (err) => {
assert.strictEqual(err.message.includes("Parameter string not correctly encoded"), false);
assert.ok(
err.message.includes(domain),
`Error message should contain the domain "${domain}". Got: ${err.message}`
);
return true;
});
});
});