feat: add rss title field and handle rss link from request
This commit is contained in:
parent
90fcbdc7d7
commit
ac87fa1969
@ -202,6 +202,7 @@ async function createTables() {
|
|||||||
table.text("footer_text");
|
table.text("footer_text");
|
||||||
table.text("custom_css");
|
table.text("custom_css");
|
||||||
table.boolean("show_powered_by").notNullable().defaultTo(true);
|
table.boolean("show_powered_by").notNullable().defaultTo(true);
|
||||||
|
table.string("rss_title", 255);
|
||||||
table.string("google_analytics_tag_id");
|
table.string("google_analytics_tag_id");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
db/knex_migrations/2026-01-05-0000-add-rss-title.js
Normal file
12
db/knex_migrations/2026-01-05-0000-add-rss-title.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("status_page", function (table) {
|
||||||
|
table.string("rss_title", 255);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("status_page", function (table) {
|
||||||
|
table.dropColumn("rss_title");
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -7,6 +7,7 @@ const analytics = require("../analytics/analytics");
|
|||||||
const { marked } = require("marked");
|
const { marked } = require("marked");
|
||||||
const { Feed } = require("feed");
|
const { Feed } = require("feed");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
|
||||||
const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");
|
const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");
|
||||||
|
|
||||||
@ -22,16 +23,17 @@ class StatusPage extends BeanModel {
|
|||||||
* Handle responses to RSS pages
|
* Handle responses to RSS pages
|
||||||
* @param {Response} response Response object
|
* @param {Response} response Response object
|
||||||
* @param {string} slug Status page slug
|
* @param {string} slug Status page slug
|
||||||
|
* @param {Request} request Request object
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async handleStatusPageRSSResponse(response, slug) {
|
static async handleStatusPageRSSResponse(response, slug, request) {
|
||||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
slug
|
slug
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (statusPage) {
|
if (statusPage) {
|
||||||
response.type("application/rss+xml");
|
response.type("application/rss+xml");
|
||||||
response.send(await StatusPage.renderRSS(statusPage, slug));
|
response.send(await StatusPage.renderRSS(statusPage, slug, request));
|
||||||
} else {
|
} else {
|
||||||
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
|
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
|
||||||
}
|
}
|
||||||
@ -64,20 +66,29 @@ class StatusPage extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SSR for RSS feed
|
* SSR for RSS feed
|
||||||
* @param {statusPage} statusPage object
|
* @param {StatusPage} statusPage Status page object
|
||||||
* @param {slug} slug from router
|
* @param {string} slug Status page slug
|
||||||
* @returns {Promise<string>} the rendered html
|
* @param {Request} request Express request object
|
||||||
|
* @returns {Promise<string>} The rendered RSS XML
|
||||||
*/
|
*/
|
||||||
static async renderRSS(statusPage, slug) {
|
static async renderRSS(statusPage, slug, request) {
|
||||||
const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage);
|
const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage);
|
||||||
|
|
||||||
let proto = config.isSSL ? "https" : "http";
|
// Build the feed URL, respecting proxy headers if trustProxy is enabled
|
||||||
let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`;
|
const feedUrl = await StatusPage.buildRSSUrl(slug, request);
|
||||||
|
|
||||||
|
// Use custom RSS title if set, otherwise fall back to status page title
|
||||||
|
let feedTitle = "Uptime Kuma RSS Feed";
|
||||||
|
if (statusPage.rss_title) {
|
||||||
|
feedTitle = statusPage.rss_title;
|
||||||
|
} else if (statusPage.title) {
|
||||||
|
feedTitle = `${statusPage.title} RSS Feed`;
|
||||||
|
}
|
||||||
|
|
||||||
const feed = new Feed({
|
const feed = new Feed({
|
||||||
title: "uptime kuma rss feed",
|
title: feedTitle,
|
||||||
description: `current status: ${statusDescription}`,
|
description: `Current status: ${statusDescription}`,
|
||||||
link: host,
|
link: feedUrl,
|
||||||
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
|
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
|
||||||
updated: new Date(), // optional, default = today
|
updated: new Date(), // optional, default = today
|
||||||
});
|
});
|
||||||
@ -85,8 +96,9 @@ class StatusPage extends BeanModel {
|
|||||||
heartbeats.forEach(heartbeat => {
|
heartbeats.forEach(heartbeat => {
|
||||||
feed.addItem({
|
feed.addItem({
|
||||||
title: `${heartbeat.name} is down`,
|
title: `${heartbeat.name} is down`,
|
||||||
description: `${heartbeat.name} has been down since ${heartbeat.time}`,
|
description: `${heartbeat.name} has been down since ${heartbeat.time} UTC`,
|
||||||
id: heartbeat.monitorID,
|
id: `${heartbeat.monitorID}-${heartbeat.time}`,
|
||||||
|
link: feedUrl,
|
||||||
date: new Date(heartbeat.time),
|
date: new Date(heartbeat.time),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -94,6 +106,38 @@ class StatusPage extends BeanModel {
|
|||||||
return feed.rss2();
|
return feed.rss2();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build RSS feed URL, handling proxy headers
|
||||||
|
* @param {string} slug Status page slug
|
||||||
|
* @param {Request} request Express request object
|
||||||
|
* @returns {Promise<string>} The full URL for the RSS feed
|
||||||
|
*/
|
||||||
|
static async buildRSSUrl(slug, request) {
|
||||||
|
if (request) {
|
||||||
|
const trustProxy = await setting("trustProxy");
|
||||||
|
|
||||||
|
// Determine protocol (check X-Forwarded-Proto if behind proxy)
|
||||||
|
let proto = request.protocol;
|
||||||
|
if (trustProxy && request.headers["x-forwarded-proto"]) {
|
||||||
|
proto = request.headers["x-forwarded-proto"].split(",")[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine host (check X-Forwarded-Host if behind proxy)
|
||||||
|
let host = request.get("host");
|
||||||
|
if (trustProxy && request.headers["x-forwarded-host"]) {
|
||||||
|
host = request.headers["x-forwarded-host"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${proto}://${host}/status/${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to config values
|
||||||
|
const proto = config.isSSL ? "https" : "http";
|
||||||
|
const host = config.hostname || "localhost";
|
||||||
|
const port = config.port;
|
||||||
|
return `${proto}://${host}:${port}/status/${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSR for status pages
|
* SSR for status pages
|
||||||
* @param {string} indexHTML HTML page to render
|
* @param {string} indexHTML HTML page to render
|
||||||
@ -415,7 +459,8 @@ class StatusPage extends BeanModel {
|
|||||||
analyticsScriptUrl: this.analytics_script_url,
|
analyticsScriptUrl: this.analytics_script_url,
|
||||||
analyticsType: this.analytics_type,
|
analyticsType: this.analytics_type,
|
||||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||||
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat
|
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat,
|
||||||
|
rssTitle: this.rss_title,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,7 +486,8 @@ class StatusPage extends BeanModel {
|
|||||||
analyticsScriptUrl: this.analytics_script_url,
|
analyticsScriptUrl: this.analytics_script_url,
|
||||||
analyticsType: this.analytics_type,
|
analyticsType: this.analytics_type,
|
||||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||||
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat
|
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat,
|
||||||
|
rssTitle: this.rss_title,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
|
|||||||
router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
|
router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
|
||||||
let slug = request.params.slug;
|
let slug = request.params.slug;
|
||||||
slug = slug.toLowerCase();
|
slug = slug.toLowerCase();
|
||||||
await StatusPage.handleStatusPageRSSResponse(response, slug);
|
await StatusPage.handleStatusPageRSSResponse(response, slug, request);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/status", cache("5 minutes"), async (request, response) => {
|
router.get("/status", cache("5 minutes"), async (request, response) => {
|
||||||
|
|||||||
@ -164,6 +164,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
statusPage.footer_text = config.footerText;
|
statusPage.footer_text = config.footerText;
|
||||||
statusPage.custom_css = config.customCSS;
|
statusPage.custom_css = config.customCSS;
|
||||||
statusPage.show_powered_by = config.showPoweredBy;
|
statusPage.show_powered_by = config.showPoweredBy;
|
||||||
|
statusPage.rss_title = config.rssTitle;
|
||||||
statusPage.show_only_last_heartbeat = config.showOnlyLastHeartbeat;
|
statusPage.show_only_last_heartbeat = config.showOnlyLastHeartbeat;
|
||||||
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
||||||
statusPage.modified_date = R.isoDateTime();
|
statusPage.modified_date = R.isoDateTime();
|
||||||
|
|||||||
@ -388,6 +388,8 @@
|
|||||||
"Proxy": "Proxy",
|
"Proxy": "Proxy",
|
||||||
"Date Created": "Date Created",
|
"Date Created": "Date Created",
|
||||||
"Footer Text": "Footer Text",
|
"Footer Text": "Footer Text",
|
||||||
|
"RSS Title": "RSS Title",
|
||||||
|
"Leave blank to use status page title": "Leave blank to use status page title",
|
||||||
"Refresh Interval": "Refresh Interval",
|
"Refresh Interval": "Refresh Interval",
|
||||||
"Refresh Interval Description": "The status page will do a full site refresh every {0} seconds",
|
"Refresh Interval Description": "The status page will do a full site refresh every {0} seconds",
|
||||||
"Show Powered By": "Show Powered By",
|
"Show Powered By": "Show Powered By",
|
||||||
|
|||||||
@ -34,6 +34,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- RSS Title -->
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="rss-title" class="form-label">{{ $t("RSS Title") }}</label>
|
||||||
|
<input id="rss-title" v-model="config.rssTitle" type="text" class="form-control" data-testid="rss-title-input">
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("Leave blank to use status page title") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="auto-refresh-interval" class="form-label">{{ $t("Refresh Interval") }}</label>
|
<label for="auto-refresh-interval" class="form-label">{{ $t("Refresh Interval") }}</label>
|
||||||
<input id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" class="form-control" :min="5" data-testid="refresh-interval-input">
|
<input id="auto-refresh-interval" v-model="config.autoRefreshInterval" type="number" class="form-control" :min="5" data-testid="refresh-interval-input">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user