Skip to content

Commit 996de03

Browse files
authored
🔀 Merge pull request #1932 from ga-lep/FEATURE/1924_uptime-kuma-status-page-widget
✨ Add Uptime Kuma Status Page Widget
2 parents 10d8559 + cea9b1e commit 996de03

File tree

3 files changed

+235
-0
lines changed

3 files changed

+235
-0
lines changed

‎docs/widgets.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
7272
- [Drone CI Build](#drone-ci-builds)
7373
- [Linkding](#linkding)
7474
- [Uptime Kuma](#uptime-kuma)
75+
- [Uptime Kuma Status Page](#uptime-kuma-status-page)
7576
- [Tactical RMM](#tactical-rmm)
7677
- **[System Resource Monitoring](#system-resource-monitoring)**
7778
- [CPU Usage Current](#current-cpu-usage)
@@ -2671,6 +2672,40 @@ Linkding is a self-hosted bookmarking service, which has a clean interface and i
26712672

26722673
---
26732674

2675+
### Uptime Kuma Status Page
2676+
2677+
[Uptime Kuma](https://github.com/louislam/uptime-kuma) is an easy-to-use self-hosted monitoring tool.
2678+
2679+
#### Options
2680+
2681+
| **Field** | **Type** | **Required** | **Description** |
2682+
| ------------------ | -------- | ------------ | --------------------------------------------------------------------------------- |
2683+
| **`host`** | `string` | Required | The URL of the Uptime Kuma instance |
2684+
| **`slug`** | `string` | Required | The slug of the status page |
2685+
| **`monitorNames`** | `strins` | _Optional_ | Names of monitored services (in the same order as on the kuma uptime status page) |
2686+
2687+
#### Example
2688+
2689+
```yaml
2690+
- type: uptime-kuma-status-page
2691+
options:
2692+
host: http://localhost:3001
2693+
slug: another-beautiful-status-page
2694+
monitorNames:
2695+
- "Name1"
2696+
- "Name2"
2697+
```
2698+
2699+
#### Info
2700+
2701+
- **CORS**: 🟢 Enabled
2702+
- **Auth**: 🟢 Not Needed
2703+
- **Price**: 🟢 Free
2704+
- **Host**: Self-Hosted (see [Uptime Kuma](https://github.com/louislam/uptime-kuma) )
2705+
- **Privacy**: _See [Uptime Kuma](https://github.com/louislam/uptime-kuma)_
2706+
2707+
---
2708+
26742709
### Tactical RMM
26752710

26762711
[Tactical RMM](https://github.com/amidaware/tacticalrmm) is a self-hosted remote monitoring & management tool.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<template>
2+
<div @click="openStatusPage" class="clickable-widget">
3+
<template v-if="errorMessage">
4+
<div class="error-message">
5+
<span class="text">{{ errorMessage }}</span>
6+
</div>
7+
</template>
8+
<template v-else-if="lastHeartbeats">
9+
<div
10+
v-for="(heartbeat, index) in lastHeartbeats"
11+
:key="index"
12+
class="item-wrapper"
13+
>
14+
<div class="item monitor-row">
15+
<div class="title-title">
16+
<span class="text">
17+
{{
18+
monitorNames && monitorNames[index]
19+
? monitorNames[index]
20+
: `Monitor ${index + 1}`
21+
}}
22+
</span>
23+
</div>
24+
<div class="monitors-container">
25+
<div class="status-container">
26+
<span
27+
class="status-pill"
28+
:class="getStatusClass(heartbeat.status)"
29+
>
30+
{{ getStatusText(heartbeat.status) }}
31+
</span>
32+
</div>
33+
</div>
34+
</div>
35+
</div>
36+
</template>
37+
</div>
38+
</template>
39+
40+
<script>
41+
import WidgetMixin from '@/mixins/WidgetMixin';
42+
43+
export default {
44+
mixins: [WidgetMixin],
45+
data() {
46+
return {
47+
lastHeartbeats: null,
48+
errorMessage: null,
49+
errorMessageConstants: {
50+
missingHost: 'No host set',
51+
missingSlug: 'No slug set',
52+
},
53+
};
54+
},
55+
computed: {
56+
host() {
57+
return this.parseAsEnvVar(this.options.host);
58+
},
59+
slug() {
60+
return this.parseAsEnvVar(this.options.slug);
61+
},
62+
monitorNames() {
63+
return this.options.monitorNames || [];
64+
},
65+
endpoint() {
66+
return `${this.host}/api/status-page/heartbeat/${this.slug}`;
67+
},
68+
statusPageUrl() {
69+
return `${this.host}/status/${this.slug}`;
70+
},
71+
},
72+
mounted() {
73+
this.fetchData();
74+
},
75+
methods: {
76+
update() {
77+
this.startLoading();
78+
this.fetchData();
79+
},
80+
fetchData() {
81+
const { host, slug } = this;
82+
if (!this.optionsValid({ host, slug })) {
83+
return;
84+
}
85+
this.makeRequest(this.endpoint)
86+
.then(this.processData)
87+
.catch((error) => {
88+
this.errorMessage = error.message || 'Failed to fetch data';
89+
});
90+
},
91+
processData(response) {
92+
const { heartbeatList } = response;
93+
const lastHeartbeats = [];
94+
// Use Object.keys to safely iterate over heartbeatList
95+
Object.keys(heartbeatList).forEach((monitorId) => {
96+
const heartbeats = heartbeatList[monitorId];
97+
if (heartbeats.length > 0) {
98+
const lastHeartbeat = heartbeats[heartbeats.length - 1];
99+
lastHeartbeats.push(lastHeartbeat);
100+
}
101+
});
102+
this.lastHeartbeats = lastHeartbeats;
103+
},
104+
optionsValid({ host, slug }) {
105+
const errors = [];
106+
if (!host) errors.push(this.errorMessageConstants.missingHost);
107+
if (!slug) errors.push(this.errorMessageConstants.missingSlug);
108+
if (errors.length > 0) {
109+
this.errorMessage = errors.join('\n');
110+
return false;
111+
}
112+
return true;
113+
},
114+
openStatusPage() {
115+
window.open(this.statusPageUrl, '_blank');
116+
},
117+
getStatusText(status) {
118+
switch (status) {
119+
case 1:
120+
return 'Up';
121+
case 0:
122+
return 'Down';
123+
case 2:
124+
return 'Pending';
125+
case 3:
126+
return 'Maintenance';
127+
default:
128+
return 'Unknown';
129+
}
130+
},
131+
getStatusClass(status) {
132+
switch (status) {
133+
case 1:
134+
return 'up';
135+
case 0:
136+
return 'down';
137+
case 2:
138+
return 'pending';
139+
case 3:
140+
return 'maintenance';
141+
default:
142+
return 'unknown';
143+
}
144+
},
145+
},
146+
};
147+
</script>
148+
149+
<style scoped lang="scss">
150+
.clickable-widget {
151+
cursor: pointer;
152+
}
153+
.status-pill {
154+
border-radius: 50em;
155+
box-sizing: border-box;
156+
font-size: 0.75em;
157+
display: inline-block;
158+
font-weight: 700;
159+
text-align: center;
160+
white-space: nowrap;
161+
vertical-align: baseline;
162+
padding: 0.35em 0.65em;
163+
margin: 0.1em 0.5em;
164+
min-width: 64px;
165+
&.up {
166+
background-color: #5cdd8b;
167+
color: black;
168+
}
169+
&.down {
170+
background-color: #dc3545;
171+
color: white;
172+
}
173+
&.pending {
174+
background-color: #f8a306;
175+
color: black;
176+
}
177+
&.maintenance {
178+
background-color: #1747f5;
179+
color: white;
180+
}
181+
&.unknown {
182+
background-color: gray;
183+
color: white;
184+
}
185+
}
186+
.monitor-row {
187+
display: flex;
188+
justify-content: space-between;
189+
padding: 0.35em 0.5em;
190+
align-items: center;
191+
}
192+
.title-title {
193+
font-weight: bold;
194+
}
195+
.error-message {
196+
color: red;
197+
font-weight: bold;
198+
}
199+
</style>

‎src/components/Widgets/WidgetBase.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const COMPAT = {
123123
'tfl-status': 'TflStatus',
124124
trmm: 'TacticalRMM',
125125
'uptime-kuma': 'UptimeKuma',
126+
'uptime-kuma-status-page': 'UptimeKumaStatusPage',
126127
'wallet-balance': 'WalletBalance',
127128
weather: 'Weather',
128129
'weather-forecast': 'WeatherForecast',

0 commit comments

Comments
 (0)