mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-20 02:35:48 +08:00
Compare commits
30 Commits
async-fs
...
custom-hea
Author | SHA1 | Date | |
---|---|---|---|
|
4b0e7de3e4 | ||
|
2a556e714f | ||
|
7f0d3a3043 | ||
|
10ebdcacaa | ||
|
a00b4ed285 | ||
|
a26d93ac52 | ||
|
c919d2c990 | ||
|
40d24e7837 | ||
|
30bf7a5e23 | ||
|
4e63d00007 | ||
|
9f35290c68 | ||
|
11007823e7 | ||
|
9ce0911e34 | ||
|
df663a81cd | ||
|
8b648a3ed1 | ||
|
9c7aa13190 | ||
|
823aeda9fe | ||
|
1590ca6c02 | ||
|
e670b5f4cf | ||
|
b2ede38269 | ||
|
1ac5696463 | ||
|
63b8c52a65 | ||
|
17dfdacd93 | ||
|
94154c49aa | ||
|
4d6ea433e6 | ||
|
81e465f418 | ||
|
a5d0f7a7db | ||
|
a16b42f98c | ||
|
65ff08b38e | ||
|
f89ed0a3a4 |
2
.github/workflows/auto-test.yml
vendored
2
.github/workflows/auto-test.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
needs: [ check-linters ]
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
if: ${{ github.repository == 'louislam/uptime-kuma' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ARMv7 ]
|
os: [ ARMv7 ]
|
||||||
|
@@ -427,7 +427,33 @@ Currently, there are 3 maintainers:
|
|||||||
### Procedures
|
### Procedures
|
||||||
|
|
||||||
We have a few procedures we follow. These are documented here:
|
We have a few procedures we follow. These are documented here:
|
||||||
|
- <details><summary>Set up a Docker Builder</summary>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
- amd64, armv7 using local.
|
||||||
|
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer pass the `npm ci` command.
|
||||||
|
1. Add the public key to the remote server.
|
||||||
|
2. Add the remote context. The remote machine must be arm64 and installed Docker CE.
|
||||||
|
```
|
||||||
|
docker context create oracle-arm64-jp --docker "host=ssh://root@100.107.174.88"
|
||||||
|
```
|
||||||
|
3. Create a new builder.
|
||||||
|
```
|
||||||
|
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
|
||||||
|
docker buildx use kuma-builder
|
||||||
|
docker buildx inspect --bootstrap
|
||||||
|
```
|
||||||
|
4. Append the remote context to the builder.
|
||||||
|
```
|
||||||
|
docker buildx create --append --name kuma-builder --platform linux/arm64 oracle-arm64-jp
|
||||||
|
```
|
||||||
|
5. Verify the builder and check if the builder is using `kuma-builder`.
|
||||||
|
```
|
||||||
|
docker buildx inspect kuma-builder
|
||||||
|
docker buildx ls
|
||||||
|
```
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
- <details><summary>Release</summary>
|
- <details><summary>Release</summary>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
@@ -490,28 +516,3 @@ We have a few procedures we follow. These are documented here:
|
|||||||
|
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Set up a Docker Builder
|
|
||||||
|
|
||||||
- amd64, armv7 using local.
|
|
||||||
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer pass the `npm ci` command.
|
|
||||||
1. Add the public key to the remote server.
|
|
||||||
2. Add the remote context. The remote machine must be arm64 and installed Docker CE.
|
|
||||||
```
|
|
||||||
docker context create oracle-arm64-jp --docker "host=ssh://root@100.107.174.88"
|
|
||||||
```
|
|
||||||
3. Create a new builder.
|
|
||||||
```
|
|
||||||
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
|
|
||||||
docker buildx use kuma-builder
|
|
||||||
docker buildx inspect --bootstrap
|
|
||||||
```
|
|
||||||
4. Append the remote context to the builder.
|
|
||||||
```
|
|
||||||
docker buildx create --append --name kuma-builder --platform linux/arm64 oracle-arm64-jp
|
|
||||||
```
|
|
||||||
5. Verify the builder and check if the builder is using `kuma-builder`.
|
|
||||||
```
|
|
||||||
docker buildx inspect kuma-builder
|
|
||||||
docker buildx ls
|
|
||||||
```
|
|
||||||
|
14
db/knex_migrations/2023-10-13-1200-add-custom-html.js
Normal file
14
db/knex_migrations/2023-10-13-1200-add-custom-html.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
// Insert column for custom HTML code
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("status_page", function (table) {
|
||||||
|
table.text("custom_html").nullable().defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("status_page", function (table) {
|
||||||
|
table.dropColumn("custom_html");
|
||||||
|
});
|
||||||
|
};
|
2
package-lock.json
generated
2
package-lock.json
generated
@@ -74,7 +74,7 @@
|
|||||||
"socket.io": "~4.6.1",
|
"socket.io": "~4.6.1",
|
||||||
"socket.io-client": "~4.6.1",
|
"socket.io-client": "~4.6.1",
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "~5.1.7",
|
||||||
"tar": "~6.2.1",
|
"tar": "~6.2.1",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2",
|
||||||
|
@@ -897,7 +897,6 @@ class Monitor extends BeanModel {
|
|||||||
retries = 0;
|
retries = 0;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
if (error?.name === "CanceledError") {
|
if (error?.name === "CanceledError") {
|
||||||
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||||
} else {
|
} else {
|
||||||
@@ -970,6 +969,7 @@ class Monitor extends BeanModel {
|
|||||||
} else if (bean.status === MAINTENANCE) {
|
} else if (bean.status === MAINTENANCE) {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
|
beatInterval = this.retryInterval;
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,6 +66,10 @@ class StatusPage extends BeanModel {
|
|||||||
head.append($(escapedGoogleAnalyticsScript));
|
head.append($(escapedGoogleAnalyticsScript));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1") {
|
||||||
|
head.append(statusPage.customHtml);
|
||||||
|
}
|
||||||
|
|
||||||
// OG Meta Tags
|
// OG Meta Tags
|
||||||
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
|
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
|
||||||
head.append(ogTitle);
|
head.append(ogTitle);
|
||||||
@@ -247,6 +251,7 @@ class StatusPage extends BeanModel {
|
|||||||
showPoweredBy: !!this.show_powered_by,
|
showPoweredBy: !!this.show_powered_by,
|
||||||
googleAnalyticsId: this.google_analytics_tag_id,
|
googleAnalyticsId: this.google_analytics_tag_id,
|
||||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||||
|
customHtml: this.custom_html
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +275,7 @@ class StatusPage extends BeanModel {
|
|||||||
showPoweredBy: !!this.show_powered_by,
|
showPoweredBy: !!this.show_powered_by,
|
||||||
googleAnalyticsId: this.google_analytics_tag_id,
|
googleAnalyticsId: this.google_analytics_tag_id,
|
||||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||||
|
customHtml: this.custom_html
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,25 +25,29 @@ class Feishu extends NotificationProvider {
|
|||||||
|
|
||||||
if (heartbeatJSON["status"] === DOWN) {
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
let downdata = {
|
let downdata = {
|
||||||
msg_type: "post",
|
msg_type: "interactive",
|
||||||
content: {
|
card: {
|
||||||
post: {
|
config: {
|
||||||
zh_cn: {
|
update_multi: false,
|
||||||
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
wide_screen_mode: true,
|
||||||
content: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
tag: "text",
|
|
||||||
text:
|
|
||||||
"[Down] " +
|
|
||||||
heartbeatJSON["msg"] +
|
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||||
|
},
|
||||||
|
template: "red",
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
text: {
|
||||||
|
tag: "lark_md",
|
||||||
|
content: getContent(heartbeatJSON),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
await axios.post(notification.feishuWebHookUrl, downdata);
|
await axios.post(notification.feishuWebHookUrl, downdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
@@ -51,25 +55,29 @@ class Feishu extends NotificationProvider {
|
|||||||
|
|
||||||
if (heartbeatJSON["status"] === UP) {
|
if (heartbeatJSON["status"] === UP) {
|
||||||
let updata = {
|
let updata = {
|
||||||
msg_type: "post",
|
msg_type: "interactive",
|
||||||
content: {
|
card: {
|
||||||
post: {
|
config: {
|
||||||
zh_cn: {
|
update_multi: false,
|
||||||
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
|
wide_screen_mode: true,
|
||||||
content: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
tag: "text",
|
|
||||||
text:
|
|
||||||
"[Up] " +
|
|
||||||
heartbeatJSON["msg"] +
|
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "UptimeKuma Alert: [UP] " + monitorJSON["name"],
|
||||||
|
},
|
||||||
|
template: "green",
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
text: {
|
||||||
|
tag: "lark_md",
|
||||||
|
content: getContent(heartbeatJSON),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
await axios.post(notification.feishuWebHookUrl, updata);
|
await axios.post(notification.feishuWebHookUrl, updata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
@@ -80,4 +88,17 @@ class Feishu extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content
|
||||||
|
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||||
|
* @returns {string} Return Successful Message
|
||||||
|
*/
|
||||||
|
function getContent(heartbeatJSON) {
|
||||||
|
return [
|
||||||
|
"**Message**: " + heartbeatJSON["msg"],
|
||||||
|
"**Ping**: " + (heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms"),
|
||||||
|
`**Time (${heartbeatJSON["timezone"]})**: ${heartbeatJSON["localDateTime"]}`
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = Feishu;
|
module.exports = Feishu;
|
||||||
|
@@ -44,9 +44,8 @@ router.get("/api/entry-page", async (request, response) => {
|
|||||||
response.json(result);
|
response.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/api/push/:pushToken", async (request, response) => {
|
router.all("/api/push/:pushToken", async (request, response) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = parseFloat(request.query.ping) || null;
|
let ping = parseFloat(request.query.ping) || null;
|
||||||
|
@@ -101,10 +101,11 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
if (!statusPage) {
|
if (!statusPage) {
|
||||||
throw new Error("No slug?");
|
throw new Error("No slug?");
|
||||||
}
|
}
|
||||||
|
const config = await statusPage.toJSON();
|
||||||
|
config.allowEditingCustomHtml = import.meta.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1";
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
config: await statusPage.toJSON(),
|
config,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
@@ -167,6 +168,9 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
||||||
statusPage.modified_date = R.isoDateTime();
|
statusPage.modified_date = R.isoDateTime();
|
||||||
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
||||||
|
if (process.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1") {
|
||||||
|
statusPage.custom_html = config.customHtml;
|
||||||
|
}
|
||||||
|
|
||||||
await R.store(statusPage);
|
await R.store(statusPage);
|
||||||
|
|
||||||
|
@@ -776,6 +776,9 @@
|
|||||||
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
"wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .",
|
||||||
"Custom Monitor Type": "Custom Monitor Type",
|
"Custom Monitor Type": "Custom Monitor Type",
|
||||||
"Google Analytics ID": "Google Analytics ID",
|
"Google Analytics ID": "Google Analytics ID",
|
||||||
|
"Custom HTML": "Custom HTML",
|
||||||
|
"customHtmlEnvVarDisabled": "environment variable {allow_custom_html} must be set to inject html to the head",
|
||||||
|
"customHtmlEnvVarEnabled": "Because the environment variable {allow_custom_html} is set, arbitrary html can be injected into the head. Make sure to remove the environment variable after use",
|
||||||
"Edit Tag": "Edit Tag",
|
"Edit Tag": "Edit Tag",
|
||||||
"Server Address": "Server Address",
|
"Server Address": "Server Address",
|
||||||
"Learn More": "Learn More",
|
"Learn More": "Learn More",
|
||||||
|
@@ -211,6 +211,20 @@ export default {
|
|||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
|
&:hover {
|
||||||
|
background-color: $primary;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: $primary;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.status-page {
|
&.status-page {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
@@ -370,9 +370,10 @@
|
|||||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<i18n-t tag="div" class="form-text" keypath="jsonQueryDescription">
|
||||||
<div class="form-text" v-html="$t('jsonQueryDescription')">
|
<a href="https://jsonata.org/">jsonata.org</a>
|
||||||
</div>
|
<a href="https://try.jsonata.org/">{{ $t('here') }}</a>
|
||||||
|
</i18n-t>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||||
|
@@ -104,6 +104,22 @@
|
|||||||
<prism-editor v-model="config.customCSS" class="css-editor" :highlight="highlighter" line-numbers></prism-editor>
|
<prism-editor v-model="config.customCSS" class="css-editor" :highlight="highlighter" line-numbers></prism-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom HTML -->
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="mb-1">{{ $t("Custom HTML") }}</div>
|
||||||
|
<prism-editor v-model="config.customHtml" class="css-editor" :highlight="highlighter" line-numbers :readonly="!config.allowEditingCustomHtml"></prism-editor>
|
||||||
|
<i18n-t v-if="config.allowEditingCustomHtml" tag="div" class="form-text" keypath="customHtmlEnvVarEnabled">
|
||||||
|
<template #allow_custom_html>
|
||||||
|
<code>UPTIME_KUMA_ALLOW_CUSTOM_HTML</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<i18n-t v-else tag="div" class="form-text" keypath="customHtmlEnvVarDisabled">
|
||||||
|
<template #allow_custom_html>
|
||||||
|
<code>UPTIME_KUMA_ALLOW_CUSTOM_HTML=1</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="danger-zone">
|
<div class="danger-zone">
|
||||||
<button class="btn btn-danger me-2" @click="deleteDialog">
|
<button class="btn btn-danger me-2" @click="deleteDialog">
|
||||||
<font-awesome-icon icon="trash" />
|
<font-awesome-icon icon="trash" />
|
||||||
|
Reference in New Issue
Block a user