1
0
mirror of https://github.com/louislam/uptime-kuma.git synced 2024-12-20 20:11:39 +02:00

Merge branch 'master' into hide-uptime-percentage

This commit is contained in:
broodroosterdev 2024-04-12 15:37:32 +02:00 committed by GitHub
commit 2192bfa42b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 2360 additions and 1680 deletions

View File

@ -79,7 +79,7 @@ body:
label: "🖥️ Deployment Environment"
description: |
examples:
- **Runtime**: Docker 20.10.9 / nodejs 14.18.0 / K8S via ... v1.3.3 / ..
- **Runtime**: Docker 20.10.9 / nodejs 18.17.1 / K8S via ... v1.3.3 / ..
- **Database**: sqlite/embedded mariadb/external mariadb
- **Filesystem used to store the database on**: Windows/ZFS/btrfs/NFSv3 on a SSD/HDD/eMMC
- **number of monitors**: 42

View File

@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 16, 20.5 ]
node: [ 18, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@ -33,13 +33,9 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install
run: npm install
- name: Build
run: npm run build
- name: Test Backend
run: npm run test-backend
- run: npm install
- run: npm run build
- run: npm run test-backend
env:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
@ -53,7 +49,7 @@ jobs:
strategy:
matrix:
os: [ ARMv7 ]
node: [ 14, 20 ]
node: [ 18, 20 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:

View File

@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node-version: [16]
node-version: [18]
steps:
- uses: actions/checkout@v4

View File

@ -230,8 +230,8 @@ The goal is to make the Uptime Kuma installation as easy as installing a mobile
## Tools
- [`Node.js`](https://nodejs.org/) >= 14
- [`npm`](https://www.npmjs.com/) >= 8.5
- [`Node.js`](https://nodejs.org/) >= 18
- [`npm`](https://www.npmjs.com/) >= 9.3
- [`git`](https://git-scm.com/)
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))

View File

@ -56,15 +56,12 @@ Requirements:
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ Replit / Heroku
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
- [Node.js](https://nodejs.org/en/download/) 18 / 20.4
- [npm](https://docs.npmjs.com/cli/) 9
- [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
```bash
# Update your npm
npm install npm@9 -g
git clone https://github.com/louislam/uptime-kuma.git
cd uptime-kuma
npm run setup

View File

@ -318,7 +318,10 @@ async function createTables() {
// monitor_tls_info
await knex.schema.createTable("monitor_tls_info", (table) => {
table.increments("id");
table.integer("monitor_id").unsigned().notNullable(); //TODO: no fk ?
table.integer("monitor_id").unsigned().notNullable()
.references("id").inTable("monitor")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.text("info_json");
});

View File

@ -0,0 +1,26 @@
exports.up = function (knex) {
return knex.schema
.alterTable("stat_daily", function (table) {
table.text("extras").defaultTo(null).comment("Extra statistics during this time period");
})
.alterTable("stat_minutely", function (table) {
table.text("extras").defaultTo(null).comment("Extra statistics during this time period");
})
.alterTable("stat_hourly", function (table) {
table.text("extras").defaultTo(null).comment("Extra statistics during this time period");
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("stat_daily", function (table) {
table.dropColumn("extras");
})
.alterTable("stat_minutely", function (table) {
table.dropColumn("extras");
})
.alterTable("stat_hourly", function (table) {
table.dropColumn("extras");
});
};

View File

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
PRAGMA writable_schema = TRUE;
UPDATE
SQLITE_MASTER
SET
sql = replace(sql,
'monitor_id INTEGER NOT NULL',
'monitor_id INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'
)
WHERE
name = 'monitor_tls_info'
AND type = 'table';
PRAGMA writable_schema = RESET;
COMMIT;

3393
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": "14 || 16 || 18 || >= 20.4.0"
"node": "18 || >= 20.4.0"
},
"scripts": {
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
@ -93,7 +93,7 @@
"croner": "~6.0.5",
"dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"express": "~4.17.3",
"express": "~4.19.2",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
@ -121,7 +121,7 @@
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nodemailer": "~6.9.13",
"nostr-tools": "^1.13.1",
"notp": "~2.0.3",
"openid-client": "^5.4.2",

View File

@ -8,6 +8,7 @@ const server = UptimeKumaServer.getInstance();
const io = server.io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
const Database = require("./database");
/**
* Send list of notification providers to client
@ -144,17 +145,20 @@ async function sendInfo(socket, hideVersion = false) {
let version;
let latestVersion;
let isContainer;
let dbType;
if (!hideVersion) {
version = checkVersion.version;
latestVersion = checkVersion.latestVersion;
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
dbType = Database.dbConfig.type;
}
socket.emit("info", {
version,
latestVersion,
isContainer,
dbType,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),

View File

@ -105,7 +105,8 @@ class Database {
"patch-add-gamedig-given-port.sql": true,
"patch-notification-config.sql": true,
"patch-fix-kafka-producer-booleans.sql": true,
"patch-timeout.sql": true, // The last file so far converted to a knex migration file
"patch-timeout.sql": true,
"patch-monitor-tls-info-add-fk.sql": true, // The last file so far converted to a knex migration file
};
/**

View File

@ -0,0 +1,23 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class CallMeBot extends NotificationProvider {
name = "CallMeBot";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const url = new URL(notification.callMeBotEndpoint);
url.searchParams.set("text", msg);
await axios.get(url.toString());
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = CallMeBot;

View File

@ -0,0 +1,39 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Cellsynt extends NotificationProvider {
name = "Cellsynt";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const data = {
// docs at https://www.cellsynt.com/en/sms/api-integration
params: {
"username": notification.cellsyntLogin,
"password": notification.cellsyntPassword,
"destination": notification.cellsyntDestination,
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"originatortype": notification.cellsyntOriginatortype,
"originator": notification.cellsyntOriginator,
"allowconcat": notification.cellsyntAllowLongSMS ? 6 : 1
}
};
try {
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data);
if (resp.data == null ) {
throw new Error("Could not connect to Cellsynt, please try again.");
} else if (resp.data.includes("Error:")) {
resp.data = resp.data.replaceAll("Error:", "");
throw new Error(resp.data);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Cellsynt;

View File

@ -0,0 +1,33 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class GtxMessaging extends NotificationProvider {
name = "gtxmessaging";
/**
* @inheritDoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
// The UP/DOWN symbols will be replaced with `???` by gtx-messaging
const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", "");
try {
const data = new URLSearchParams();
data.append("from", notification.gtxMessagingFrom.trim());
data.append("to", notification.gtxMessagingTo.trim());
data.append("text", text);
const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`;
await axios.post(url, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = GtxMessaging;

View File

@ -26,6 +26,93 @@ class Slack extends NotificationProvider {
}
}
/**
* Builds the actions available in the slack message
* @param {string} baseURL Uptime Kuma base URL
* @param {object} monitorJSON The monitor config
* @returns {Array} The relevant action objects
*/
static buildActions(baseURL, monitorJSON) {
const actions = [];
if (baseURL) {
actions.push({
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
});
}
if (monitorJSON.url) {
actions.push({
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit site",
},
"value": "Site",
"url": monitorJSON.url,
});
}
return actions;
}
/**
* Builds the different blocks the Slack message consists of.
* @param {string} baseURL Uptime Kuma base URL
* @param {object} monitorJSON The monitor object
* @param {object} heartbeatJSON The heartbeat object
* @param {string} title The message title
* @param {string} msg The message body
* @returns {Array<object>} The rich content blocks for the Slack message
*/
static buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
//create an array to dynamically add blocks
const blocks = [];
// the header block
blocks.push({
"type": "header",
"text": {
"type": "plain_text",
"text": title,
},
});
// the body block, containing the details
blocks.push({
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
}
],
});
const actions = this.buildActions(baseURL, monitorJSON);
if (actions.length > 0) {
//the actions block, containing buttons
blocks.push({
"type": "actions",
"elements": actions,
});
}
return blocks;
}
/**
* @inheritdoc
*/
@ -48,35 +135,18 @@ class Slack extends NotificationProvider {
return okMsg;
}
const textMsg = "Uptime Kuma Alert";
const baseURL = await setting("primaryBaseURL");
const title = "Uptime Kuma Alert";
let data = {
"text": `${textMsg}\n${msg}`,
"text": `${title}\n${msg}`,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": textMsg,
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
}],
}
],
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
]
};
@ -85,26 +155,6 @@ class Slack extends NotificationProvider {
await Slack.deprecateURL(notification.slackbutton);
}
const baseURL = await setting("primaryBaseURL");
// Button
if (baseURL) {
data.attachments.forEach(element => {
element.blocks.push({
"type": "actions",
"elements": [{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
}],
});
});
}
await axios.post(notification.slackwebhookURL, data);
return okMsg;
} catch (error) {

View File

@ -0,0 +1,39 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Whapi extends NotificationProvider {
name = "whapi";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.whapiAuthToken,
}
};
let data = {
"to": notification.whapiRecipient,
"body": msg,
};
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/\/+$/, "") + "/messages/text";
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Whapi;

View File

@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
@ -55,6 +56,9 @@ const GoAlert = require("./notification-providers/goalert");
const SMSManager = require("./notification-providers/smsmanager");
const ServerChan = require("./notification-providers/serverchan");
const ZohoCliq = require("./notification-providers/zoho-cliq");
const Whapi = require("./notification-providers/whapi");
const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
class Notification {
@ -78,6 +82,7 @@ class Notification {
new Apprise(),
new Bark(),
new ClickSendSMS(),
new CallMeBot(),
new SMSC(),
new DingDing(),
new Discord(),
@ -126,7 +131,10 @@ class Notification {
new Webhook(),
new WeCom(),
new GoAlert(),
new ZohoCliq()
new ZohoCliq(),
new Whapi(),
new GtxMessaging(),
new Cellsynt(),
];
for (let item of list) {
if (! item.name) {

View File

@ -116,14 +116,23 @@ class UptimeCalculator {
]);
for (let bean of minutelyStatBeans) {
let key = bean.timestamp;
this.minutelyUptimeDataList.push(key, {
let data = {
up: bean.up,
down: bean.down,
avgPing: bean.ping,
minPing: bean.pingMin,
maxPing: bean.pingMax,
});
};
if (bean.extras != null) {
data = {
...data,
...JSON.parse(bean.extras),
};
}
let key = bean.timestamp;
this.minutelyUptimeDataList.push(key, data);
}
// Load hourly data from database (recent 30 days only)
@ -133,14 +142,22 @@ class UptimeCalculator {
]);
for (let bean of hourlyStatBeans) {
let key = bean.timestamp;
this.hourlyUptimeDataList.push(key, {
let data = {
up: bean.up,
down: bean.down,
avgPing: bean.ping,
minPing: bean.pingMin,
maxPing: bean.pingMax,
});
};
if (bean.extras != null) {
data = {
...data,
...JSON.parse(bean.extras),
};
}
this.hourlyUptimeDataList.push(bean.timestamp, data);
}
// Load daily data from database (recent 365 days only)
@ -150,14 +167,22 @@ class UptimeCalculator {
]);
for (let bean of dailyStatBeans) {
let key = bean.timestamp;
this.dailyUptimeDataList.push(key, {
let data = {
up: bean.up,
down: bean.down,
avgPing: bean.ping,
minPing: bean.pingMin,
maxPing: bean.pingMax,
});
};
if (bean.extras != null) {
data = {
...data,
...JSON.parse(bean.extras),
};
}
this.dailyUptimeDataList.push(bean.timestamp, data);
}
}
@ -170,11 +195,6 @@ class UptimeCalculator {
async update(status, ping = 0) {
let date = this.getCurrentDate();
// Don't count MAINTENANCE into uptime
if (status === MAINTENANCE) {
return date;
}
let flatStatus = this.flatStatus(status);
if (flatStatus === DOWN && ping > 0) {
@ -189,7 +209,12 @@ class UptimeCalculator {
let hourlyData = this.hourlyUptimeDataList[hourlyKey];
let dailyData = this.dailyUptimeDataList[dailyKey];
if (flatStatus === UP) {
if (status === MAINTENANCE) {
minutelyData.maintenance = minutelyData.maintenance ? minutelyData.maintenance + 1 : 1;
hourlyData.maintenance = hourlyData.maintenance ? hourlyData.maintenance + 1 : 1;
dailyData.maintenance = dailyData.maintenance ? dailyData.maintenance + 1 : 1;
} else if (flatStatus === UP) {
minutelyData.up += 1;
hourlyData.up += 1;
dailyData.up += 1;
@ -233,7 +258,7 @@ class UptimeCalculator {
}
}
} else {
} else if (flatStatus === DOWN) {
minutelyData.down += 1;
hourlyData.down += 1;
dailyData.down += 1;
@ -263,6 +288,13 @@ class UptimeCalculator {
dailyStatBean.ping = dailyData.avgPing;
dailyStatBean.pingMin = dailyData.minPing;
dailyStatBean.pingMax = dailyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, ...extras } = dailyData;
if (Object.keys(extras).length > 0) {
dailyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(dailyStatBean);
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
@ -271,6 +303,13 @@ class UptimeCalculator {
hourlyStatBean.ping = hourlyData.avgPing;
hourlyStatBean.pingMin = hourlyData.minPing;
hourlyStatBean.pingMax = hourlyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, ...extras } = hourlyData;
if (Object.keys(extras).length > 0) {
hourlyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(hourlyStatBean);
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
@ -279,6 +318,13 @@ class UptimeCalculator {
minutelyStatBean.ping = minutelyData.avgPing;
minutelyStatBean.pingMin = minutelyData.minPing;
minutelyStatBean.pingMax = minutelyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, ...extras } = minutelyData;
if (Object.keys(extras).length > 0) {
minutelyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(minutelyStatBean);
// Remove the old data
@ -474,7 +520,7 @@ class UptimeCalculator {
flatStatus(status) {
switch (status) {
case UP:
// case MAINTENANCE:
case MAINTENANCE:
return UP;
case DOWN:
case PENDING:
@ -606,7 +652,11 @@ class UptimeCalculator {
avgPing = totalPing / total.up;
}
uptimeData.uptime = total.up / (total.up + total.down);
if (total.up + total.down === 0) {
uptimeData.uptime = 0;
} else {
uptimeData.uptime = total.up / (total.up + total.down);
}
uptimeData.avgPing = avgPing;
return uptimeData;
}

View File

@ -115,6 +115,7 @@ export default {
"apprise": this.$t("apprise"),
"Bark": "Bark",
"clicksendsms": "ClickSend SMS",
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
"discord": "Discord",
"GoogleChat": "Google Chat (Google Workspace)",
"gorush": "Gorush",
@ -152,7 +153,10 @@ export default {
"Splunk": "Splunk",
"webhook": "Webhook",
"GoAlert": "GoAlert",
"ZohoCliq": "ZohoCliq"
"ZohoCliq": "ZohoCliq",
"whapi": "WhatsApp (Whapi)",
"gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt",
};
// Put notifications here if it's not supported in most regions or its documentation is not in English

View File

@ -0,0 +1,13 @@
<template>
<div class="mb-3">
<label for="callmebot-endpoint" class="form-label">{{ $t("Endpoint") }}</label>
<input id="callmebot-endpoint" v-model="$parent.notification.callMeBotEndpoint" type="text" class="form-control" required>
<i18n-t tag="div" keypath="callMeBotGet" class="form-text">
<a href="https://www.callmebot.com/blog/free-api-facebook-messenger/" target="_blank">Facebook Messenger</a>
<a href="https://www.callmebot.com/blog/test-whatsapp-api/" target="_blank">WhatsApp</a>
<a href="https://www.callmebot.com/blog/telegram-phone-call-using-your-browser/" target="_blank">Telegram Call</a>
1 message / 10 sec; 1 call / 65 sec
<!--There is no public documentation available. This data is based on testing!-->
</i18n-t>
</div>
</template>

View File

@ -0,0 +1,54 @@
<template>
<div class="mb-3">
<label for="cellsynt-login" class="form-label">{{ $t("Username") }}</label>
<input id="cellsynt-login" v-model="$parent.notification.cellsyntLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="cellsynt-key" class="form-label">{{ $t("Password") }}</label>
<HiddenInput id="cellsynt-key" v-model="$parent.notification.cellsyntPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="cellsynt-Originatortype" class="form-label">{{ $t("Originator type") }}</label>
<select id="cellsynt-Originatortype" v-model="$parent.notification.cellsyntOriginatortype" :required="true" class="form-select">
<option value="alpha">{{ $t("Alphanumeric (recommended)") }}</option>
<option value="numeric">{{ $t("Telephone number") }}</option>
</select>
<div class="form-text">
<p><b>{{ $t("Alphanumeric (recommended)") }}:</b><br /> {{ $t("Alphanumeric string (max 11 alphanumeric characters). Recipients can not reply to the message.") }}</p>
<p><b>{{ $t("Telephone number") }}:</b><br /> {{ $t("Numeric value (max 15 digits) with telephone number on international format without leading 00 (example UK number 07920 110 000 should be set as 447920110000). Recipients can reply to the message.") }}</p>
</div>
</div>
<div class="mb-3">
<label for="cellsynt-originator" class="form-label">{{ $t("Originator") }} <small>({{ $parent.notification.cellsyntOriginatortype === 'alpha' ? $t("max 11 alphanumeric characters") : $t("max 15 digits") }})</small></label>
<input v-if="$parent.notification.cellsyntOriginatortype === 'alpha'" id="cellsynt-originator" v-model="$parent.notification.cellsyntOriginator" type="text" class="form-control" pattern="[a-zA-Z0-9\s]+" maxlength="11" required>
<input v-else id="cellsynt-originator" v-model="$parent.notification.cellsyntOriginator" type="number" class="form-control" pattern="[0-9]+" maxlength="15" required>
<div class="form-text"><p>{{ $t("Visible on recipient's mobile phone as originator of the message. Allowed values and function depends on parameter originatortype.") }}</p></div>
</div>
<div class="mb-3">
<label for="cellsynt-destination" class="form-label">{{ $t("Destination") }}</label>
<input id="cellsynt-destination" v-model="$parent.notification.cellsyntDestination" type="text" class="form-control" required>
<div class="form-text"><p>{{ $t("Recipient's telephone number using international format with leading 00 followed by country code, e.g. 00447920110000 for the UK number 07920 110 000 (max 17 digits in total). Max 25000 comma separated recipients per HTTP request.") }}</p></div>
</div>
<div class="form-check form-switch">
<input id="cellsynt-allow-long" v-model="$parent.notification.cellsyntAllowLongSMS" type="checkbox" class="form-check-input">
<label for="cellsynt-allow-long" class="form-label">{{ $t("Allow Long SMS") }}</label>
<div class="form-text">{{ $t("Split long messages into up to 6 parts. 153 x 6 = 918 characters.") }}</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://www.cellsynt.com/en/" target="_blank">https://www.cellsynt.com/en/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput
},
mounted() {
this.$parent.notification.cellsyntOriginatortype ||= "alpha";
this.$parent.notification.cellsyntOriginator ||= "uptimekuma";
}
};
</script>

View File

@ -0,0 +1,49 @@
<template>
<div class="mb-3">
<label for="gtxmessaging-api-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="gtxmessaging-api-key" v-model="$parent.notification.gtxMessagingApiKey" :required="true"></HiddenInput>
<div class="form-text">
{{ $t("gtxMessagingApiKeyHint") }}
</div>
</div>
<div class="mb-3">
<label for="gtxmessaging-from" class="form-label">{{ $t("From Phone Number / Transmission Path Originating Address (TPOA)") }}</label>
<input id="gtxmessaging-from" v-model="$parent.notification.gtxMessagingFrom" type="text" class="form-control" required>
<i18n-t tag="div" keypath="gtxMessagingFromHint" class="form-text">
<template #e164>
<a href="https://wikipedia.org/wiki/E.164">E.164</a>
</template>
<template #e212>
<a href="https://wikipedia.org/wiki/E.212">E.212</a>
</template>
<template #e214>
<a href="https://wikipedia.org/wiki/E.214">E.214</a>
</template>
</i18n-t>
</div>
<div class="mb-3">
<label for="gtxmessaging-to" class="form-label">{{ $t("To Phone Number") }}</label>
<input id="gtxmessaging-to" v-model="$parent.notification.gtxMessagingTo" type="text" pattern="^\+\d+$" class="form-control" required>
<i18n-t tag="div" keypath="gtxMessagingToHint" class="form-text">
<template #e164>
<a href="https://wikipedia.org/wiki/E.164">E.164</a>
</template>
<template #e212>
<a href="https://wikipedia.org/wiki/E.212">E.212</a>
</template>
<template #e214>
<a href="https://wikipedia.org/wiki/E.214">E.214</a>
</template>
</i18n-t>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput
}
};
</script>

View File

@ -0,0 +1,33 @@
<template>
<div class="mb-3">
<label for="whapi-api-url" class="form-label">{{ $t("API URL") }}</label>
<input id="whapi-api-url" v-model="$parent.notification.whapiApiUrl" placeholder="https://gate.whapi.cloud/" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="whapi-auth-token" class="form-label">{{ $t("Token") }}</label>
<HiddenInput id="whapi-auth-token" v-model="$parent.notification.whapiAuthToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetWhapiUrlAndToken" class="form-text">
<a href="https://panel.whapi.cloud/dashboard" target="_blank">https://panel.whapi.cloud/dashboard</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="whapi-recipient" class="form-label">{{ $t("whapiRecipient") }}</label>
<input id="whapi-recipient" v-model="$parent.notification.whapiRecipient" type="text" pattern="^[\d-]{10,31}(@[\w\.]{1,})?$" class="form-control" required>
<div class="form-text">{{ $t("wayToWriteWhapiRecipient", ["00117612345678", "00117612345678@s.whatsapp.net", "123456789012345678@g.us"]) }}</div>
</div>
<i18n-t tag="div" keypath="More info on:" class="mb-3 form-text">
<a href="https://whapi.cloud/" target="_blank">https://whapi.cloud/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
}
};
</script>

View File

@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue";
import Bark from "./Bark.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
import CallMeBot from "./CallMeBot.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
@ -13,6 +14,7 @@ import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue";
import GrafanaOncall from "./GrafanaOncall.vue";
import GtxMessaging from "./GtxMessaging.vue";
import HomeAssistant from "./HomeAssistant.vue";
import HeiiOnCall from "./HeiiOnCall.vue";
import Kook from "./Kook.vue";
@ -53,6 +55,8 @@ import WeCom from "./WeCom.vue";
import GoAlert from "./GoAlert.vue";
import ZohoCliq from "./ZohoCliq.vue";
import Splunk from "./Splunk.vue";
import Whapi from "./Whapi.vue";
import Cellsynt from "./Cellsynt.vue";
/**
* Manage all notification form.
@ -65,6 +69,7 @@ const NotificationFormList = {
"apprise": Apprise,
"Bark": Bark,
"clicksendsms": ClickSendSMS,
"CallMeBot": CallMeBot,
"smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
@ -113,7 +118,10 @@ const NotificationFormList = {
"WeCom": WeCom,
"GoAlert": GoAlert,
"ServerChan": ServerChan,
"ZohoCliq": ZohoCliq
"ZohoCliq": ZohoCliq,
"whapi": Whapi,
"gtxmessaging": GtxMessaging,
"Cellsynt": Cellsynt,
};
export default NotificationFormList;

View File

@ -28,7 +28,7 @@
</button>
</div>
<div class="my-4">
<div class="my-3">
<div v-if="$root.info.dbType === 'sqlite'" class="my-3">
<button class="btn btn-outline-info me-2" @click="shrinkDatabase">
{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})
</button>

View File

@ -57,7 +57,7 @@ for (let lang in languageList) {
};
}
const rtlLangs = [ "fa", "ar-SY", "ur" ];
const rtlLangs = [ "he-IL", "fa", "ar-SY", "ur" ];
export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language

View File

@ -886,7 +886,30 @@
"deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
"GrafanaOncallUrl": "Grafana Oncall URL",
"Browser Screenshot": "Browser Screenshot",
"wayToWriteWhapiRecipient": "The phone number with the international prefix, but without the plus sign at the start ({0}), the Contact ID ({1}) or the Group ID ({2}).",
"wayToGetWhapiUrlAndToken": "You can get the API URL and the token by going into your desired channel from {0}",
"whapiRecipient": "Phone Number / Contact ID / Group ID",
"API URL": "API URL",
"What is a Remote Browser?": "What is a Remote Browser?",
"wayToGetHeiiOnCallDetails": "How to get the Trigger ID and API Keys is explained in the {documentation}",
"documentationOf": "{0} Documentation"
"documentationOf": "{0} Documentation",
"callMeBotGet": "Here you can generate an endpoint for {0}, {1} and {2}. Keep in mind that you might get rate limited. The ratelimits appear to be: {3}",
"gtxMessagingApiKeyHint": "You can find your API key at: My Routing Accounts > Show Account Information > API Credentials > REST API (v2.x)",
"From Phone Number / Transmission Path Originating Address (TPOA)": "From Phone Number / Transmission Path Originating Address (TPOA)",
"gtxMessagingFromHint": "On mobile phones, your recipients sees the TPOA displayed as the sender of the message. Allowed are up to 11 alphanumeric characters, a shortcode, the local longcode or international numbers ({e164}, {e212} or {e214})",
"To Phone Number": "To Phone Number",
"gtxMessagingToHint": "International format, with leading \"+\" ({e164}, {e212} or {e214})",
"Originator type": "Originator type",
"Alphanumeric (recommended)": "Alphanumeric (recommended)",
"Telephone number": "Telephone number",
"cellsyntOriginatortypeAlphanumeric": "Alphanumeric string (max 11 alphanumeric characters). Recipients can not reply to the message.",
"cellsyntOriginatortypeNumeric": "Numeric value (max 15 digits) with telephone number on international format without leading 00 (example UK number 07920 110 000 should be set as 447920110000). Recipients can reply to the message.",
"Originator": "Originator",
"cellsyntOriginator": "Visible on recipient's mobile phone as originator of the message. Allowed values and function depends on parameter originatortype.",
"Destination": "Destination",
"cellsyntDestination": "Recipient's telephone number using international format with leading 00 followed by country code, e.g. 00447920110000 for the UK number 07920 110 000 (max 17 digits in total). Max 25000 comma separated recipients per HTTP request.",
"Allow Long SMS": "Allow Long SMS",
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
"max 15 digits": "max 15 digits",
"max 11 alphanumeric characters": "max 11 alphanumeric characters"
}