@davidfuchs/mcp-uptime-kuma 0.6.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -439
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +745 -11
- package/dist/server.js.map +1 -1
- package/dist/types/docker-host.d.ts +26 -0
- package/dist/types/docker-host.d.ts.map +1 -0
- package/dist/types/docker-host.js +13 -0
- package/dist/types/docker-host.js.map +1 -0
- package/dist/types/heartbeat.d.ts +31 -31
- package/dist/types/heartbeat.d.ts.map +1 -1
- package/dist/types/heartbeat.js +1 -1
- package/dist/types/heartbeat.js.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/maintenance.d.ts +70 -0
- package/dist/types/maintenance.d.ts.map +1 -0
- package/dist/types/maintenance.js +22 -0
- package/dist/types/maintenance.js.map +1 -0
- package/dist/types/notification.d.ts +29 -0
- package/dist/types/notification.d.ts.map +1 -0
- package/dist/types/notification.js +14 -0
- package/dist/types/notification.js.map +1 -0
- package/dist/types/status-page.d.ts +46 -0
- package/dist/types/status-page.d.ts.map +1 -0
- package/dist/types/status-page.js +19 -0
- package/dist/types/status-page.js.map +1 -0
- package/dist/uptime-kuma-client.d.ts +210 -1
- package/dist/uptime-kuma-client.d.ts.map +1 -1
- package/dist/uptime-kuma-client.js +637 -4
- package/dist/uptime-kuma-client.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -3
|
@@ -26,8 +26,14 @@ export class UptimeKumaClient {
|
|
|
26
26
|
heartbeatListCache = {};
|
|
27
27
|
uptimeCache = {};
|
|
28
28
|
avgPingCache = {};
|
|
29
|
+
notificationListCache = {};
|
|
30
|
+
tagListCache = [];
|
|
31
|
+
maintenanceListCache = {};
|
|
32
|
+
statusPageListCache = {};
|
|
33
|
+
dockerHostListCache = [];
|
|
29
34
|
server;
|
|
30
35
|
shouldLog;
|
|
36
|
+
loginCredentials = null;
|
|
31
37
|
constructor(url, server, shouldLog) {
|
|
32
38
|
this.url = url;
|
|
33
39
|
this.server = server;
|
|
@@ -55,18 +61,69 @@ export class UptimeKumaClient {
|
|
|
55
61
|
this.socket = io(this.url, {
|
|
56
62
|
reconnection: true,
|
|
57
63
|
reconnectionDelay: 1000,
|
|
58
|
-
reconnectionAttempts:
|
|
64
|
+
reconnectionAttempts: Infinity,
|
|
59
65
|
});
|
|
66
|
+
let initialConnect = true;
|
|
60
67
|
this.socket.on('connect', () => {
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
if (initialConnect) {
|
|
69
|
+
initialConnect = false;
|
|
70
|
+
this.safeLog('info', 'Successfully connected to Uptime Kuma server');
|
|
71
|
+
resolve();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.safeLog('info', 'Reconnected to Uptime Kuma server, re-authenticating...');
|
|
75
|
+
this.reauthenticate();
|
|
76
|
+
}
|
|
63
77
|
});
|
|
64
78
|
this.socket.on('connect_error', (error) => {
|
|
65
79
|
this.safeLog('error', `Connection error: ${error.message}`);
|
|
66
|
-
|
|
80
|
+
if (initialConnect) {
|
|
81
|
+
reject(new Error(`Connection failed: ${error.message}`));
|
|
82
|
+
}
|
|
67
83
|
});
|
|
68
84
|
});
|
|
69
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Re-authenticate after a reconnection to refresh all cached data.
|
|
88
|
+
* When the server restarts or the connection drops, Socket.IO reconnects
|
|
89
|
+
* the transport but the server no longer considers the client authenticated.
|
|
90
|
+
* Without re-emitting login, the server won't send monitorList or heartbeat
|
|
91
|
+
* events, leaving the cache permanently stale.
|
|
92
|
+
*/
|
|
93
|
+
reauthenticate() {
|
|
94
|
+
if (!this.socket || !this.loginCredentials)
|
|
95
|
+
return;
|
|
96
|
+
// Clear stale caches so they are fully replaced by fresh data from the server
|
|
97
|
+
this.monitorListCache = {};
|
|
98
|
+
this.heartbeatListCache = {};
|
|
99
|
+
this.uptimeCache = {};
|
|
100
|
+
this.avgPingCache = {};
|
|
101
|
+
const { username, password, token, jwtToken } = this.loginCredentials;
|
|
102
|
+
if (jwtToken) {
|
|
103
|
+
this.socket.emit('loginByToken', jwtToken, (response) => {
|
|
104
|
+
if (response.ok) {
|
|
105
|
+
this.safeLog('info', 'Re-authenticated after reconnection (JWT)');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.safeLog('error', `Re-authentication failed: ${response.msg || 'unknown error'}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (username) {
|
|
113
|
+
this.socket.emit('login', { username, password, token }, (response) => {
|
|
114
|
+
if (response.ok) {
|
|
115
|
+
this.safeLog('info', 'Re-authenticated after reconnection');
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.safeLog('error', `Re-authentication failed: ${response.msg || 'unknown error'}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.socket.emit('login');
|
|
124
|
+
this.safeLog('info', 'Re-authenticated after reconnection (anonymous)');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
70
127
|
/**
|
|
71
128
|
* Disconnect from the Uptime Kuma server
|
|
72
129
|
*/
|
|
@@ -78,6 +135,11 @@ export class UptimeKumaClient {
|
|
|
78
135
|
this.socket.off('deleteMonitorFromList');
|
|
79
136
|
this.socket.off('heartbeatList');
|
|
80
137
|
this.socket.off('heartbeat');
|
|
138
|
+
this.socket.off('notificationList');
|
|
139
|
+
this.socket.off('tagList');
|
|
140
|
+
this.socket.off('maintenanceList');
|
|
141
|
+
this.socket.off('statusPageList');
|
|
142
|
+
this.socket.off('dockerHostList');
|
|
81
143
|
this.socket.disconnect();
|
|
82
144
|
this.socket = null;
|
|
83
145
|
}
|
|
@@ -86,6 +148,11 @@ export class UptimeKumaClient {
|
|
|
86
148
|
this.heartbeatListCache = {};
|
|
87
149
|
this.uptimeCache = {};
|
|
88
150
|
this.avgPingCache = {};
|
|
151
|
+
this.notificationListCache = {};
|
|
152
|
+
this.tagListCache = [];
|
|
153
|
+
this.maintenanceListCache = {};
|
|
154
|
+
this.statusPageListCache = {};
|
|
155
|
+
this.dockerHostListCache = [];
|
|
89
156
|
}
|
|
90
157
|
/**
|
|
91
158
|
* Login using username and password, or JWT token
|
|
@@ -102,11 +169,18 @@ export class UptimeKumaClient {
|
|
|
102
169
|
reject(new Error('Not connected to server'));
|
|
103
170
|
return;
|
|
104
171
|
}
|
|
172
|
+
// Store credentials for re-authentication on reconnect
|
|
173
|
+
this.loginCredentials = { username, password, token, jwtToken };
|
|
105
174
|
// Set up listeners for monitor list and heartbeat updates before login
|
|
106
175
|
this.setupMonitorListListeners();
|
|
107
176
|
this.setupHeartbeatListeners();
|
|
108
177
|
this.setupUptimeListeners();
|
|
109
178
|
this.setupAvgPingListeners();
|
|
179
|
+
this.setupNotificationListListeners();
|
|
180
|
+
this.setupTagListListeners();
|
|
181
|
+
this.setupMaintenanceListListeners();
|
|
182
|
+
this.setupStatusPageListListeners();
|
|
183
|
+
this.setupDockerHostListListeners();
|
|
110
184
|
// If JWT token is provided, use token-based authentication
|
|
111
185
|
if (jwtToken) {
|
|
112
186
|
this.socket.emit('loginByToken', jwtToken, (response) => {
|
|
@@ -528,6 +602,565 @@ export class UptimeKumaClient {
|
|
|
528
602
|
}
|
|
529
603
|
return summaries;
|
|
530
604
|
}
|
|
605
|
+
// ─── New listener setup methods ────────────────────────────────────────────
|
|
606
|
+
setupNotificationListListeners() {
|
|
607
|
+
if (!this.socket)
|
|
608
|
+
return;
|
|
609
|
+
this.socket.on('notificationList', (notificationList) => {
|
|
610
|
+
this.safeLog('debug', `Received notificationList with ${Object.keys(notificationList).length} notifications`);
|
|
611
|
+
this.notificationListCache = notificationList;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
setupTagListListeners() {
|
|
615
|
+
if (!this.socket)
|
|
616
|
+
return;
|
|
617
|
+
this.socket.on('tagList', (tagList) => {
|
|
618
|
+
this.safeLog('debug', `Received tagList with ${tagList.length} tags`);
|
|
619
|
+
this.tagListCache = tagList;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
setupMaintenanceListListeners() {
|
|
623
|
+
if (!this.socket)
|
|
624
|
+
return;
|
|
625
|
+
this.socket.on('maintenanceList', (maintenanceList) => {
|
|
626
|
+
this.safeLog('debug', `Received maintenanceList with ${Object.keys(maintenanceList).length} windows`);
|
|
627
|
+
this.maintenanceListCache = maintenanceList;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
setupStatusPageListListeners() {
|
|
631
|
+
if (!this.socket)
|
|
632
|
+
return;
|
|
633
|
+
this.socket.on('statusPageList', (statusPageList) => {
|
|
634
|
+
this.safeLog('debug', `Received statusPageList with ${Object.keys(statusPageList).length} status pages`);
|
|
635
|
+
this.statusPageListCache = statusPageList;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
setupDockerHostListListeners() {
|
|
639
|
+
if (!this.socket)
|
|
640
|
+
return;
|
|
641
|
+
this.socket.on('dockerHostList', (dockerHostList) => {
|
|
642
|
+
this.safeLog('debug', `Received dockerHostList with ${dockerHostList.length} docker hosts`);
|
|
643
|
+
this.dockerHostListCache = dockerHostList;
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
// ─── Monitor write operations ───────────────────────────────────────────────
|
|
647
|
+
/**
|
|
648
|
+
* Create a new monitor. If `tags` are supplied, they are applied after
|
|
649
|
+
* creation using the separate `addMonitorTag` socket event — the `add`
|
|
650
|
+
* socket handler does not process tags.
|
|
651
|
+
*
|
|
652
|
+
* @param monitorData - Monitor configuration (type-specific fields should be included)
|
|
653
|
+
* @returns Promise resolving to the API response with the new monitorID
|
|
654
|
+
*/
|
|
655
|
+
async createMonitor(monitorData) {
|
|
656
|
+
const { tags, ...payload } = monitorData;
|
|
657
|
+
const response = await new Promise((resolve, reject) => {
|
|
658
|
+
if (!this.socket || !this.socket.connected) {
|
|
659
|
+
reject(new Error('Not connected to server'));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.socket.emit('add', payload, (res) => {
|
|
663
|
+
if (res.ok) {
|
|
664
|
+
this.safeLog('info', `Successfully created monitor (ID: ${res.monitorID})`);
|
|
665
|
+
resolve(res);
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
reject(new Error(res.msg || 'Failed to create monitor'));
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
if (tags && Array.isArray(tags) && response.monitorID != null) {
|
|
673
|
+
await this.reconcileMonitorTags(response.monitorID, tags);
|
|
674
|
+
}
|
|
675
|
+
return response;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Update an existing monitor. If `tags` are supplied, they are reconciled
|
|
679
|
+
* against the monitor's current tag set using `addMonitorTag` /
|
|
680
|
+
* `deleteMonitorTag` — the `editMonitor` socket handler does not process
|
|
681
|
+
* tags. Tags whose name is not yet in the catalog are auto-created via
|
|
682
|
+
* `addTag` before binding.
|
|
683
|
+
*
|
|
684
|
+
* @param monitorData - Monitor configuration including the id field
|
|
685
|
+
* @returns Promise resolving to the API response
|
|
686
|
+
*/
|
|
687
|
+
async updateMonitor(monitorData) {
|
|
688
|
+
const { tags, ...payload } = monitorData;
|
|
689
|
+
const response = await new Promise((resolve, reject) => {
|
|
690
|
+
if (!this.socket || !this.socket.connected) {
|
|
691
|
+
reject(new Error('Not connected to server'));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.socket.emit('editMonitor', payload, (res) => {
|
|
695
|
+
if (res.ok) {
|
|
696
|
+
this.safeLog('info', `Successfully updated monitor (ID: ${monitorData['id']})`);
|
|
697
|
+
resolve(res);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
reject(new Error(res.msg || 'Failed to update monitor'));
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
if (tags && Array.isArray(tags) && monitorData['id'] != null) {
|
|
705
|
+
await this.reconcileMonitorTags(Number(monitorData['id']), tags);
|
|
706
|
+
}
|
|
707
|
+
return response;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Fetch the tag catalog synchronously from the server. Uptime Kuma does
|
|
711
|
+
* not push `tagList` events — it only responds to the `getTags` request —
|
|
712
|
+
* so relying on the push-populated cache returns stale (often empty) data.
|
|
713
|
+
* This method also refreshes `tagListCache` so subsequent cache reads work.
|
|
714
|
+
*/
|
|
715
|
+
fetchTagList() {
|
|
716
|
+
return new Promise((resolve, reject) => {
|
|
717
|
+
if (!this.socket || !this.socket.connected) {
|
|
718
|
+
reject(new Error('Not connected to server'));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.socket.emit('getTags', (res) => {
|
|
722
|
+
if (res.ok && res.tags) {
|
|
723
|
+
this.tagListCache = res.tags;
|
|
724
|
+
resolve(res.tags);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
reject(new Error(res.msg || 'Failed to fetch tag list'));
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Bind a tag to a monitor (socket: `addMonitorTag`).
|
|
734
|
+
*/
|
|
735
|
+
addMonitorTag(tagID, monitorID, value) {
|
|
736
|
+
return new Promise((resolve, reject) => {
|
|
737
|
+
if (!this.socket || !this.socket.connected) {
|
|
738
|
+
reject(new Error('Not connected to server'));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.socket.emit('addMonitorTag', tagID, monitorID, value, (res) => {
|
|
742
|
+
if (res.ok)
|
|
743
|
+
resolve(res);
|
|
744
|
+
else
|
|
745
|
+
reject(new Error(res.msg || `Failed to add tag ${tagID} to monitor ${monitorID}`));
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Unbind a tag from a monitor (socket: `deleteMonitorTag`).
|
|
751
|
+
*/
|
|
752
|
+
deleteMonitorTag(tagID, monitorID, value) {
|
|
753
|
+
return new Promise((resolve, reject) => {
|
|
754
|
+
if (!this.socket || !this.socket.connected) {
|
|
755
|
+
reject(new Error('Not connected to server'));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
this.socket.emit('deleteMonitorTag', tagID, monitorID, value, (res) => {
|
|
759
|
+
if (res.ok)
|
|
760
|
+
resolve(res);
|
|
761
|
+
else
|
|
762
|
+
reject(new Error(res.msg || `Failed to remove tag ${tagID} from monitor ${monitorID}`));
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Reconcile a monitor's tags against the desired list. Auto-creates any
|
|
768
|
+
* tag name that isn't yet in the catalog. Tag identity is `(name, value)`.
|
|
769
|
+
*/
|
|
770
|
+
async reconcileMonitorTags(monitorID, desiredTags) {
|
|
771
|
+
const currentMonitor = this.monitorListCache[String(monitorID)];
|
|
772
|
+
const currentTags = (currentMonitor?.tags ?? []);
|
|
773
|
+
const key = (name, value) => `${name}\u0000${value ?? ''}`;
|
|
774
|
+
const currentKeys = new Set(currentTags.map((t) => key(t.name, t.value)));
|
|
775
|
+
const desiredKeys = new Set(desiredTags.map((t) => key(String(t.name), t.value)));
|
|
776
|
+
// Uptime Kuma never pushes `tagList` events, so the cache is unreliable
|
|
777
|
+
// — fetch the catalog synchronously via the `getTags` socket request.
|
|
778
|
+
const freshTags = await this.fetchTagList();
|
|
779
|
+
const nameToID = new Map();
|
|
780
|
+
for (const t of freshTags)
|
|
781
|
+
nameToID.set(t.name, t.id);
|
|
782
|
+
for (const desired of desiredTags) {
|
|
783
|
+
const name = String(desired.name);
|
|
784
|
+
const value = desired.value ?? '';
|
|
785
|
+
if (currentKeys.has(key(name, value)))
|
|
786
|
+
continue;
|
|
787
|
+
let tagID = nameToID.get(name);
|
|
788
|
+
if (tagID == null) {
|
|
789
|
+
const color = desired.color ?? '#808080';
|
|
790
|
+
const created = await this.addTag(name, color);
|
|
791
|
+
tagID = created.tag?.id;
|
|
792
|
+
if (tagID != null)
|
|
793
|
+
nameToID.set(name, tagID);
|
|
794
|
+
}
|
|
795
|
+
if (tagID == null) {
|
|
796
|
+
throw new Error(`Could not resolve tag ID for "${name}"`);
|
|
797
|
+
}
|
|
798
|
+
await this.addMonitorTag(tagID, monitorID, value);
|
|
799
|
+
}
|
|
800
|
+
for (const existing of currentTags) {
|
|
801
|
+
if (desiredKeys.has(key(existing.name, existing.value)))
|
|
802
|
+
continue;
|
|
803
|
+
const tagID = existing.tag_id ?? nameToID.get(existing.name);
|
|
804
|
+
if (tagID == null)
|
|
805
|
+
continue;
|
|
806
|
+
await this.deleteMonitorTag(tagID, monitorID, existing.value ?? '');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Delete a monitor
|
|
811
|
+
*
|
|
812
|
+
* @param monitorID - The ID of the monitor to delete
|
|
813
|
+
* @returns Promise resolving to the API response
|
|
814
|
+
*/
|
|
815
|
+
deleteMonitor(monitorID) {
|
|
816
|
+
return new Promise((resolve, reject) => {
|
|
817
|
+
if (!this.socket || !this.socket.connected) {
|
|
818
|
+
reject(new Error('Not connected to server'));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
this.socket.emit('deleteMonitor', monitorID, (response) => {
|
|
822
|
+
if (response.ok) {
|
|
823
|
+
this.safeLog('info', `Successfully deleted monitor ${monitorID}`);
|
|
824
|
+
resolve(response);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
reject(new Error(response.msg || 'Failed to delete monitor'));
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
// ─── Notification operations ────────────────────────────────────────────────
|
|
833
|
+
/**
|
|
834
|
+
* Get the cached notification list
|
|
835
|
+
*/
|
|
836
|
+
getNotificationList() {
|
|
837
|
+
return Object.values(this.notificationListCache);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Add or update a notification channel
|
|
841
|
+
*
|
|
842
|
+
* @param notification - Notification configuration
|
|
843
|
+
* @param notificationID - If provided, updates existing; otherwise creates new
|
|
844
|
+
* @returns Promise resolving to the API response with the notification id
|
|
845
|
+
*/
|
|
846
|
+
addNotification(notification, notificationID) {
|
|
847
|
+
return new Promise((resolve, reject) => {
|
|
848
|
+
if (!this.socket || !this.socket.connected) {
|
|
849
|
+
reject(new Error('Not connected to server'));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const id = notificationID ?? null;
|
|
853
|
+
this.socket.emit('addNotification', notification, id, (response) => {
|
|
854
|
+
if (response.ok) {
|
|
855
|
+
this.safeLog('info', `Successfully saved notification (ID: ${response.id})`);
|
|
856
|
+
resolve(response);
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
reject(new Error(response.msg || 'Failed to save notification'));
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Delete a notification channel
|
|
866
|
+
*
|
|
867
|
+
* @param notificationID - The ID of the notification to delete
|
|
868
|
+
* @returns Promise resolving to the API response
|
|
869
|
+
*/
|
|
870
|
+
deleteNotification(notificationID) {
|
|
871
|
+
return new Promise((resolve, reject) => {
|
|
872
|
+
if (!this.socket || !this.socket.connected) {
|
|
873
|
+
reject(new Error('Not connected to server'));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
this.socket.emit('deleteNotification', notificationID, (response) => {
|
|
877
|
+
if (response.ok) {
|
|
878
|
+
this.safeLog('info', `Successfully deleted notification ${notificationID}`);
|
|
879
|
+
resolve(response);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
reject(new Error(response.msg || 'Failed to delete notification'));
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
// ─── Docker host operations ─────────────────────────────────────────────────
|
|
888
|
+
/**
|
|
889
|
+
* Get the cached docker host list
|
|
890
|
+
*/
|
|
891
|
+
getDockerHostList() {
|
|
892
|
+
return this.dockerHostListCache;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Add or update a docker host
|
|
896
|
+
*
|
|
897
|
+
* @param dockerHost - Docker host configuration (name, dockerType, dockerDaemon)
|
|
898
|
+
* @param dockerHostID - If provided, updates existing; otherwise creates new
|
|
899
|
+
* @returns Promise resolving to the API response with the docker host id
|
|
900
|
+
*/
|
|
901
|
+
addDockerHost(dockerHost, dockerHostID) {
|
|
902
|
+
return new Promise((resolve, reject) => {
|
|
903
|
+
if (!this.socket || !this.socket.connected) {
|
|
904
|
+
reject(new Error('Not connected to server'));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const id = dockerHostID ?? null;
|
|
908
|
+
this.socket.emit('addDockerHost', dockerHost, id, (response) => {
|
|
909
|
+
if (response.ok) {
|
|
910
|
+
this.safeLog('info', `Successfully saved docker host (ID: ${response.id})`);
|
|
911
|
+
resolve(response);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
reject(new Error(response.msg || 'Failed to save docker host'));
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Delete a docker host. Any monitors referencing it will have their docker_host
|
|
921
|
+
* field cleared by Uptime Kuma.
|
|
922
|
+
*
|
|
923
|
+
* @param dockerHostID - The ID of the docker host to delete
|
|
924
|
+
* @returns Promise resolving to the API response
|
|
925
|
+
*/
|
|
926
|
+
deleteDockerHost(dockerHostID) {
|
|
927
|
+
return new Promise((resolve, reject) => {
|
|
928
|
+
if (!this.socket || !this.socket.connected) {
|
|
929
|
+
reject(new Error('Not connected to server'));
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.socket.emit('deleteDockerHost', dockerHostID, (response) => {
|
|
933
|
+
if (response.ok) {
|
|
934
|
+
this.safeLog('info', `Successfully deleted docker host ${dockerHostID}`);
|
|
935
|
+
resolve(response);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
reject(new Error(response.msg || 'Failed to delete docker host'));
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Test connectivity to a docker host without persisting it. Returns a friendly
|
|
945
|
+
* message containing the number of containers when reachable.
|
|
946
|
+
*
|
|
947
|
+
* @param dockerHost - Docker host configuration to test (name, dockerType, dockerDaemon)
|
|
948
|
+
* @returns Promise resolving to the API response
|
|
949
|
+
*/
|
|
950
|
+
testDockerHost(dockerHost) {
|
|
951
|
+
return new Promise((resolve, reject) => {
|
|
952
|
+
if (!this.socket || !this.socket.connected) {
|
|
953
|
+
reject(new Error('Not connected to server'));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
this.socket.emit('testDockerHost', dockerHost, (response) => {
|
|
957
|
+
// Resolve either way so callers can inspect ok/msg without try/catch
|
|
958
|
+
// (matches the pattern used by UK's UI, which shows both success and
|
|
959
|
+
// failure messages from the same callback).
|
|
960
|
+
resolve(response);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
// ─── Tag operations ─────────────────────────────────────────────────────────
|
|
965
|
+
/**
|
|
966
|
+
* Get the cached tag list
|
|
967
|
+
*/
|
|
968
|
+
getTagList() {
|
|
969
|
+
return this.tagListCache;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Create a new tag
|
|
973
|
+
*
|
|
974
|
+
* @param name - Tag name
|
|
975
|
+
* @param color - Tag color (hex string, e.g. '#ff0000')
|
|
976
|
+
* @returns Promise resolving to the created tag object
|
|
977
|
+
*/
|
|
978
|
+
addTag(name, color) {
|
|
979
|
+
return new Promise((resolve, reject) => {
|
|
980
|
+
if (!this.socket || !this.socket.connected) {
|
|
981
|
+
reject(new Error('Not connected to server'));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
this.socket.emit('addTag', { name, color }, (response) => {
|
|
985
|
+
if (response.ok) {
|
|
986
|
+
this.safeLog('info', `Successfully created tag "${name}" (ID: ${response.tag?.id})`);
|
|
987
|
+
resolve(response);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
reject(new Error(response.msg || 'Failed to create tag'));
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Delete a tag
|
|
997
|
+
*
|
|
998
|
+
* @param tagID - The ID of the tag to delete
|
|
999
|
+
* @returns Promise resolving to the API response
|
|
1000
|
+
*/
|
|
1001
|
+
deleteTag(tagID) {
|
|
1002
|
+
return new Promise((resolve, reject) => {
|
|
1003
|
+
if (!this.socket || !this.socket.connected) {
|
|
1004
|
+
reject(new Error('Not connected to server'));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
this.socket.emit('deleteTag', tagID, (response) => {
|
|
1008
|
+
if (response.ok) {
|
|
1009
|
+
this.safeLog('info', `Successfully deleted tag ${tagID}`);
|
|
1010
|
+
resolve(response);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
reject(new Error(response.msg || 'Failed to delete tag'));
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
// ─── Maintenance operations ─────────────────────────────────────────────────
|
|
1019
|
+
/**
|
|
1020
|
+
* Get the cached maintenance window list
|
|
1021
|
+
*/
|
|
1022
|
+
getMaintenanceList() {
|
|
1023
|
+
return Object.values(this.maintenanceListCache);
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Create a new maintenance window
|
|
1027
|
+
*
|
|
1028
|
+
* @param maintenanceData - Maintenance window configuration
|
|
1029
|
+
* @returns Promise resolving to the API response with the maintenance ID
|
|
1030
|
+
*/
|
|
1031
|
+
createMaintenance(maintenanceData) {
|
|
1032
|
+
return new Promise((resolve, reject) => {
|
|
1033
|
+
if (!this.socket || !this.socket.connected) {
|
|
1034
|
+
reject(new Error('Not connected to server'));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
this.socket.emit('addMaintenance', maintenanceData, (response) => {
|
|
1038
|
+
if (response.ok) {
|
|
1039
|
+
this.safeLog('info', `Successfully created maintenance window (ID: ${response.maintenanceID})`);
|
|
1040
|
+
resolve(response);
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
reject(new Error(response.msg || 'Failed to create maintenance window'));
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
// ─── Status page operations ─────────────────────────────────────────────────
|
|
1049
|
+
/**
|
|
1050
|
+
* Get the cached status page list
|
|
1051
|
+
*/
|
|
1052
|
+
getStatusPageList() {
|
|
1053
|
+
return Object.values(this.statusPageListCache);
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get full details of a single status page, including publicGroupList with monitors
|
|
1057
|
+
* and any active incidents. Uses the public HTTP API (`/api/status-page/{slug}`),
|
|
1058
|
+
* which returns the same data the status page UI renders — richer than the
|
|
1059
|
+
* socket `getStatusPage` event, which only returns config.
|
|
1060
|
+
*
|
|
1061
|
+
* @param slug - The status page slug
|
|
1062
|
+
* @returns Promise resolving to the status page config, groups, and incidents
|
|
1063
|
+
*/
|
|
1064
|
+
async getStatusPage(slug) {
|
|
1065
|
+
try {
|
|
1066
|
+
const baseUrl = this.url.replace(/\/$/, '');
|
|
1067
|
+
const res = await fetch(`${baseUrl}/api/status-page/${encodeURIComponent(slug)}`);
|
|
1068
|
+
if (res.status === 404) {
|
|
1069
|
+
return { ok: false, msg: `Status page ${slug} not found` };
|
|
1070
|
+
}
|
|
1071
|
+
if (!res.ok) {
|
|
1072
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
1073
|
+
}
|
|
1074
|
+
const data = await res.json();
|
|
1075
|
+
return {
|
|
1076
|
+
ok: true,
|
|
1077
|
+
config: data.config,
|
|
1078
|
+
publicGroupList: data.publicGroupList,
|
|
1079
|
+
incidents: data.incidents ?? [],
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
1084
|
+
throw new Error(`Failed to get status page ${slug}: ${msg}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Create a new (empty) status page with the given title and slug
|
|
1089
|
+
*
|
|
1090
|
+
* Note: This creates a blank status page. Use updateStatusPage afterwards to
|
|
1091
|
+
* set description, theme, groups, monitors, etc.
|
|
1092
|
+
*
|
|
1093
|
+
* @param title - Display title of the status page
|
|
1094
|
+
* @param slug - URL slug (lowercase letters, digits, and dashes only)
|
|
1095
|
+
* @returns Promise resolving to the API response
|
|
1096
|
+
*/
|
|
1097
|
+
createStatusPage(title, slug) {
|
|
1098
|
+
return new Promise((resolve, reject) => {
|
|
1099
|
+
if (!this.socket || !this.socket.connected) {
|
|
1100
|
+
reject(new Error('Not connected to server'));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
this.socket.emit('addStatusPage', title, slug, (response) => {
|
|
1104
|
+
if (response.ok) {
|
|
1105
|
+
this.safeLog('info', `Successfully created status page ${slug}`);
|
|
1106
|
+
resolve(response);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
reject(new Error(response.msg || `Failed to create status page ${slug}`));
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Update an existing status page's config and group/monitor list
|
|
1116
|
+
*
|
|
1117
|
+
* @param slug - The status page slug (immutable identifier)
|
|
1118
|
+
* @param config - Status page configuration (title, description, theme, published, etc.)
|
|
1119
|
+
* @param publicGroupList - Ordered groups, each with a name, weight, and monitorList `[{id}]`
|
|
1120
|
+
* @param imgDataUrl - Optional icon as data URL (pass empty string to keep existing)
|
|
1121
|
+
* @returns Promise resolving to the API response
|
|
1122
|
+
*/
|
|
1123
|
+
updateStatusPage(slug, config, publicGroupList = [], imgDataUrl = '') {
|
|
1124
|
+
return new Promise((resolve, reject) => {
|
|
1125
|
+
if (!this.socket || !this.socket.connected) {
|
|
1126
|
+
reject(new Error('Not connected to server'));
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
this.socket.emit('saveStatusPage', slug, config, imgDataUrl, publicGroupList, (response) => {
|
|
1130
|
+
if (response.ok) {
|
|
1131
|
+
this.safeLog('info', `Successfully updated status page ${slug}`);
|
|
1132
|
+
resolve(response);
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
reject(new Error(response.msg || `Failed to update status page ${slug}`));
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Delete a status page
|
|
1142
|
+
*
|
|
1143
|
+
* @param slug - The status page slug
|
|
1144
|
+
* @returns Promise resolving to the API response
|
|
1145
|
+
*/
|
|
1146
|
+
deleteStatusPage(slug) {
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
if (!this.socket || !this.socket.connected) {
|
|
1149
|
+
reject(new Error('Not connected to server'));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
this.socket.emit('deleteStatusPage', slug, (response) => {
|
|
1153
|
+
if (response.ok) {
|
|
1154
|
+
this.safeLog('info', `Successfully deleted status page ${slug}`);
|
|
1155
|
+
resolve(response);
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
reject(new Error(response.msg || `Failed to delete status page ${slug}`));
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
// ─── Socket accessor ─────────────────────────────────────────────────────────
|
|
531
1164
|
/**
|
|
532
1165
|
* Get the socket instance (for advanced usage)
|
|
533
1166
|
*/
|