From 5bd98353aff8ba7100c36f5df0bc63f0ea4257aa Mon Sep 17 00:00:00 2001
From: Hiago Silva Souza <hiasilva@gmail.com>
Date: Sat, 25 Jan 2025 00:24:20 -0300
Subject: [PATCH 1/5] feat: generate badge from a status page

---
 package-lock.json                     |   4 +-
 server/model/status_page.js           |  85 ++++++++++++-
 server/routers/status-page-router.js  |   8 +-
 src/components/BadgeBuilderDialog.vue | 153 ++++++++++++++++++++++
 src/icon.js                           |   2 +
 src/lang/en.json                      |  19 ++-
 src/lang/pt-BR.json                   |  19 ++-
 src/pages/StatusPage.vue              | 177 +++++++++++++++-----------
 8 files changed, 382 insertions(+), 85 deletions(-)
 create mode 100644 src/components/BadgeBuilderDialog.vue

diff --git a/package-lock.json b/package-lock.json
index 8d3f58b4d..ccf1a628c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "uptime-kuma",
-    "version": "2.0.0-beta.0",
+    "version": "2.0.0-beta.1",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
             "name": "uptime-kuma",
-            "version": "2.0.0-beta.0",
+            "version": "2.0.0-beta.1",
             "license": "MIT",
             "dependencies": {
                 "@grpc/grpc-js": "~1.8.22",
diff --git a/server/model/status_page.js b/server/model/status_page.js
index 38f548ebb..08a69154c 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -9,6 +9,7 @@ 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 { makeBadge } = require("badge-maker");
 
 class StatusPage extends BeanModel {
 
@@ -16,7 +17,7 @@ class StatusPage extends BeanModel {
      * Like this: { "test-uptime.kuma.pet": "default" }
      * @type {{}}
      */
-    static domainMappingList = { };
+    static domainMappingList = {};
 
     /**
      * Handle responses to RSS pages
@@ -61,6 +62,26 @@ class StatusPage extends BeanModel {
         }
     }
 
+    /**
+     * Handle responses to status page
+     * @param {Response} response Response object
+     * @param {string} slug Status page slug
+     * @param {object} config Config for badge
+     * @returns {Promise<void>}
+     */
+    static async handleStatusPageSVGResponse(response, slug, config) {
+        slug = slug.replace('.svg', '')
+        let statusPage = await R.findOne("status_page", " slug = ? ", [
+            slug.replace('.svg', '')
+        ]);
+        if (statusPage) {
+            response.set('Content-Type', 'image/svg+xml')
+            response.send(await StatusPage.renderSVG(slug, config, statusPage));
+        } else {
+            response.status(404).send(await StatusPage.renderSVG(slug, config, null));
+        }
+    }
+
     /**
      * SSR for RSS feed
      * @param {statusPage} statusPage object
@@ -152,6 +173,62 @@ class StatusPage extends BeanModel {
         return $.root().html();
     }
 
+    static async renderSVG(slug, userConfig = {}, statusPage) {
+        let allowedBadgeStyles = ['flat', 'flat-square', 'for-the-badge', 'plastic', 'social'];
+        if (!allowedBadgeStyles.includes(userConfig.badgeStyle)) {
+            userConfig.badgeStyle = 'flat'
+        }
+
+        // returns unknown svg
+        if (statusPage === null) {
+            return makeBadge({ message: userConfig.notFoundText || 'not found', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' })
+        } else {
+
+            // get all heartbeats that correspond to this statusPage
+            const config = await statusPage.toPublicJSON();
+
+            // Public Group List
+            const showTags = !!statusPage.show_tags;
+
+            const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
+                statusPage.id
+            ]);
+
+            let heartbeats = [];
+
+            for (let groupBean of list) {
+                let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
+                for (const monitor of monitorGroup.monitorList) {
+                    const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]);
+                    if (heartbeat) {
+                        heartbeats.push({
+                            ...monitor,
+                            status: heartbeat.status,
+                            time: heartbeat.time
+                        });
+                    }
+                }
+            }
+
+            // calculate RSS feed description
+            let status = StatusPage.overallStatus(heartbeats);
+
+            switch (status) {
+                case STATUS_PAGE_ALL_DOWN:
+                    return makeBadge({ message: userConfig.allDownText ?? 'all down', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                case STATUS_PAGE_ALL_UP:
+                    return makeBadge({ message: userConfig.allOperationalText ?? 'all operational', color: 'green', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                case STATUS_PAGE_PARTIAL_DOWN:
+                    return makeBadge({ message: userConfig.partialDownText ?? 'partial down', color: 'orange', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                case STATUS_PAGE_MAINTENANCE:
+                    return makeBadge({ message: userConfig.maintenanceText || 'maintenance', color: 'yellow', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                default:
+                    return makeBadge({ message: userConfig.unknownText || 'unknown', color: 'lightgray', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+            }
+        }
+
+    }
+
     /**
      * @param {heartbeats} heartbeats from getRSSPageData
      * @returns {number} status_page constant from util.ts
@@ -174,7 +251,7 @@ class StatusPage extends BeanModel {
             }
         }
 
-        if (! hasUp) {
+        if (!hasUp) {
             status = STATUS_PAGE_ALL_DOWN;
         }
 
@@ -231,7 +308,7 @@ class StatusPage extends BeanModel {
         for (let groupBean of list) {
             let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
             for (const monitor of monitorGroup.monitorList) {
-                const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
+                const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]);
                 if (heartbeat) {
                     heartbeats.push({
                         ...monitor,
@@ -471,7 +548,7 @@ class StatusPage extends BeanModel {
                 SELECT DISTINCT maintenance_id
                 FROM maintenance_status_page
                 WHERE status_page_id = ?
-            `, [ statusPageId ]);
+            `, [statusPageId]);
 
             for (const maintenanceID of maintenanceIDList) {
                 let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js
index 893f57564..9c5b88101 100644
--- a/server/routers/status-page-router.js
+++ b/server/routers/status-page-router.js
@@ -16,7 +16,13 @@ const server = UptimeKumaServer.getInstance();
 router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
     let slug = request.params.slug;
     slug = slug.toLowerCase();
-    await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
+
+    if (slug.endsWith('.svg')) {
+        await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || '{}'));
+    } else {
+        await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
+    }
+
 });
 
 router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue
new file mode 100644
index 000000000..8b4f9cc79
--- /dev/null
+++ b/src/components/BadgeBuilderDialog.vue
@@ -0,0 +1,153 @@
+<template>
+    <form @submit.prevent="submit">
+        <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 id="exampleModalLabel" class="modal-title">
+                            {{ $t("Generate Badge") }}
+                        </h5>
+                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
+                    </div>
+                    <div class="modal-body">
+                        <div class="mb-3">
+                            <label for="badge-builder-badge-style" class="form-label">{{ $t("Badge Style") }}</label>
+                            <select id="badge-builder-badge-style" v-model="config.badgeStyle" class="form-select">
+                                <option value="flat">Flat</option>
+                                <option value="flat-square">Flat Square</option>
+                                <option value="for-the-badge">For The Badge</option>
+                                <option value="plastic">Plastic</option>
+                                <option value="social">Social</option>
+                            </select>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="badge-not-found-text" class="form-label">{{ $t("Return to not found") }}</label>
+                            <input id="badge-not-found-text" v-model="config.notFoundText" type="text"
+                                class="form-control">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="badge-operational-text" class="form-label">{{ $t("Return to operational")
+                                }}</label>
+                            <input id="badge-operational-text" v-model="config.allOperationalText" type="text"
+                                class="form-control">
+                        </div>
+
+
+                        <div class="mb-3">
+                            <label for="badge-partial-down-text" class="form-label">{{ $t("Return to partial down")
+                                }}</label>
+                            <input id="badge-partial-down-text" v-model="config.partialDownText" type="text"
+                                class="form-control">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="badge-maintenance-text" class="form-label">{{ $t("Return to maintenance")
+                                }}</label>
+                            <input id="badge-maintenance-text" v-model="config.maintenanceText" type="text"
+                                class="form-control">
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="badge-unknown-text" class="form-label">{{ $t("Return to unknown")
+                                }}</label>
+                            <input id="badge-unknown-text" v-model="config.unknownText" type="text"
+                                class="form-control">
+                        </div>
+
+                        <div class="Preview" v-if="slug">
+                            <div>{{ $t("Preview") }}</div>
+                            <img :src="getSvgURL(true)" class="mt-2" />
+                        </div>
+
+                    </div>
+
+
+
+                    <div class="modal-footer">
+                        <button type="button" @click="copyImageURL()" class="btn btn-primary">
+                            <!-- <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> -->
+                            {{ $t("Copy Image URL") }}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+
+</template>
+
+<script>
+import { Modal } from "bootstrap";
+import { useToast } from "vue-toastification";
+import axios from 'axios';
+const toast = useToast();
+
+export default {
+    components: {},
+    props: {},
+    emits: [],
+    data() {
+        return {
+            model: null,
+            slug: null,
+            config: {
+                badgeStyle: 'flat',
+                notFoundText: this.$t('not found'),
+                allDownText: this.$t('all down'),
+                allOperationalText: this.$t('operational'),
+                partialDownText: this.$t('partial down'),
+                maintenanceText: this.$t('maintenance'),
+                unknownText: this.$t('unknown')
+            }
+        };
+    },
+
+    computed: {
+    },
+
+    watch: {
+    },
+
+    mounted() {
+        this.modal = new Modal(this.$refs.modal);
+    },
+    methods: {
+        /**
+         * Show settings for specified notification
+         * @param {number} notificationID ID of notification to show
+         * @returns {void}
+         */
+        show(slug) {
+            this.slug = slug;
+            this.modal.show();
+        },
+
+        getSvgURL(disableCache = false) {
+            return axios.defaults.baseURL + '/status/' + this.slug + '.svg?config=' + encodeURIComponent(JSON.stringify(this.config)).replace(/%5B/g, '[').replace(/%5D/g, ']') + (disableCache ? '&noCache=' + Date.now() : '');
+        },
+
+        copyImageURL() {
+            const text = this.getSvgURL();
+            navigator.clipboard.writeText(text).then(() => {
+                toast.success(this.$t("Image url copied to clipboard."));
+            }).catch(err => {
+                toast.error(this.$t("Unable to copy image URL."));
+            });
+        }
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../assets/vars.scss";
+
+.dark {
+
+    .modal-dialog .form-text,
+    .modal-dialog p {
+        color: $dark-font-color;
+    }
+}
+</style>
diff --git a/src/icon.js b/src/icon.js
index 7bdfe1ca0..4bfb279ea 100644
--- a/src/icon.js
+++ b/src/icon.js
@@ -18,6 +18,7 @@ import {
     faPlus,
     faSearch,
     faTachometerAlt,
+    faTag,
     faTimes,
     faTimesCircle,
     faTrash,
@@ -64,6 +65,7 @@ library.add(
     faPlus,
     faSearch,
     faTachometerAlt,
+    faTag,
     faTimes,
     faTimesCircle,
     faTrash,
diff --git a/src/lang/en.json b/src/lang/en.json
index e215f1031..711d8b1eb 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -1051,5 +1051,20 @@
     "RabbitMQ Password": "RabbitMQ Password",
     "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
     "SendGrid API Key": "SendGrid API Key",
-    "Separate multiple email addresses with commas": "Separate multiple email addresses with commas"
-}
+    "Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
+    "Generate Badge": "Generate Badge",
+    "Return to not found": "Return to not found",
+    "Return to operational": "Return to operational",
+    "Return to partial down": "Return to partial down",
+    "Return to maintenance": "Return to maintenance",
+    "Return to unknown": "Return to unknown",
+    "not found": "not found",
+    "operational": "operational",
+    "partial down": "partial down",
+    "maintenance": "maintenance",
+    "unknown": "unknown",
+    "Preview": "Preview",
+    "Copy Image URL": "Copy Image URL",
+    "Image url copied to clipboard.": "Image url copied to clipboard.",
+    "Unable to copy image URL.": "Unable to copy image URL."
+}
\ No newline at end of file
diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json
index 8319c656d..8d8682502 100644
--- a/src/lang/pt-BR.json
+++ b/src/lang/pt-BR.json
@@ -994,5 +994,20 @@
     "aboutSlackUsername": "Altera o nome de exibição do remetente da mensagem. Se quiser mencionar alguém, inclua a menção no nome amigável.",
     "Send rich messages": "Enviar mensagens ricas",
     "Host Onesender": "Servidor Onesender",
-    "Token Onesender": "Chave Onesender"
-}
+    "Token Onesender": "Chave Onesender",
+    "Generate Badge": "Gerar Distintivo",
+    "Return to not found": "Retorno para não encontrado",
+    "Return to operational": "Retorno para operacional",
+    "Return to partial down": "Retorno para parcialmente degradado",
+    "Return to maintenance": "Retorno para em manutenção",
+    "Return to unknown": "Retorno para desconhecido",
+    "not found": "não encontrado",
+    "operational": "operacional",
+    "partial down": "parcialmente fora",
+    "maintenance": "manutenção",
+    "unknown": "desconhecido",
+    "Preview": "Pré-visualizar",
+    "Copy Image URL": "Copiar URL da imagem",
+    "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.",
+    "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência."
+}
\ No newline at end of file
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index 116968282..b8d585f39 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -19,7 +19,8 @@
                 <!-- Description -->
                 <div class="my-3">
                     <label for="description" class="form-label">{{ $t("Description") }}</label>
-                    <textarea id="description" v-model="config.description" class="form-control" data-testid="description-input"></textarea>
+                    <textarea id="description" v-model="config.description" class="form-control"
+                        data-testid="description-input"></textarea>
                     <div class="form-text">
                         {{ $t("markdownSupported") }}
                     </div>
@@ -28,7 +29,8 @@
                 <!-- Footer Text -->
                 <div class="my-3">
                     <label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label>
-                    <textarea id="footer-text" v-model="config.footerText" class="form-control" data-testid="footer-text-input"></textarea>
+                    <textarea id="footer-text" v-model="config.footerText" class="form-control"
+                        data-testid="footer-text-input"></textarea>
                     <div class="form-text">
                         {{ $t("markdownSupported") }}
                     </div>
@@ -36,7 +38,8 @@
 
                 <div class="my-3">
                     <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">
                     <div class="form-text">
                         {{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }}
                     </div>
@@ -52,40 +55,49 @@
                 </div>
 
                 <div class="my-3 form-check form-switch">
-                    <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox" data-testid="show-tags-checkbox">
+                    <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox"
+                        data-testid="show-tags-checkbox">
                     <label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
                 </div>
 
                 <!-- Show Powered By -->
                 <div class="my-3 form-check form-switch">
-                    <input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox" data-testid="show-powered-by-checkbox">
+                    <input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox"
+                        data-testid="show-powered-by-checkbox">
                     <label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label>
                 </div>
 
                 <!-- Show certificate expiry -->
                 <div class="my-3 form-check form-switch">
-                    <input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input" type="checkbox" data-testid="show-certificate-expiry-checkbox">
-                    <label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry") }}</label>
+                    <input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input"
+                        type="checkbox" data-testid="show-certificate-expiry-checkbox">
+                    <label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry")
+                        }}</label>
                 </div>
 
                 <div v-if="false" class="my-3">
-                    <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") }}</sup></label>
-                    <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
+                    <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon")
+                            }}</sup></label>
+                    <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password"
+                        class="form-control">
                 </div>
 
                 <!-- Domain Name List -->
                 <div class="my-3">
                     <label class="form-label">
                         {{ $t("Domain Names") }}
-                        <button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
+                        <button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')"
+                            @click="addDomainField">
                             <font-awesome-icon icon="plus-circle" class="action text-primary" />
                         </button>
                     </label>
 
                     <ul class="list-group domain-name-list">
                         <li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
-                            <input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
-                            <button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
+                            <input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input"
+                                placeholder="example.com" />
+                            <button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [domain])"
+                                @click="removeDomain(index)">
                                 <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
                             </button>
                         </li>
@@ -95,13 +107,15 @@
                 <!-- Google Analytics -->
                 <div class="my-3">
                     <label for="googleAnalyticsTag" class="form-label">{{ $t("Google Analytics ID") }}</label>
-                    <input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" data-testid="google-analytics-input">
+                    <input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control"
+                        data-testid="google-analytics-input">
                 </div>
 
                 <!-- Custom CSS -->
                 <div class="my-3">
                     <div class="mb-1">{{ $t("Custom CSS") }}</div>
-                    <prism-editor v-model="config.customCSS" class="css-editor" data-testid="custom-css-input" :highlight="highlighter" line-numbers></prism-editor>
+                    <prism-editor v-model="config.customCSS" class="css-editor" data-testid="custom-css-input"
+                        :highlight="highlighter" line-numbers></prism-editor>
                 </div>
 
                 <div class="danger-zone">
@@ -127,7 +141,7 @@
         </div>
 
         <!-- Main Status Page -->
-        <div :class="{ edit: enableEditMode}" class="main">
+        <div :class="{ edit: enableEditMode }" class="main">
             <!-- Logo & Title -->
             <h1 class="mb-4 title-flex">
                 <!-- Logo -->
@@ -138,17 +152,9 @@
 
                 <!-- Uploader -->
                 <!--    url="/api/status-page/upload-logo" -->
-                <ImageCropUpload
-                    v-model="showImageCropUpload"
-                    field="img"
-                    :width="128"
-                    :height="128"
-                    :langType="$i18n.locale"
-                    img-format="png"
-                    :noCircle="true"
-                    :noSquare="false"
-                    @crop-success="cropSuccess"
-                />
+                <ImageCropUpload v-model="showImageCropUpload" field="img" :width="128" :height="128"
+                    :langType="$i18n.locale" img-format="png" :noCircle="true" :noSquare="false"
+                    @crop-success="cropSuccess" />
 
                 <!-- Title -->
                 <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
@@ -162,14 +168,20 @@
                         {{ $t("Edit Status Page") }}
                     </button>
 
-                    <a href="/manage-status-page" class="btn btn-info">
-                        <font-awesome-icon icon="tachometer-alt" />
+                    <a href="/manage-status-page" class="btn btn-info me-2">
+                        <font-awesome-icon icon="tag" />
                         {{ $t("Go to Dashboard") }}
                     </a>
+
+                    <a href='#' @click="$refs.badgeBuilderDialog.show(slug)" class="btn btn-info">
+                        <font-awesome-icon icon="tag" />
+                        {{ $t("Generate Badge") }}
+                    </a>
                 </div>
 
                 <div v-else>
-                    <button class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button" @click="createIncident">
+                    <button class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button"
+                        @click="createIncident">
                         <font-awesome-icon icon="bullhorn" />
                         {{ $t("Create Incident") }}
                     </button>
@@ -177,28 +189,35 @@
             </div>
 
             <!-- Incident -->
-            <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass" data-testid="incident">
+            <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"
+                data-testid="incident">
                 <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
-                <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" data-testid="incident-title" />
+                <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true"
+                    class="alert-heading" data-testid="incident-title" />
 
                 <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
-                <Editable v-if="editIncidentMode" v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" />
+                <Editable v-if="editIncidentMode" v-model="incident.content" tag="div"
+                    :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" />
                 <div v-if="editIncidentMode" class="form-text">
                     {{ $t("markdownSupported") }}
                 </div>
                 <!-- eslint-disable-next-line vue/no-v-html-->
-                <div v-if="! editIncidentMode" class="content" data-testid="incident-content" v-html="incidentHTML"></div>
+                <div v-if="!editIncidentMode" class="content" data-testid="incident-content" v-html="incidentHTML">
+                </div>
 
                 <!-- Incident Date -->
                 <div class="date mt-3">
-                    {{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
+                    {{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{
+                        dateFromNow(incident.createdDate) }})<br />
                     <span v-if="incident.lastUpdatedDate">
-                        {{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
+                        {{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{
+                            dateFromNow(incident.lastUpdatedDate) }})
                     </span>
                 </div>
 
                 <div v-if="editMode" class="mt-3">
-                    <button v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button" @click="postIncident">
+                    <button v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button"
+                        @click="postIncident">
                         <font-awesome-icon icon="bullhorn" />
                         {{ $t("Post") }}
                     </button>
@@ -214,16 +233,23 @@
                     </button>
 
                     <div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
-                        <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+                        <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button"
+                            data-bs-toggle="dropdown" aria-expanded="false">
                             {{ $t("Style") }}: {{ $t(incident.style) }}
                         </button>
                         <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
+                            <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a>
+                            </li>
+                            <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning")
+                                    }}</a></li>
+                            <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger")
+                                    }}</a></li>
+                            <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary")
+                                    }}</a></li>
+                            <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light")
+                                    }}</a></li>
+                            <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a>
+                            </li>
                         </ul>
                     </div>
 
@@ -270,10 +296,8 @@
 
             <!-- Maintenance -->
             <template v-if="maintenanceList.length > 0">
-                <div
-                    v-for="maintenance in maintenanceList" :key="maintenance.id"
-                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
-                >
+                <div v-for="maintenance in maintenanceList" :key="maintenance.id"
+                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert">
                     <h4 class="alert-heading">{{ maintenance.title }}</h4>
                     <!-- eslint-disable-next-line vue/no-v-html-->
                     <div class="content" v-html="maintenanceHTML(maintenance.description)"></div>
@@ -283,9 +307,11 @@
 
             <!-- Description -->
             <strong v-if="editMode">{{ $t("Description") }}:</strong>
-            <Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" data-testid="description-editable" />
+            <Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div"
+                class="mb-4 description" data-testid="description-editable" />
             <!-- eslint-disable-next-line vue/no-v-html-->
-            <div v-if="! enableEditMode" class="alert-heading p-2" data-testid="description" v-html="descriptionHTML"></div>
+            <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="description" v-html="descriptionHTML">
+            </div>
 
             <div v-if="editMode" class="mb-4">
                 <div>
@@ -298,26 +324,20 @@
                 <div class="mt-3">
                     <div v-if="sortedMonitorList.length > 0 && loadedData">
                         <label>{{ $t("Add a monitor") }}:</label>
-                        <VueMultiselect
-                            v-model="selectedMonitor"
-                            :options="sortedMonitorList"
-                            :multiple="false"
-                            :searchable="true"
-                            :placeholder="$t('Add a monitor')"
-                            label="name"
-                            trackBy="name"
-                            class="mt-3"
-                            data-testid="monitor-select"
-                        >
+                        <VueMultiselect v-model="selectedMonitor" :options="sortedMonitorList" :multiple="false"
+                            :searchable="true" :placeholder="$t('Add a monitor')" label="name" trackBy="name"
+                            class="mt-3" data-testid="monitor-select">
                             <template #option="{ option }">
                                 <div class="d-inline-flex">
-                                    <span>{{ option.pathName }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span>
+                                    <span>{{ option.pathName }}
+                                        <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" />
+                                    </span>
                                 </div>
                             </template>
                         </VueMultiselect>
                     </div>
                     <div v-else class="text-center">
-                        {{ $t("No monitors available.") }}  <router-link to="/add">{{ $t("Add one") }}</router-link>
+                        {{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
                     </div>
                 </div>
             </div>
@@ -328,29 +348,35 @@
                     👀 {{ $t("statusPageNothing") }}
                 </div>
 
-                <PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" :show-certificate-expiry="config.showCertificateExpiry" />
+                <PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags"
+                    :show-certificate-expiry="config.showCertificateExpiry" />
             </div>
 
             <footer class="mt-5 mb-4">
                 <div class="custom-footer-text text-start">
                     <strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
                 </div>
-                <Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" />
+                <Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode"
+                    :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" />
                 <!-- eslint-disable-next-line vue/no-v-html-->
-                <div v-if="! enableEditMode" class="alert-heading p-2" data-testid="footer-text" v-html="footerHTML"></div>
+                <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="footer-text" v-html="footerHTML">
+                </div>
 
                 <p v-if="config.showPoweredBy" data-testid="powered-by">
-                    {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
+                    {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer"
+                        href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma") }}</a>
                 </p>
 
                 <div class="refresh-info mb-2">
-                    <div>{{ $t("Last Updated") }}:  {{ lastUpdateTimeDisplay }}</div>
-                    <div data-testid="update-countdown-text">{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div>
+                    <div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div>
+                    <div data-testid="update-countdown-text">{{ $tc("statusPageRefreshIn", [updateCountdownText]) }}
+                    </div>
                 </div>
             </footer>
         </div>
 
-        <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
+        <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')"
+            @yes="deleteStatusPage">
             {{ $t("deleteStatusPageMsg") }}
         </Confirm>
 
@@ -358,6 +384,8 @@
             {{ config.customCSS }}
         </component>
     </div>
+
+    <BadgeBuilderDialog ref="badgeBuilderDialog" />
 </template>
 
 <script>
@@ -383,6 +411,7 @@ import { getResBaseURL } from "../util-frontend";
 import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
 import Tag from "../components/Tag.vue";
 import VueMultiselect from "vue-multiselect";
+import BadgeBuilderDialog from "../components/BadgeBuilderDialog.vue";
 
 const toast = useToast();
 dayjs.extend(duration);
@@ -405,6 +434,7 @@ export default {
         PrismEditor,
         MaintenanceTime,
         Tag,
+        BadgeBuilderDialog,
         VueMultiselect
     },
 
@@ -471,7 +501,7 @@ export default {
             let result = [];
 
             for (let id in this.$root.monitorList) {
-                if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) {
+                if (this.$root.monitorList[id] && !(id in this.$root.publicMonitorList)) {
                     let monitor = this.$root.monitorList[id];
                     result.push(monitor);
                 }
@@ -555,7 +585,7 @@ export default {
                 }
             }
 
-            if (! hasUp) {
+            if (!hasUp) {
                 status = STATUS_PAGE_ALL_DOWN;
             }
 
@@ -723,7 +753,7 @@ export default {
             }, (this.config.autoRefreshInterval + 10) * 1000);
 
             this.updateUpdateTimer();
-        }).catch( function (error) {
+        }).catch(function (error) {
             if (error.response.status === 404) {
                 location.href = "/page-not-found";
             }
@@ -770,7 +800,7 @@ export default {
          */
         updateHeartbeatList() {
             // If editMode, it will use the data from websocket.
-            if (! this.editMode) {
+            if (!this.editMode) {
                 axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
                     const { heartbeatList, uptimeList } = res.data;
 
@@ -1267,5 +1297,4 @@ footer {
 .refresh-info {
     opacity: 0.7;
 }
-
 </style>

From a9e59fded62bc7b37589e3305cc18e810d0b642f Mon Sep 17 00:00:00 2001
From: Hiago Silva Souza <hiasilva@gmail.com>
Date: Sat, 25 Jan 2025 00:36:59 -0300
Subject: [PATCH 2/5] fix: lint problems

---
 server/model/status_page.js           |  81 ++++++++---
 server/routers/status-page-router.js  |   4 +-
 src/components/BadgeBuilderDialog.vue |  70 ++++-----
 src/pages/StatusPage.vue              | 195 +++++++++++++++++---------
 4 files changed, 228 insertions(+), 122 deletions(-)

diff --git a/server/model/status_page.js b/server/model/status_page.js
index 08a69154c..66a226879 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -63,19 +63,19 @@ class StatusPage extends BeanModel {
     }
 
     /**
-     * Handle responses to status page
-     * @param {Response} response Response object
-     * @param {string} slug Status page slug
-     * @param {object} config Config for badge
-     * @returns {Promise<void>}
+     * Generate SVG Status
+     * @param {response} response express response
+     * @param {slug} slug from router
+     * @param {config} config config for the svg
+     * @returns {Promise<string>} returns a svg file
      */
     static async handleStatusPageSVGResponse(response, slug, config) {
-        slug = slug.replace('.svg', '')
+        slug = slug.replace(".svg", "");
         let statusPage = await R.findOne("status_page", " slug = ? ", [
-            slug.replace('.svg', '')
+            slug.replace(".svg", "")
         ]);
         if (statusPage) {
-            response.set('Content-Type', 'image/svg+xml')
+            response.set("Content-Type", "image/svg+xml");
             response.send(await StatusPage.renderSVG(slug, config, statusPage));
         } else {
             response.status(404).send(await StatusPage.renderSVG(slug, config, null));
@@ -173,15 +173,28 @@ class StatusPage extends BeanModel {
         return $.root().html();
     }
 
+    /**
+     * render SVG file
+     * @param {slug} slug from router
+     * @param {userConfig} userConfig generated by user
+     * @param {statusPage} statusPage object
+     * @returns {Promise<string>} a SVG string
+     */
     static async renderSVG(slug, userConfig = {}, statusPage) {
-        let allowedBadgeStyles = ['flat', 'flat-square', 'for-the-badge', 'plastic', 'social'];
+        let allowedBadgeStyles = [ "flat", "flat-square", "for-the-badge", "plastic", "social" ];
         if (!allowedBadgeStyles.includes(userConfig.badgeStyle)) {
-            userConfig.badgeStyle = 'flat'
+            userConfig.badgeStyle = "flat";
         }
 
         // returns unknown svg
         if (statusPage === null) {
-            return makeBadge({ message: userConfig.notFoundText || 'not found', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' })
+            return makeBadge({
+                message: userConfig.notFoundText || "not found",
+                color: "red",
+                style: userConfig.badgeStyle,
+                label: slug,
+                labelColor: "gray"
+            });
         } else {
 
             // get all heartbeats that correspond to this statusPage
@@ -199,7 +212,7 @@ class StatusPage extends BeanModel {
             for (let groupBean of list) {
                 let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
                 for (const monitor of monitorGroup.monitorList) {
-                    const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]);
+                    const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
                     if (heartbeat) {
                         heartbeats.push({
                             ...monitor,
@@ -215,15 +228,45 @@ class StatusPage extends BeanModel {
 
             switch (status) {
                 case STATUS_PAGE_ALL_DOWN:
-                    return makeBadge({ message: userConfig.allDownText ?? 'all down', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                    return makeBadge({
+                        message: userConfig.allDownText ?? "all down",
+                        color: "red",
+                        style: userConfig.badgeStyle,
+                        label: slug,
+                        labelColor: "gray"
+                    });
                 case STATUS_PAGE_ALL_UP:
-                    return makeBadge({ message: userConfig.allOperationalText ?? 'all operational', color: 'green', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                    return makeBadge({
+                        message: userConfig.allOperationalText ?? "all operational",
+                        color: "green",
+                        style: userConfig.badgeStyle,
+                        label: slug,
+                        labelColor: "gray"
+                    });
                 case STATUS_PAGE_PARTIAL_DOWN:
-                    return makeBadge({ message: userConfig.partialDownText ?? 'partial down', color: 'orange', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                    return makeBadge({
+                        message: userConfig.partialDownText ?? "partial down",
+                        color: "orange",
+                        style: userConfig.badgeStyle,
+                        label: slug,
+                        labelColor: "gray"
+                    });
                 case STATUS_PAGE_MAINTENANCE:
-                    return makeBadge({ message: userConfig.maintenanceText || 'maintenance', color: 'yellow', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                    return makeBadge({
+                        message: userConfig.maintenanceText || "maintenance",
+                        color: "yellow",
+                        style: userConfig.badgeStyle,
+                        label: slug,
+                        labelColor: "gray"
+                    });
                 default:
-                    return makeBadge({ message: userConfig.unknownText || 'unknown', color: 'lightgray', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' });
+                    return makeBadge({
+                        message: userConfig.unknownText || "unknown",
+                        color: "lightgray",
+                        style: userConfig.badgeStyle,
+                        label: slug,
+                        labelColor: "gray"
+                    });
             }
         }
 
@@ -308,7 +351,7 @@ class StatusPage extends BeanModel {
         for (let groupBean of list) {
             let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
             for (const monitor of monitorGroup.monitorList) {
-                const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]);
+                const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
                 if (heartbeat) {
                     heartbeats.push({
                         ...monitor,
@@ -548,7 +591,7 @@ class StatusPage extends BeanModel {
                 SELECT DISTINCT maintenance_id
                 FROM maintenance_status_page
                 WHERE status_page_id = ?
-            `, [statusPageId]);
+            `, [ statusPageId ]);
 
             for (const maintenanceID of maintenanceIDList) {
                 let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js
index 9c5b88101..c97b12d44 100644
--- a/server/routers/status-page-router.js
+++ b/server/routers/status-page-router.js
@@ -17,8 +17,8 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
     let slug = request.params.slug;
     slug = slug.toLowerCase();
 
-    if (slug.endsWith('.svg')) {
-        await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || '{}'));
+    if (slug.endsWith(".svg")) {
+        await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || "{}"));
     } else {
         await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
     }
diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue
index 8b4f9cc79..5e79a1f6f 100644
--- a/src/components/BadgeBuilderDialog.vue
+++ b/src/components/BadgeBuilderDialog.vue
@@ -23,50 +23,56 @@
 
                         <div class="mb-3">
                             <label for="badge-not-found-text" class="form-label">{{ $t("Return to not found") }}</label>
-                            <input id="badge-not-found-text" v-model="config.notFoundText" type="text"
-                                class="form-control">
+                            <input
+                                id="badge-not-found-text" v-model="config.notFoundText" type="text"
+                                class="form-control"
+                            >
                         </div>
 
                         <div class="mb-3">
                             <label for="badge-operational-text" class="form-label">{{ $t("Return to operational")
-                                }}</label>
-                            <input id="badge-operational-text" v-model="config.allOperationalText" type="text"
-                                class="form-control">
+                            }}</label>
+                            <input
+                                id="badge-operational-text" v-model="config.allOperationalText" type="text"
+                                class="form-control"
+                            >
                         </div>
 
-
                         <div class="mb-3">
                             <label for="badge-partial-down-text" class="form-label">{{ $t("Return to partial down")
-                                }}</label>
-                            <input id="badge-partial-down-text" v-model="config.partialDownText" type="text"
-                                class="form-control">
+                            }}</label>
+                            <input
+                                id="badge-partial-down-text" v-model="config.partialDownText" type="text"
+                                class="form-control"
+                            >
                         </div>
 
                         <div class="mb-3">
                             <label for="badge-maintenance-text" class="form-label">{{ $t("Return to maintenance")
-                                }}</label>
-                            <input id="badge-maintenance-text" v-model="config.maintenanceText" type="text"
-                                class="form-control">
+                            }}</label>
+                            <input
+                                id="badge-maintenance-text" v-model="config.maintenanceText" type="text"
+                                class="form-control"
+                            >
                         </div>
 
                         <div class="mb-3">
                             <label for="badge-unknown-text" class="form-label">{{ $t("Return to unknown")
-                                }}</label>
-                            <input id="badge-unknown-text" v-model="config.unknownText" type="text"
-                                class="form-control">
+                            }}</label>
+                            <input
+                                id="badge-unknown-text" v-model="config.unknownText" type="text"
+                                class="form-control"
+                            >
                         </div>
 
-                        <div class="Preview" v-if="slug">
+                        <div v-if="slug" class="Preview">
                             <div>{{ $t("Preview") }}</div>
                             <img :src="getSvgURL(true)" class="mt-2" />
                         </div>
-
                     </div>
 
-
-
                     <div class="modal-footer">
-                        <button type="button" @click="copyImageURL()" class="btn btn-primary">
+                        <button type="button" class="btn btn-primary" @click="copyImageURL()">
                             <!-- <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> -->
                             {{ $t("Copy Image URL") }}
                         </button>
@@ -75,13 +81,12 @@
             </div>
         </div>
     </form>
-
 </template>
 
 <script>
 import { Modal } from "bootstrap";
 import { useToast } from "vue-toastification";
-import axios from 'axios';
+import axios from "axios";
 const toast = useToast();
 
 export default {
@@ -93,13 +98,13 @@ export default {
             model: null,
             slug: null,
             config: {
-                badgeStyle: 'flat',
-                notFoundText: this.$t('not found'),
-                allDownText: this.$t('all down'),
-                allOperationalText: this.$t('operational'),
-                partialDownText: this.$t('partial down'),
-                maintenanceText: this.$t('maintenance'),
-                unknownText: this.$t('unknown')
+                badgeStyle: "flat",
+                notFoundText: this.$t("not found"),
+                allDownText: this.$t("all down"),
+                allOperationalText: this.$t("operational"),
+                partialDownText: this.$t("partial down"),
+                maintenanceText: this.$t("maintenance"),
+                unknownText: this.$t("unknown")
             }
         };
     },
@@ -114,18 +119,13 @@ export default {
         this.modal = new Modal(this.$refs.modal);
     },
     methods: {
-        /**
-         * Show settings for specified notification
-         * @param {number} notificationID ID of notification to show
-         * @returns {void}
-         */
         show(slug) {
             this.slug = slug;
             this.modal.show();
         },
 
         getSvgURL(disableCache = false) {
-            return axios.defaults.baseURL + '/status/' + this.slug + '.svg?config=' + encodeURIComponent(JSON.stringify(this.config)).replace(/%5B/g, '[').replace(/%5D/g, ']') + (disableCache ? '&noCache=' + Date.now() : '');
+            return axios.defaults.baseURL + "/status/" + this.slug + ".svg?config=" + encodeURIComponent(JSON.stringify(this.config)).replace(/%5B/g, "[").replace(/%5D/g, "]") + (disableCache ? "&noCache=" + Date.now() : "");
         },
 
         copyImageURL() {
diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue
index b8d585f39..601f9e869 100644
--- a/src/pages/StatusPage.vue
+++ b/src/pages/StatusPage.vue
@@ -19,8 +19,10 @@
                 <!-- Description -->
                 <div class="my-3">
                     <label for="description" class="form-label">{{ $t("Description") }}</label>
-                    <textarea id="description" v-model="config.description" class="form-control"
-                        data-testid="description-input"></textarea>
+                    <textarea
+                        id="description" v-model="config.description" class="form-control"
+                        data-testid="description-input"
+                    ></textarea>
                     <div class="form-text">
                         {{ $t("markdownSupported") }}
                     </div>
@@ -29,8 +31,10 @@
                 <!-- Footer Text -->
                 <div class="my-3">
                     <label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label>
-                    <textarea id="footer-text" v-model="config.footerText" class="form-control"
-                        data-testid="footer-text-input"></textarea>
+                    <textarea
+                        id="footer-text" v-model="config.footerText" class="form-control"
+                        data-testid="footer-text-input"
+                    ></textarea>
                     <div class="form-text">
                         {{ $t("markdownSupported") }}
                     </div>
@@ -38,8 +42,10 @@
 
                 <div class="my-3">
                     <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"
+                    >
                     <div class="form-text">
                         {{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }}
                     </div>
@@ -55,49 +61,63 @@
                 </div>
 
                 <div class="my-3 form-check form-switch">
-                    <input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox"
-                        data-testid="show-tags-checkbox">
+                    <input
+                        id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox"
+                        data-testid="show-tags-checkbox"
+                    >
                     <label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
                 </div>
 
                 <!-- Show Powered By -->
                 <div class="my-3 form-check form-switch">
-                    <input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox"
-                        data-testid="show-powered-by-checkbox">
+                    <input
+                        id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox"
+                        data-testid="show-powered-by-checkbox"
+                    >
                     <label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label>
                 </div>
 
                 <!-- Show certificate expiry -->
                 <div class="my-3 form-check form-switch">
-                    <input id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input"
-                        type="checkbox" data-testid="show-certificate-expiry-checkbox">
+                    <input
+                        id="show-certificate-expiry" v-model="config.showCertificateExpiry" class="form-check-input"
+                        type="checkbox" data-testid="show-certificate-expiry-checkbox"
+                    >
                     <label class="form-check-label" for="show-certificate-expiry">{{ $t("showCertificateExpiry")
-                        }}</label>
+                    }}</label>
                 </div>
 
                 <div v-if="false" class="my-3">
                     <label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon")
-                            }}</sup></label>
-                    <input id="password" v-model="config.password" disabled type="password" autocomplete="new-password"
-                        class="form-control">
+                    }}</sup></label>
+                    <input
+                        id="password" v-model="config.password" disabled type="password" autocomplete="new-password"
+                        class="form-control"
+                    >
                 </div>
 
                 <!-- Domain Name List -->
                 <div class="my-3">
                     <label class="form-label">
                         {{ $t("Domain Names") }}
-                        <button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')"
-                            @click="addDomainField">
+                        <button
+                            class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')"
+                            @click="addDomainField"
+                        >
                             <font-awesome-icon icon="plus-circle" class="action text-primary" />
                         </button>
                     </label>
 
                     <ul class="list-group domain-name-list">
                         <li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
-                            <input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input"
-                                placeholder="example.com" />
-                            <button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [domain])"
-                                @click="removeDomain(index)">
+                            <input
+                                v-model="config.domainNameList[index]" type="text" class="no-bg domain-input"
+                                placeholder="example.com"
+                            />
+                            <button
+                                class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [domain])"
+                                @click="removeDomain(index)"
+                            >
                                 <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
                             </button>
                         </li>
@@ -107,15 +127,19 @@
                 <!-- Google Analytics -->
                 <div class="my-3">
                     <label for="googleAnalyticsTag" class="form-label">{{ $t("Google Analytics ID") }}</label>
-                    <input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control"
-                        data-testid="google-analytics-input">
+                    <input
+                        id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control"
+                        data-testid="google-analytics-input"
+                    >
                 </div>
 
                 <!-- Custom CSS -->
                 <div class="my-3">
                     <div class="mb-1">{{ $t("Custom CSS") }}</div>
-                    <prism-editor v-model="config.customCSS" class="css-editor" data-testid="custom-css-input"
-                        :highlight="highlighter" line-numbers></prism-editor>
+                    <prism-editor
+                        v-model="config.customCSS" class="css-editor" data-testid="custom-css-input"
+                        :highlight="highlighter" line-numbers
+                    ></prism-editor>
                 </div>
 
                 <div class="danger-zone">
@@ -152,9 +176,11 @@
 
                 <!-- Uploader -->
                 <!--    url="/api/status-page/upload-logo" -->
-                <ImageCropUpload v-model="showImageCropUpload" field="img" :width="128" :height="128"
+                <ImageCropUpload
+                    v-model="showImageCropUpload" field="img" :width="128" :height="128"
                     :langType="$i18n.locale" img-format="png" :noCircle="true" :noSquare="false"
-                    @crop-success="cropSuccess" />
+                    @crop-success="cropSuccess"
+                />
 
                 <!-- Title -->
                 <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
@@ -173,15 +199,17 @@
                         {{ $t("Go to Dashboard") }}
                     </a>
 
-                    <a href='#' @click="$refs.badgeBuilderDialog.show(slug)" class="btn btn-info">
+                    <a href="#" class="btn btn-info" @click="$refs.badgeBuilderDialog.show(slug)">
                         <font-awesome-icon icon="tag" />
                         {{ $t("Generate Badge") }}
                     </a>
                 </div>
 
                 <div v-else>
-                    <button class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button"
-                        @click="createIncident">
+                    <button
+                        class="btn btn-primary btn-add-group me-2" data-testid="create-incident-button"
+                        @click="createIncident"
+                    >
                         <font-awesome-icon icon="bullhorn" />
                         {{ $t("Create Incident") }}
                     </button>
@@ -189,15 +217,21 @@
             </div>
 
             <!-- Incident -->
-            <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"
-                data-testid="incident">
+            <div
+                v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"
+                data-testid="incident"
+            >
                 <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
-                <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true"
-                    class="alert-heading" data-testid="incident-title" />
+                <Editable
+                    v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true"
+                    class="alert-heading" data-testid="incident-title"
+                />
 
                 <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
-                <Editable v-if="editIncidentMode" v-model="incident.content" tag="div"
-                    :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable" />
+                <Editable
+                    v-if="editIncidentMode" v-model="incident.content" tag="div"
+                    :contenteditable="editIncidentMode" class="content" data-testid="incident-content-editable"
+                />
                 <div v-if="editIncidentMode" class="form-text">
                     {{ $t("markdownSupported") }}
                 </div>
@@ -216,8 +250,10 @@
                 </div>
 
                 <div v-if="editMode" class="mt-3">
-                    <button v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button"
-                        @click="postIncident">
+                    <button
+                        v-if="editIncidentMode" class="btn btn-light me-2" data-testid="post-incident-button"
+                        @click="postIncident"
+                    >
                         <font-awesome-icon icon="bullhorn" />
                         {{ $t("Post") }}
                     </button>
@@ -233,22 +269,34 @@
                     </button>
 
                     <div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
-                        <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button"
-                            data-bs-toggle="dropdown" aria-expanded="false">
+                        <button
+                            id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button"
+                            data-bs-toggle="dropdown" aria-expanded="false"
+                        >
                             {{ $t("Style") }}: {{ $t(incident.style) }}
                         </button>
                         <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a>
+                            <li>
+                                <a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a>
                             </li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning")
-                                    }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger")
-                                    }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary")
-                                    }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light")
-                                    }}</a></li>
-                            <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a>
+                            <li>
+                                <a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning")
+                                }}</a>
+                            </li>
+                            <li>
+                                <a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger")
+                                }}</a>
+                            </li>
+                            <li>
+                                <a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary")
+                                }}</a>
+                            </li>
+                            <li>
+                                <a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light")
+                                }}</a>
+                            </li>
+                            <li>
+                                <a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a>
                             </li>
                         </ul>
                     </div>
@@ -296,8 +344,10 @@
 
             <!-- Maintenance -->
             <template v-if="maintenanceList.length > 0">
-                <div v-for="maintenance in maintenanceList" :key="maintenance.id"
-                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert">
+                <div
+                    v-for="maintenance in maintenanceList" :key="maintenance.id"
+                    class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
+                >
                     <h4 class="alert-heading">{{ maintenance.title }}</h4>
                     <!-- eslint-disable-next-line vue/no-v-html-->
                     <div class="content" v-html="maintenanceHTML(maintenance.description)"></div>
@@ -307,8 +357,10 @@
 
             <!-- Description -->
             <strong v-if="editMode">{{ $t("Description") }}:</strong>
-            <Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div"
-                class="mb-4 description" data-testid="description-editable" />
+            <Editable
+                v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div"
+                class="mb-4 description" data-testid="description-editable"
+            />
             <!-- eslint-disable-next-line vue/no-v-html-->
             <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="description" v-html="descriptionHTML">
             </div>
@@ -324,9 +376,11 @@
                 <div class="mt-3">
                     <div v-if="sortedMonitorList.length > 0 && loadedData">
                         <label>{{ $t("Add a monitor") }}:</label>
-                        <VueMultiselect v-model="selectedMonitor" :options="sortedMonitorList" :multiple="false"
+                        <VueMultiselect
+                            v-model="selectedMonitor" :options="sortedMonitorList" :multiple="false"
                             :searchable="true" :placeholder="$t('Add a monitor')" label="name" trackBy="name"
-                            class="mt-3" data-testid="monitor-select">
+                            class="mt-3" data-testid="monitor-select"
+                        >
                             <template #option="{ option }">
                                 <div class="d-inline-flex">
                                     <span>{{ option.pathName }}
@@ -348,35 +402,44 @@
                     👀 {{ $t("statusPageNothing") }}
                 </div>
 
-                <PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags"
-                    :show-certificate-expiry="config.showCertificateExpiry" />
+                <PublicGroupList
+                    :edit-mode="enableEditMode" :show-tags="config.showTags"
+                    :show-certificate-expiry="config.showCertificateExpiry"
+                />
             </div>
 
             <footer class="mt-5 mb-4">
                 <div class="custom-footer-text text-start">
                     <strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
                 </div>
-                <Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode"
-                    :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable" />
+                <Editable
+                    v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode"
+                    :noNL="false" class="alert-heading p-2" data-testid="custom-footer-editable"
+                />
                 <!-- eslint-disable-next-line vue/no-v-html-->
                 <div v-if="!enableEditMode" class="alert-heading p-2" data-testid="footer-text" v-html="footerHTML">
                 </div>
 
                 <p v-if="config.showPoweredBy" data-testid="powered-by">
-                    {{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer"
-                        href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma") }}</a>
+                    {{ $t("Powered by") }} <a
+                        target="_blank" rel="noopener noreferrer"
+                        href="https://github.com/louislam/uptime-kuma"
+                    >{{ $t("Uptime Kuma") }}</a>
                 </p>
 
                 <div class="refresh-info mb-2">
                     <div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div>
-                    <div data-testid="update-countdown-text">{{ $tc("statusPageRefreshIn", [updateCountdownText]) }}
+                    <div data-testid="update-countdown-text">
+                        {{ $tc("statusPageRefreshIn", [updateCountdownText]) }}
                     </div>
                 </div>
             </footer>
         </div>
 
-        <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')"
-            @yes="deleteStatusPage">
+        <Confirm
+            ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')"
+            @yes="deleteStatusPage"
+        >
             {{ $t("deleteStatusPageMsg") }}
         </Confirm>
 

From 4fb371bd262798798ae4abed05813629cf0105ae Mon Sep 17 00:00:00 2001
From: Hiago Silva Souza <hiasilva@gmail.com>
Date: Sat, 25 Jan 2025 00:40:56 -0300
Subject: [PATCH 3/5] fix: lint problems

---
 src/components/BadgeBuilderDialog.vue | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue
index 5e79a1f6f..4b70ec512 100644
--- a/src/components/BadgeBuilderDialog.vue
+++ b/src/components/BadgeBuilderDialog.vue
@@ -144,7 +144,6 @@ export default {
 @import "../assets/vars.scss";
 
 .dark {
-
     .modal-dialog .form-text,
     .modal-dialog p {
         color: $dark-font-color;

From 56ae3f086844924977582cc4c188c6f522c13f40 Mon Sep 17 00:00:00 2001
From: Hiago Silva Souza <hiasilva@gmail.com>
Date: Sat, 25 Jan 2025 00:46:42 -0300
Subject: [PATCH 4/5] fix: lang files formatation

---
 src/lang/en.json    | 2 +-
 src/lang/pt-BR.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/lang/en.json b/src/lang/en.json
index 711d8b1eb..eecacbf99 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -1067,4 +1067,4 @@
     "Copy Image URL": "Copy Image URL",
     "Image url copied to clipboard.": "Image url copied to clipboard.",
     "Unable to copy image URL.": "Unable to copy image URL."
-}
\ No newline at end of file
+}
diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json
index 8d8682502..ab4d705c2 100644
--- a/src/lang/pt-BR.json
+++ b/src/lang/pt-BR.json
@@ -1010,4 +1010,4 @@
     "Copy Image URL": "Copiar URL da imagem",
     "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.",
     "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência."
-}
\ No newline at end of file
+}

From c4f4548a40a89103b70a63d573c3fc0f1dd9bed6 Mon Sep 17 00:00:00 2001
From: Hiago Silva Souza <hiasilva@gmail.com>
Date: Sat, 25 Jan 2025 07:53:37 -0300
Subject: [PATCH 5/5] fix: revert file to last commit to follow guide lines

---
 src/lang/pt-BR.json | 17 +----------------
 1 file changed, 1 insertion(+), 16 deletions(-)

diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json
index ab4d705c2..8319c656d 100644
--- a/src/lang/pt-BR.json
+++ b/src/lang/pt-BR.json
@@ -994,20 +994,5 @@
     "aboutSlackUsername": "Altera o nome de exibição do remetente da mensagem. Se quiser mencionar alguém, inclua a menção no nome amigável.",
     "Send rich messages": "Enviar mensagens ricas",
     "Host Onesender": "Servidor Onesender",
-    "Token Onesender": "Chave Onesender",
-    "Generate Badge": "Gerar Distintivo",
-    "Return to not found": "Retorno para não encontrado",
-    "Return to operational": "Retorno para operacional",
-    "Return to partial down": "Retorno para parcialmente degradado",
-    "Return to maintenance": "Retorno para em manutenção",
-    "Return to unknown": "Retorno para desconhecido",
-    "not found": "não encontrado",
-    "operational": "operacional",
-    "partial down": "parcialmente fora",
-    "maintenance": "manutenção",
-    "unknown": "desconhecido",
-    "Preview": "Pré-visualizar",
-    "Copy Image URL": "Copiar URL da imagem",
-    "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.",
-    "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência."
+    "Token Onesender": "Chave Onesender"
 }