-
-
Notifications
You must be signed in to change notification settings - Fork 7.3k
feat: implement incident history #6469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
1a9748d
11a9414
71f32ae
c730f39
1057cc9
9e000ae
aced37d
db883c0
6fee737
4bdea1e
f9061eb
72753d0
8076275
fea823d
22bc222
c0a5ea8
ff084d6
79cc7e8
5ed6804
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ const { marked } = require("marked"); | |
| const { Feed } = require("feed"); | ||
| const config = require("../config"); | ||
|
|
||
| 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, INCIDENT_PAGE_SIZE } = require("../../src/util"); | ||
|
|
||
| class StatusPage extends BeanModel { | ||
|
|
||
|
|
@@ -267,14 +267,11 @@ class StatusPage extends BeanModel { | |
| static async getStatusPageData(statusPage) { | ||
| const config = await statusPage.toPublicJSON(); | ||
|
|
||
| // Incident | ||
| let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [ | ||
| // All active incidents | ||
| let incidents = await R.find("incident", " pin = 1 AND active = 1 AND status_page_id = ? ORDER BY created_date DESC", [ | ||
| statusPage.id, | ||
| ]); | ||
|
|
||
| if (incident) { | ||
| incident = incident.toPublicJSON(); | ||
| } | ||
| incidents = incidents.map(i => i.toPublicJSON()); | ||
|
|
||
| let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id); | ||
|
|
||
|
|
@@ -294,7 +291,7 @@ class StatusPage extends BeanModel { | |
| // Response | ||
| return { | ||
| config, | ||
| incident, | ||
| incidents, | ||
| publicGroupList, | ||
| maintenanceList, | ||
| }; | ||
|
|
@@ -468,6 +465,31 @@ class StatusPage extends BeanModel { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get paginated incident history for a status page | ||
| * @param {number} statusPageId ID of the status page | ||
| * @param {number} page Page number (1-based) | ||
| * @param {boolean} isPublic Whether to return public or admin data | ||
| * @returns {Promise<object>} Paginated incident data | ||
| */ | ||
| static async getIncidentHistory(statusPageId, page, isPublic = true) { | ||
| const offset = (page - 1) * INCIDENT_PAGE_SIZE; | ||
|
|
||
| const incidents = await R.find("incident", | ||
| " status_page_id = ? ORDER BY created_date DESC LIMIT ? OFFSET ? ", | ||
| [ statusPageId, INCIDENT_PAGE_SIZE, offset ] | ||
| ); | ||
|
||
|
|
||
| const total = await R.count("incident", " status_page_id = ? ", [ statusPageId ]); | ||
|
|
||
| return { | ||
| incidents: incidents.map(i => isPublic ? i.toPublicJSON() : i.toJSON()), | ||
| total, | ||
| page, | ||
| totalPages: Math.ceil(total / INCIDENT_PAGE_SIZE) | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Get list of maintenances | ||
| * @param {number} statusPageId ID of status page to get maintenance for | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,21 @@ const apicache = require("../modules/apicache"); | |
| const StatusPage = require("../model/status_page"); | ||
| const { UptimeKumaServer } = require("../uptime-kuma-server"); | ||
|
|
||
| /** | ||
| * Validates incident data | ||
| * @param {object} incident - The incident object | ||
| * @returns {void} | ||
| * @throws {Error} If validation fails | ||
| */ | ||
| function validateIncident(incident) { | ||
| if (!incident.title || incident.title.trim() === "") { | ||
| throw new Error("Please input title"); | ||
| } | ||
| if (!incident.content || incident.content.trim() === "") { | ||
| throw new Error("Please input content"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Socket handlers for status page | ||
| * @param {Socket} socket Socket.io instance to add listeners on | ||
|
|
@@ -26,10 +41,6 @@ module.exports.statusPageSocketHandler = (socket) => { | |
| throw new Error("slug is not found"); | ||
| } | ||
|
|
||
| await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [ | ||
| statusPageID | ||
| ]); | ||
|
|
||
| let incidentBean; | ||
|
|
||
| if (incident.id) { | ||
|
|
@@ -47,6 +58,7 @@ module.exports.statusPageSocketHandler = (socket) => { | |
| incidentBean.content = incident.content; | ||
| incidentBean.style = incident.style; | ||
| incidentBean.pin = true; | ||
| incidentBean.active = true; | ||
| incidentBean.status_page_id = statusPageID; | ||
|
|
||
| if (incident.id) { | ||
|
|
@@ -90,6 +102,192 @@ module.exports.statusPageSocketHandler = (socket) => { | |
| } | ||
| }); | ||
|
|
||
| socket.on("getIncidentHistory", async (slug, page, callback) => { | ||
|
||
| try { | ||
| checkLogin(socket); | ||
|
|
||
| let statusPageID = await StatusPage.slugToID(slug); | ||
| if (!statusPageID) { | ||
| throw new Error("slug is not found"); | ||
| } | ||
|
|
||
| const result = await StatusPage.getIncidentHistory(statusPageID, page, false); | ||
| callback({ | ||
| ok: true, | ||
| ...result | ||
| }); | ||
| } catch (error) { | ||
| callback({ | ||
| ok: false, | ||
| msg: error.message, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| socket.on("getPublicIncidentHistory", async (slug, page, callback) => { | ||
| try { | ||
| let statusPageID = await StatusPage.slugToID(slug); | ||
| if (!statusPageID) { | ||
| throw new Error("slug is not found"); | ||
| } | ||
|
|
||
| const result = await StatusPage.getIncidentHistory(statusPageID, page, true); | ||
| callback({ | ||
| ok: true, | ||
| ...result | ||
| }); | ||
| } catch (error) { | ||
| callback({ | ||
| ok: false, | ||
| msg: error.message, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| socket.on("editIncident", async (slug, incidentID, incident, callback) => { | ||
| try { | ||
| checkLogin(socket); | ||
|
|
||
| let statusPageID = await StatusPage.slugToID(slug); | ||
| if (!statusPageID) { | ||
| callback({ | ||
| ok: false, | ||
| msg: "slug is not found", | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| let bean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [ incidentID, statusPageID ]); | ||
| if (!bean) { | ||
| callback({ | ||
| ok: false, | ||
| msg: "Incident not found or access denied", | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| validateIncident(incident); | ||
| } catch (e) { | ||
| callback({ | ||
| ok: false, | ||
| msg: e.message, | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| const validStyles = [ "info", "warning", "danger", "primary", "light", "dark" ]; | ||
| if (!validStyles.includes(incident.style)) { | ||
| incident.style = "warning"; | ||
| } | ||
|
|
||
| bean.title = incident.title; | ||
| bean.content = incident.content; | ||
| bean.style = incident.style; | ||
| bean.pin = incident.pin !== false; | ||
| bean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); | ||
|
|
||
| await R.store(bean); | ||
|
|
||
| callback({ | ||
| ok: true, | ||
| msg: "Saved.", | ||
| msgi18n: true, | ||
| incident: bean.toJSON(), | ||
| }); | ||
| } catch (error) { | ||
| callback({ | ||
| ok: false, | ||
| msg: error.message, | ||
| msgi18n: true, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| socket.on("deleteIncident", async (slug, incidentID, callback) => { | ||
| try { | ||
| checkLogin(socket); | ||
|
|
||
| let statusPageID = await StatusPage.slugToID(slug); | ||
| if (!statusPageID) { | ||
| callback({ | ||
| ok: false, | ||
| msg: "slug is not found", | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| let bean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [ incidentID, statusPageID ]); | ||
| if (!bean) { | ||
| callback({ | ||
| ok: false, | ||
| msg: "Incident not found or access denied", | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| await R.trash(bean); | ||
|
|
||
| callback({ | ||
| ok: true, | ||
| msg: "successDeleted", | ||
| msgi18n: true, | ||
| }); | ||
| } catch (error) { | ||
| callback({ | ||
| ok: false, | ||
| msg: error.message, | ||
| msgi18n: true, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| socket.on("resolveIncident", async (slug, incidentID, callback) => { | ||
| try { | ||
| checkLogin(socket); | ||
|
|
||
| let statusPageID = await StatusPage.slugToID(slug); | ||
| if (!statusPageID) { | ||
| callback({ | ||
| ok: false, | ||
| msg: "slug is not found", | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| let bean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [ incidentID, statusPageID ]); | ||
| if (!bean) { | ||
| callback({ | ||
| ok: false, | ||
| msg: "Incident not found or access denied", | ||
| msgi18n: true | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| await bean.resolve(); | ||
|
|
||
| callback({ | ||
| ok: true, | ||
| msg: "Resolved", | ||
aluminyoom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| msgi18n: true, | ||
| incident: bean.toJSON(), | ||
| }); | ||
| } catch (error) { | ||
| callback({ | ||
| ok: false, | ||
| msg: error.message, | ||
| msgi18n: true, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| socket.on("getStatusPage", async (slug, callback) => { | ||
| try { | ||
| checkLogin(socket); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't consider this quite internal, or am I missing something? π€
Can't this just be added to
toPublicJSON?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
definitely trivial yea, reasoning behind this is to expose the only needed data for public endpoints. since
status_page_iddoesn't really get consumed on the public facing side.we can add it to
toPublicJSONbut it would be unused for public endpoints.edit: formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets simplify this then to just include it up in
toPublicJSON.