matterbridge 3.0.2-dev-20250515-4122c94 → 3.0.2
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/dist/cli.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -2
- package/dist/cli.js.map +1 -0
- package/dist/cluster/export.d.ts +2 -0
- package/dist/cluster/export.d.ts.map +1 -0
- package/dist/cluster/export.js +2 -0
- package/dist/cluster/export.js.map +1 -0
- package/dist/defaultConfigSchema.d.ts +27 -0
- package/dist/defaultConfigSchema.d.ts.map +1 -0
- package/dist/defaultConfigSchema.js +23 -0
- package/dist/defaultConfigSchema.js.map +1 -0
- package/dist/deviceManager.d.ts +114 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +94 -1
- package/dist/deviceManager.js.map +1 -0
- package/dist/frontend.d.ts +241 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +334 -15
- package/dist/frontend.js.map +1 -0
- package/dist/helpers.d.ts +46 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +49 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -0
- package/dist/logger/export.d.ts +2 -0
- package/dist/logger/export.d.ts.map +1 -0
- package/dist/logger/export.js +1 -0
- package/dist/logger/export.js.map +1 -0
- package/dist/matter/behaviors.d.ts +2 -0
- package/dist/matter/behaviors.d.ts.map +1 -0
- package/dist/matter/behaviors.js +2 -0
- package/dist/matter/behaviors.js.map +1 -0
- package/dist/matter/clusters.d.ts +2 -0
- package/dist/matter/clusters.d.ts.map +1 -0
- package/dist/matter/clusters.js +2 -0
- package/dist/matter/clusters.js.map +1 -0
- package/dist/matter/devices.d.ts +2 -0
- package/dist/matter/devices.d.ts.map +1 -0
- package/dist/matter/devices.js +2 -0
- package/dist/matter/devices.js.map +1 -0
- package/dist/matter/endpoints.d.ts +2 -0
- package/dist/matter/endpoints.d.ts.map +1 -0
- package/dist/matter/endpoints.js +2 -0
- package/dist/matter/endpoints.js.map +1 -0
- package/dist/matter/export.d.ts +5 -0
- package/dist/matter/export.d.ts.map +1 -0
- package/dist/matter/export.js +2 -0
- package/dist/matter/export.js.map +1 -0
- package/dist/matter/types.d.ts +3 -0
- package/dist/matter/types.d.ts.map +1 -0
- package/dist/matter/types.js +2 -0
- package/dist/matter/types.js.map +1 -0
- package/dist/matterbridge.d.ts +435 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +746 -47
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +40 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +34 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +1188 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +53 -4
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +494 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +431 -12
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +40 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +34 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +965 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +807 -11
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +2728 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +147 -9
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +294 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +225 -7
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +187 -0
- package/dist/matterbridgeTypes.d.ts.map +1 -0
- package/dist/matterbridgeTypes.js +24 -0
- package/dist/matterbridgeTypes.js.map +1 -0
- package/dist/pluginManager.d.ts +273 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +264 -3
- package/dist/pluginManager.js.map +1 -0
- package/dist/roboticVacuumCleaner.d.ts +43 -0
- package/dist/roboticVacuumCleaner.d.ts.map +1 -0
- package/dist/roboticVacuumCleaner.js +39 -3
- package/dist/roboticVacuumCleaner.js.map +1 -0
- package/dist/shelly.d.ts +153 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +155 -7
- package/dist/shelly.js.map +1 -0
- package/dist/storage/export.d.ts +2 -0
- package/dist/storage/export.d.ts.map +1 -0
- package/dist/storage/export.js +1 -0
- package/dist/storage/export.js.map +1 -0
- package/dist/update.d.ts +58 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +53 -0
- package/dist/update.js.map +1 -0
- package/dist/utils/colorUtils.d.ts +61 -0
- package/dist/utils/colorUtils.d.ts.map +1 -0
- package/dist/utils/colorUtils.js +205 -2
- package/dist/utils/colorUtils.js.map +1 -0
- package/dist/utils/commandLine.d.ts +58 -0
- package/dist/utils/commandLine.d.ts.map +1 -0
- package/dist/utils/commandLine.js +53 -0
- package/dist/utils/commandLine.js.map +1 -0
- package/dist/utils/copyDirectory.d.ts +32 -0
- package/dist/utils/copyDirectory.d.ts.map +1 -0
- package/dist/utils/copyDirectory.js +37 -1
- package/dist/utils/copyDirectory.js.map +1 -0
- package/dist/utils/createZip.d.ts +38 -0
- package/dist/utils/createZip.d.ts.map +1 -0
- package/dist/utils/createZip.js +42 -2
- package/dist/utils/createZip.js.map +1 -0
- package/dist/utils/deepCopy.d.ts +31 -0
- package/dist/utils/deepCopy.d.ts.map +1 -0
- package/dist/utils/deepCopy.js +38 -0
- package/dist/utils/deepCopy.js.map +1 -0
- package/dist/utils/deepEqual.d.ts +53 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/deepEqual.js +71 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/export.d.ts +11 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/export.js +1 -0
- package/dist/utils/export.js.map +1 -0
- package/dist/utils/hex.d.ts +48 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/hex.js +57 -0
- package/dist/utils/hex.js.map +1 -0
- package/dist/utils/isvalid.d.ts +102 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +100 -0
- package/dist/utils/isvalid.js.map +1 -0
- package/dist/utils/network.d.ts +69 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +76 -5
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/wait.d.ts +51 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +53 -5
- package/dist/utils/wait.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/frontend.js
CHANGED
|
@@ -1,28 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the class Frontend.
|
|
3
|
+
*
|
|
4
|
+
* @file frontend.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @date 2025-01-13
|
|
7
|
+
* @version 1.0.2
|
|
8
|
+
*
|
|
9
|
+
* Copyright 2025, 2026, 2027 Luca Liguori.
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
* you may not use this file except in compliance with the License.
|
|
13
|
+
* You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License. *
|
|
22
|
+
*/
|
|
23
|
+
// @matter
|
|
1
24
|
import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
|
|
25
|
+
// Node modules
|
|
2
26
|
import { createServer } from 'node:http';
|
|
3
27
|
import https from 'node:https';
|
|
4
28
|
import os from 'node:os';
|
|
5
29
|
import path from 'node:path';
|
|
6
30
|
import { promises as fs } from 'node:fs';
|
|
31
|
+
// Third-party modules
|
|
7
32
|
import express from 'express';
|
|
8
33
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
9
34
|
import multer from 'multer';
|
|
35
|
+
// AnsiLogger module
|
|
10
36
|
import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW, nt } from './logger/export.js';
|
|
37
|
+
// Matterbridge
|
|
11
38
|
import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean } from './utils/export.js';
|
|
12
39
|
import { plg } from './matterbridgeTypes.js';
|
|
13
40
|
import { hasParameter } from './utils/export.js';
|
|
14
41
|
import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
|
|
42
|
+
/**
|
|
43
|
+
* Websocket message ID for logging.
|
|
44
|
+
* @constant {number}
|
|
45
|
+
*/
|
|
15
46
|
export const WS_ID_LOG = 0;
|
|
47
|
+
/**
|
|
48
|
+
* Websocket message ID indicating a refresh is needed.
|
|
49
|
+
* @constant {number}
|
|
50
|
+
*/
|
|
16
51
|
export const WS_ID_REFRESH_NEEDED = 1;
|
|
52
|
+
/**
|
|
53
|
+
* Websocket message ID indicating a restart is needed.
|
|
54
|
+
* @constant {number}
|
|
55
|
+
*/
|
|
17
56
|
export const WS_ID_RESTART_NEEDED = 2;
|
|
57
|
+
/**
|
|
58
|
+
* Websocket message ID indicating a cpu update.
|
|
59
|
+
* @constant {number}
|
|
60
|
+
*/
|
|
18
61
|
export const WS_ID_CPU_UPDATE = 3;
|
|
62
|
+
/**
|
|
63
|
+
* Websocket message ID indicating a memory update.
|
|
64
|
+
* @constant {number}
|
|
65
|
+
*/
|
|
19
66
|
export const WS_ID_MEMORY_UPDATE = 4;
|
|
67
|
+
/**
|
|
68
|
+
* Websocket message ID indicating an uptime update.
|
|
69
|
+
* @constant {number}
|
|
70
|
+
*/
|
|
20
71
|
export const WS_ID_UPTIME_UPDATE = 5;
|
|
72
|
+
/**
|
|
73
|
+
* Websocket message ID indicating a snackbar message.
|
|
74
|
+
* @constant {number}
|
|
75
|
+
*/
|
|
21
76
|
export const WS_ID_SNACKBAR = 6;
|
|
77
|
+
/**
|
|
78
|
+
* Websocket message ID indicating matterbridge has un update available.
|
|
79
|
+
* @constant {number}
|
|
80
|
+
*/
|
|
22
81
|
export const WS_ID_UPDATE_NEEDED = 7;
|
|
82
|
+
/**
|
|
83
|
+
* Websocket message ID indicating a state update.
|
|
84
|
+
* @constant {number}
|
|
85
|
+
*/
|
|
23
86
|
export const WS_ID_STATEUPDATE = 8;
|
|
87
|
+
/**
|
|
88
|
+
* Websocket message ID indicating to close a permanent snackbar message.
|
|
89
|
+
* @constant {number}
|
|
90
|
+
*/
|
|
24
91
|
export const WS_ID_CLOSE_SNACKBAR = 9;
|
|
92
|
+
/**
|
|
93
|
+
* Websocket message ID indicating a shelly system update.
|
|
94
|
+
* check:
|
|
95
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/check
|
|
96
|
+
* perform:
|
|
97
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/perform
|
|
98
|
+
* @constant {number}
|
|
99
|
+
*/
|
|
25
100
|
export const WS_ID_SHELLY_SYS_UPDATE = 100;
|
|
101
|
+
/**
|
|
102
|
+
* Websocket message ID indicating a shelly main update.
|
|
103
|
+
* check:
|
|
104
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/check
|
|
105
|
+
* perform:
|
|
106
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/perform
|
|
107
|
+
* @constant {number}
|
|
108
|
+
*/
|
|
26
109
|
export const WS_ID_SHELLY_MAIN_UPDATE = 101;
|
|
27
110
|
export class Frontend {
|
|
28
111
|
matterbridge;
|
|
@@ -35,7 +118,7 @@ export class Frontend {
|
|
|
35
118
|
webSocketServer;
|
|
36
119
|
constructor(matterbridge) {
|
|
37
120
|
this.matterbridge = matterbridge;
|
|
38
|
-
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4
|
|
121
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
39
122
|
}
|
|
40
123
|
set logLevel(logLevel) {
|
|
41
124
|
this.log.logLevel = logLevel;
|
|
@@ -43,13 +126,43 @@ export class Frontend {
|
|
|
43
126
|
async start(port = 8283) {
|
|
44
127
|
this.port = port;
|
|
45
128
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
129
|
+
// Initialize multer with the upload directory
|
|
46
130
|
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
47
131
|
await fs.mkdir(uploadDir, { recursive: true });
|
|
48
132
|
const upload = multer({ dest: uploadDir });
|
|
133
|
+
// Create the express app that serves the frontend
|
|
49
134
|
this.expressApp = express();
|
|
135
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
136
|
+
/*
|
|
137
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
138
|
+
for (const method of methods) {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
143
|
+
try {
|
|
144
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
145
|
+
return original(path, ...rest);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
*/
|
|
153
|
+
// Log all requests to the server for debugging
|
|
154
|
+
/*
|
|
155
|
+
this.expressApp.use((req, res, next) => {
|
|
156
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
157
|
+
next();
|
|
158
|
+
});
|
|
159
|
+
*/
|
|
160
|
+
// Serve static files from '/static' endpoint
|
|
50
161
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
51
162
|
if (!hasParameter('ssl')) {
|
|
163
|
+
// Create an HTTP server and attach the express app
|
|
52
164
|
this.httpServer = createServer(this.expressApp);
|
|
165
|
+
// Listen on the specified port
|
|
53
166
|
if (hasParameter('ingress')) {
|
|
54
167
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
55
168
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -63,6 +176,7 @@ export class Frontend {
|
|
|
63
176
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
64
177
|
});
|
|
65
178
|
}
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
180
|
this.httpServer.on('error', (error) => {
|
|
67
181
|
this.log.error(`Frontend http server error listening on ${this.port}`);
|
|
68
182
|
switch (error.code) {
|
|
@@ -78,6 +192,7 @@ export class Frontend {
|
|
|
78
192
|
});
|
|
79
193
|
}
|
|
80
194
|
else {
|
|
195
|
+
// Load the SSL certificate, the private key and optionally the CA certificate
|
|
81
196
|
let cert;
|
|
82
197
|
try {
|
|
83
198
|
cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
@@ -105,7 +220,9 @@ export class Frontend {
|
|
|
105
220
|
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
|
|
106
221
|
}
|
|
107
222
|
const serverOptions = { cert, key, ca };
|
|
223
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
108
224
|
this.httpsServer = https.createServer(serverOptions, this.expressApp);
|
|
225
|
+
// Listen on the specified port
|
|
109
226
|
if (hasParameter('ingress')) {
|
|
110
227
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
111
228
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -119,6 +236,7 @@ export class Frontend {
|
|
|
119
236
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
120
237
|
});
|
|
121
238
|
}
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
240
|
this.httpsServer.on('error', (error) => {
|
|
123
241
|
this.log.error(`Frontend https server error listening on ${this.port}`);
|
|
124
242
|
switch (error.code) {
|
|
@@ -135,16 +253,18 @@ export class Frontend {
|
|
|
135
253
|
}
|
|
136
254
|
if (this.initializeError)
|
|
137
255
|
return;
|
|
256
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
138
257
|
const wssPort = this.port;
|
|
139
258
|
const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
|
|
140
259
|
this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
|
|
141
260
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
142
261
|
const clientIp = request.socket.remoteAddress;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
262
|
+
// Set the global logger callback for the WebSocketServer
|
|
263
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
264
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
|
|
265
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
266
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
|
|
267
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
148
268
|
AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
|
|
149
269
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
150
270
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -178,6 +298,7 @@ export class Frontend {
|
|
|
178
298
|
this.webSocketServer.on('error', (ws, error) => {
|
|
179
299
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
180
300
|
});
|
|
301
|
+
// Subscribe to cli events
|
|
181
302
|
const { cliEmitter } = await import('./cli.js');
|
|
182
303
|
cliEmitter.removeAllListeners();
|
|
183
304
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
@@ -189,6 +310,7 @@ export class Frontend {
|
|
|
189
310
|
cliEmitter.on('cpu', (cpuUsage) => {
|
|
190
311
|
this.wssSendCpuUpdate(cpuUsage);
|
|
191
312
|
});
|
|
313
|
+
// Endpoint to validate login code
|
|
192
314
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
193
315
|
const { password } = req.body;
|
|
194
316
|
this.log.debug('The frontend sent /api/login', password);
|
|
@@ -207,23 +329,27 @@ export class Frontend {
|
|
|
207
329
|
this.log.warn('/api/login error wrong password');
|
|
208
330
|
res.json({ valid: false });
|
|
209
331
|
}
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
210
333
|
}
|
|
211
334
|
catch (error) {
|
|
212
335
|
this.log.error('/api/login error getting password');
|
|
213
336
|
res.json({ valid: false });
|
|
214
337
|
}
|
|
215
338
|
});
|
|
339
|
+
// Endpoint to provide health check for docker
|
|
216
340
|
this.expressApp.get('/health', (req, res) => {
|
|
217
341
|
this.log.debug('Express received /health');
|
|
218
342
|
const healthStatus = {
|
|
219
|
-
status: 'ok',
|
|
220
|
-
uptime: process.uptime(),
|
|
221
|
-
timestamp: new Date().toISOString(),
|
|
343
|
+
status: 'ok', // Indicate service is healthy
|
|
344
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
345
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
222
346
|
};
|
|
223
347
|
res.status(200).json(healthStatus);
|
|
224
348
|
});
|
|
349
|
+
// Endpoint to provide memory usage details
|
|
225
350
|
this.expressApp.get('/memory', async (req, res) => {
|
|
226
351
|
this.log.debug('Express received /memory');
|
|
352
|
+
// Memory usage from process
|
|
227
353
|
const memoryUsageRaw = process.memoryUsage();
|
|
228
354
|
const memoryUsage = {
|
|
229
355
|
rss: this.formatMemoryUsage(memoryUsageRaw.rss),
|
|
@@ -232,10 +358,13 @@ export class Frontend {
|
|
|
232
358
|
external: this.formatMemoryUsage(memoryUsageRaw.external),
|
|
233
359
|
arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
|
|
234
360
|
};
|
|
361
|
+
// V8 heap statistics
|
|
235
362
|
const { default: v8 } = await import('node:v8');
|
|
236
363
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
237
364
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
365
|
+
// Format heapStats
|
|
238
366
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
|
|
367
|
+
// Format heapSpaces
|
|
239
368
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
240
369
|
...space,
|
|
241
370
|
space_size: this.formatMemoryUsage(space.space_size),
|
|
@@ -253,19 +382,23 @@ export class Frontend {
|
|
|
253
382
|
};
|
|
254
383
|
res.status(200).json(memoryReport);
|
|
255
384
|
});
|
|
385
|
+
// Endpoint to provide settings
|
|
256
386
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
257
387
|
this.log.debug('The frontend sent /api/settings');
|
|
258
388
|
res.json(await this.getApiSettings());
|
|
259
389
|
});
|
|
390
|
+
// Endpoint to provide plugins
|
|
260
391
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
261
392
|
this.log.debug('The frontend sent /api/plugins');
|
|
262
393
|
res.json(this.getBaseRegisteredPlugins());
|
|
263
394
|
});
|
|
395
|
+
// Endpoint to provide devices
|
|
264
396
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
265
397
|
this.log.debug('The frontend sent /api/devices');
|
|
266
398
|
const devices = await this.getDevices();
|
|
267
399
|
res.json(devices);
|
|
268
400
|
});
|
|
401
|
+
// Endpoint to view the matterbridge log
|
|
269
402
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
270
403
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
271
404
|
try {
|
|
@@ -278,6 +411,7 @@ export class Frontend {
|
|
|
278
411
|
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
279
412
|
}
|
|
280
413
|
});
|
|
414
|
+
// Endpoint to view the matter.js log
|
|
281
415
|
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
282
416
|
this.log.debug('The frontend sent /api/view-mjlog');
|
|
283
417
|
try {
|
|
@@ -290,6 +424,7 @@ export class Frontend {
|
|
|
290
424
|
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
291
425
|
}
|
|
292
426
|
});
|
|
427
|
+
// Endpoint to view the shelly log
|
|
293
428
|
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
294
429
|
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
295
430
|
try {
|
|
@@ -302,6 +437,7 @@ export class Frontend {
|
|
|
302
437
|
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
303
438
|
}
|
|
304
439
|
});
|
|
440
|
+
// Endpoint to download the matterbridge log
|
|
305
441
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
306
442
|
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
307
443
|
try {
|
|
@@ -314,6 +450,7 @@ export class Frontend {
|
|
|
314
450
|
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
315
451
|
}
|
|
316
452
|
res.type('text/plain');
|
|
453
|
+
// res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
317
454
|
res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
318
455
|
if (error) {
|
|
319
456
|
this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
@@ -321,6 +458,7 @@ export class Frontend {
|
|
|
321
458
|
}
|
|
322
459
|
});
|
|
323
460
|
});
|
|
461
|
+
// Endpoint to download the matter log
|
|
324
462
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
325
463
|
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
326
464
|
try {
|
|
@@ -340,6 +478,7 @@ export class Frontend {
|
|
|
340
478
|
}
|
|
341
479
|
});
|
|
342
480
|
});
|
|
481
|
+
// Endpoint to download the shelly log
|
|
343
482
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
344
483
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
345
484
|
try {
|
|
@@ -359,6 +498,7 @@ export class Frontend {
|
|
|
359
498
|
}
|
|
360
499
|
});
|
|
361
500
|
});
|
|
501
|
+
// Endpoint to download the matterbridge storage directory
|
|
362
502
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
363
503
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
364
504
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
|
|
@@ -369,6 +509,7 @@ export class Frontend {
|
|
|
369
509
|
}
|
|
370
510
|
});
|
|
371
511
|
});
|
|
512
|
+
// Endpoint to download the matter storage file
|
|
372
513
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
373
514
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
374
515
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
|
|
@@ -379,6 +520,7 @@ export class Frontend {
|
|
|
379
520
|
}
|
|
380
521
|
});
|
|
381
522
|
});
|
|
523
|
+
// Endpoint to download the matterbridge plugin directory
|
|
382
524
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
383
525
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
384
526
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
@@ -389,6 +531,7 @@ export class Frontend {
|
|
|
389
531
|
}
|
|
390
532
|
});
|
|
391
533
|
});
|
|
534
|
+
// Endpoint to download the matterbridge plugin config files
|
|
392
535
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
393
536
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
394
537
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
@@ -399,6 +542,7 @@ export class Frontend {
|
|
|
399
542
|
}
|
|
400
543
|
});
|
|
401
544
|
});
|
|
545
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
402
546
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
403
547
|
this.log.debug('The frontend sent /api/download-backup');
|
|
404
548
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
@@ -408,6 +552,7 @@ export class Frontend {
|
|
|
408
552
|
}
|
|
409
553
|
});
|
|
410
554
|
});
|
|
555
|
+
// Endpoint to upload a package
|
|
411
556
|
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
412
557
|
const { filename } = req.body;
|
|
413
558
|
const file = req.file;
|
|
@@ -417,10 +562,13 @@ export class Frontend {
|
|
|
417
562
|
return;
|
|
418
563
|
}
|
|
419
564
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
565
|
+
// Define the path where the plugin file will be saved
|
|
420
566
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
421
567
|
try {
|
|
568
|
+
// Move the uploaded file to the specified path
|
|
422
569
|
await fs.rename(file.path, filePath);
|
|
423
570
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
571
|
+
// Install the plugin package
|
|
424
572
|
if (filename.endsWith('.tgz')) {
|
|
425
573
|
await this.matterbridge.spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
|
|
426
574
|
this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
|
|
@@ -439,6 +587,7 @@ export class Frontend {
|
|
|
439
587
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
440
588
|
}
|
|
441
589
|
});
|
|
590
|
+
// Fallback for routing (must be the last route)
|
|
442
591
|
this.expressApp.use((req, res) => {
|
|
443
592
|
this.log.debug('The frontend sent:', req.url);
|
|
444
593
|
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
@@ -446,6 +595,7 @@ export class Frontend {
|
|
|
446
595
|
this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
|
|
447
596
|
}
|
|
448
597
|
async stop() {
|
|
598
|
+
// Close the http server
|
|
449
599
|
if (this.httpServer) {
|
|
450
600
|
this.httpServer.close((error) => {
|
|
451
601
|
if (error) {
|
|
@@ -459,6 +609,7 @@ export class Frontend {
|
|
|
459
609
|
this.httpServer = undefined;
|
|
460
610
|
this.log.debug('Frontend http server closed successfully');
|
|
461
611
|
}
|
|
612
|
+
// Close the https server
|
|
462
613
|
if (this.httpsServer) {
|
|
463
614
|
this.httpsServer.close((error) => {
|
|
464
615
|
if (error) {
|
|
@@ -472,12 +623,15 @@ export class Frontend {
|
|
|
472
623
|
this.httpsServer = undefined;
|
|
473
624
|
this.log.debug('Frontend https server closed successfully');
|
|
474
625
|
}
|
|
626
|
+
// Remove listeners from the express app
|
|
475
627
|
if (this.expressApp) {
|
|
476
628
|
this.expressApp.removeAllListeners();
|
|
477
629
|
this.expressApp = undefined;
|
|
478
630
|
this.log.debug('Frontend app closed successfully');
|
|
479
631
|
}
|
|
632
|
+
// Close the WebSocket server
|
|
480
633
|
if (this.webSocketServer) {
|
|
634
|
+
// Close all active connections
|
|
481
635
|
this.webSocketServer.clients.forEach((client) => {
|
|
482
636
|
if (client.readyState === WebSocket.OPEN) {
|
|
483
637
|
client.close();
|
|
@@ -491,9 +645,11 @@ export class Frontend {
|
|
|
491
645
|
this.log.debug('WebSocket server closed successfully');
|
|
492
646
|
}
|
|
493
647
|
});
|
|
648
|
+
// this.webSocketServer.removeAllListeners();
|
|
494
649
|
this.webSocketServer = undefined;
|
|
495
650
|
}
|
|
496
651
|
}
|
|
652
|
+
// Function to format bytes to KB, MB, or GB
|
|
497
653
|
formatMemoryUsage = (bytes) => {
|
|
498
654
|
if (bytes >= 1024 ** 3) {
|
|
499
655
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -505,6 +661,7 @@ export class Frontend {
|
|
|
505
661
|
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
506
662
|
}
|
|
507
663
|
};
|
|
664
|
+
// Function to format system uptime with only the most significant unit
|
|
508
665
|
formatOsUpTime = (seconds) => {
|
|
509
666
|
if (seconds >= 86400) {
|
|
510
667
|
const days = Math.floor(seconds / 86400);
|
|
@@ -520,8 +677,14 @@ export class Frontend {
|
|
|
520
677
|
}
|
|
521
678
|
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
522
679
|
};
|
|
680
|
+
/**
|
|
681
|
+
* Retrieves the api settings data.
|
|
682
|
+
*
|
|
683
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
684
|
+
*/
|
|
523
685
|
async getApiSettings() {
|
|
524
686
|
const { lastCpuUsage } = await import('./cli.js');
|
|
687
|
+
// Update the system information
|
|
525
688
|
this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
|
|
526
689
|
this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
|
|
527
690
|
this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
|
|
@@ -530,6 +693,7 @@ export class Frontend {
|
|
|
530
693
|
this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
|
|
531
694
|
this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
|
|
532
695
|
this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
|
|
696
|
+
// Update the matterbridge information
|
|
533
697
|
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
534
698
|
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
535
699
|
this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
|
|
@@ -548,6 +712,11 @@ export class Frontend {
|
|
|
548
712
|
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
549
713
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
550
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Retrieves the reachable attribute.
|
|
717
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
718
|
+
* @returns {boolean} The reachable attribute.
|
|
719
|
+
*/
|
|
551
720
|
getReachability(device) {
|
|
552
721
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
553
722
|
return false;
|
|
@@ -572,13 +741,20 @@ export class Frontend {
|
|
|
572
741
|
}
|
|
573
742
|
return;
|
|
574
743
|
};
|
|
744
|
+
// Root endpoint
|
|
575
745
|
if (device.hasClusterServer(PowerSource.Cluster.id))
|
|
576
746
|
return powerSource(device);
|
|
747
|
+
// Child endpoints
|
|
577
748
|
for (const child of device.getChildEndpoints()) {
|
|
578
749
|
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
579
750
|
return powerSource(child);
|
|
580
751
|
}
|
|
581
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Retrieves the cluster text description from a given device.
|
|
755
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
756
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
757
|
+
*/
|
|
582
758
|
getClusterTextFromDevice(device) {
|
|
583
759
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
584
760
|
return '';
|
|
@@ -620,6 +796,7 @@ export class Frontend {
|
|
|
620
796
|
let attributes = '';
|
|
621
797
|
let supportedModes = [];
|
|
622
798
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
799
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
623
800
|
if (typeof attributeValue === 'undefined')
|
|
624
801
|
return;
|
|
625
802
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -711,8 +888,13 @@ export class Frontend {
|
|
|
711
888
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
712
889
|
attributes += `${getUserLabel(device)} `;
|
|
713
890
|
});
|
|
891
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
714
892
|
return attributes.trimStart().trimEnd();
|
|
715
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
896
|
+
* @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
|
|
897
|
+
*/
|
|
716
898
|
getBaseRegisteredPlugins() {
|
|
717
899
|
const baseRegisteredPlugins = [];
|
|
718
900
|
for (const plugin of this.matterbridge.plugins) {
|
|
@@ -751,11 +933,18 @@ export class Frontend {
|
|
|
751
933
|
}
|
|
752
934
|
return baseRegisteredPlugins;
|
|
753
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Retrieves the devices from Matterbridge.
|
|
938
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
939
|
+
* @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices.
|
|
940
|
+
*/
|
|
754
941
|
async getDevices(pluginName) {
|
|
755
942
|
const devices = [];
|
|
756
943
|
this.matterbridge.devices.forEach(async (device) => {
|
|
944
|
+
// Filter by pluginName if provided
|
|
757
945
|
if (pluginName && pluginName !== device.plugin)
|
|
758
946
|
return;
|
|
947
|
+
// Check if the device has the required properties
|
|
759
948
|
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
760
949
|
return;
|
|
761
950
|
const cluster = this.getClusterTextFromDevice(device);
|
|
@@ -775,6 +964,13 @@ export class Frontend {
|
|
|
775
964
|
});
|
|
776
965
|
return devices;
|
|
777
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* Handles incoming websocket messages for the Matterbridge frontend.
|
|
969
|
+
*
|
|
970
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
971
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
972
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
973
|
+
*/
|
|
778
974
|
async wsMessageHandler(client, message) {
|
|
779
975
|
let data;
|
|
780
976
|
try {
|
|
@@ -821,8 +1017,10 @@ export class Frontend {
|
|
|
821
1017
|
this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
|
|
822
1018
|
const packageName = data.params.packageName.replace(/@.*$/, '');
|
|
823
1019
|
if (data.params.restart === false && packageName !== 'matterbridge') {
|
|
1020
|
+
// The install comes from InstallPlugins
|
|
824
1021
|
this.matterbridge.plugins.add(packageName).then((plugin) => {
|
|
825
1022
|
if (plugin) {
|
|
1023
|
+
// The plugin is not registered
|
|
826
1024
|
this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
|
|
827
1025
|
this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => {
|
|
828
1026
|
this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
|
|
@@ -830,6 +1028,7 @@ export class Frontend {
|
|
|
830
1028
|
});
|
|
831
1029
|
}
|
|
832
1030
|
else {
|
|
1031
|
+
// The plugin is already registered
|
|
833
1032
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
834
1033
|
this.wssSendRefreshRequired('plugins');
|
|
835
1034
|
this.wssSendRestartRequired();
|
|
@@ -837,6 +1036,7 @@ export class Frontend {
|
|
|
837
1036
|
});
|
|
838
1037
|
}
|
|
839
1038
|
else {
|
|
1039
|
+
// The package is matterbridge
|
|
840
1040
|
if (this.matterbridge.restartMode !== '') {
|
|
841
1041
|
this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
|
|
842
1042
|
this.matterbridge.shutdownProcess();
|
|
@@ -858,6 +1058,7 @@ export class Frontend {
|
|
|
858
1058
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
|
|
859
1059
|
return;
|
|
860
1060
|
}
|
|
1061
|
+
// The package is a plugin
|
|
861
1062
|
const plugin = this.matterbridge.plugins.get(data.params.packageName);
|
|
862
1063
|
if (plugin) {
|
|
863
1064
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
@@ -866,6 +1067,7 @@ export class Frontend {
|
|
|
866
1067
|
this.wssSendRefreshRequired('plugins');
|
|
867
1068
|
this.wssSendRefreshRequired('devices');
|
|
868
1069
|
}
|
|
1070
|
+
// Uninstall the package
|
|
869
1071
|
this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
|
|
870
1072
|
this.matterbridge
|
|
871
1073
|
.spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
|
|
@@ -1142,6 +1344,7 @@ export class Frontend {
|
|
|
1142
1344
|
});
|
|
1143
1345
|
endpointServer.getChildEndpoints().forEach((childEndpoint) => {
|
|
1144
1346
|
deviceTypes = [];
|
|
1347
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1145
1348
|
const name = childEndpoint.endpoint?.id;
|
|
1146
1349
|
const clusterServers = childEndpoint.getAllClusterServers();
|
|
1147
1350
|
clusterServers.forEach((clusterServer) => {
|
|
@@ -1255,22 +1458,22 @@ export class Frontend {
|
|
|
1255
1458
|
if (isValidString(data.params.value, 4)) {
|
|
1256
1459
|
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
1257
1460
|
if (data.params.value === 'Debug') {
|
|
1258
|
-
await this.matterbridge.setLogLevel("debug");
|
|
1461
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
1259
1462
|
}
|
|
1260
1463
|
else if (data.params.value === 'Info') {
|
|
1261
|
-
await this.matterbridge.setLogLevel("info");
|
|
1464
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
1262
1465
|
}
|
|
1263
1466
|
else if (data.params.value === 'Notice') {
|
|
1264
|
-
await this.matterbridge.setLogLevel("notice");
|
|
1467
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
1265
1468
|
}
|
|
1266
1469
|
else if (data.params.value === 'Warn') {
|
|
1267
|
-
await this.matterbridge.setLogLevel("warn");
|
|
1470
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
1268
1471
|
}
|
|
1269
1472
|
else if (data.params.value === 'Error') {
|
|
1270
|
-
await this.matterbridge.setLogLevel("error");
|
|
1473
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
1271
1474
|
}
|
|
1272
1475
|
else if (data.params.value === 'Fatal') {
|
|
1273
|
-
await this.matterbridge.setLogLevel("fatal");
|
|
1476
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
1274
1477
|
}
|
|
1275
1478
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
1276
1479
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
|
|
@@ -1281,6 +1484,7 @@ export class Frontend {
|
|
|
1281
1484
|
this.log.debug('Matterbridge file log:', data.params.value);
|
|
1282
1485
|
this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
|
|
1283
1486
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
1487
|
+
// Create the file logger for matterbridge
|
|
1284
1488
|
if (data.params.value)
|
|
1285
1489
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
|
|
1286
1490
|
else
|
|
@@ -1436,15 +1640,19 @@ export class Frontend {
|
|
|
1436
1640
|
return;
|
|
1437
1641
|
}
|
|
1438
1642
|
const config = plugin.configJson;
|
|
1643
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1439
1644
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1645
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1440
1646
|
if (select === 'serial')
|
|
1441
1647
|
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
1442
1648
|
if (select === 'name')
|
|
1443
1649
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1444
1650
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
1651
|
+
// Remove postfix from the serial if it exists
|
|
1445
1652
|
if (config.postfix) {
|
|
1446
1653
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1447
1654
|
}
|
|
1655
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1448
1656
|
if (isValidArray(config.whiteList, 1)) {
|
|
1449
1657
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1450
1658
|
config.whiteList.push(data.params.serial);
|
|
@@ -1453,6 +1661,7 @@ export class Frontend {
|
|
|
1453
1661
|
config.whiteList.push(data.params.name);
|
|
1454
1662
|
}
|
|
1455
1663
|
}
|
|
1664
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1456
1665
|
if (isValidArray(config.blackList, 1)) {
|
|
1457
1666
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1458
1667
|
config.blackList = config.blackList.filter((item) => item !== data.params.serial);
|
|
@@ -1482,7 +1691,9 @@ export class Frontend {
|
|
|
1482
1691
|
return;
|
|
1483
1692
|
}
|
|
1484
1693
|
const config = plugin.configJson;
|
|
1694
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1485
1695
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1696
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1486
1697
|
if (select === 'serial')
|
|
1487
1698
|
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
1488
1699
|
if (select === 'name')
|
|
@@ -1491,6 +1702,7 @@ export class Frontend {
|
|
|
1491
1702
|
if (config.postfix) {
|
|
1492
1703
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1493
1704
|
}
|
|
1705
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1494
1706
|
if (isValidArray(config.whiteList, 1)) {
|
|
1495
1707
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1496
1708
|
config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
|
|
@@ -1499,6 +1711,7 @@ export class Frontend {
|
|
|
1499
1711
|
config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
|
|
1500
1712
|
}
|
|
1501
1713
|
}
|
|
1714
|
+
// Add the serial to the blackList
|
|
1502
1715
|
if (isValidArray(config.blackList)) {
|
|
1503
1716
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1504
1717
|
config.blackList.push(data.params.serial);
|
|
@@ -1531,114 +1744,219 @@ export class Frontend {
|
|
|
1531
1744
|
this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
|
|
1532
1745
|
}
|
|
1533
1746
|
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
1749
|
+
*
|
|
1750
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
1751
|
+
* @param {string} time - The time string of the message
|
|
1752
|
+
* @param {string} name - The logger name of the message
|
|
1753
|
+
* @param {string} message - The content of the message.
|
|
1754
|
+
*
|
|
1755
|
+
* @remark
|
|
1756
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
1757
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
1758
|
+
* The function sends the message to all connected clients.
|
|
1759
|
+
*/
|
|
1534
1760
|
wssSendMessage(level, time, name, message) {
|
|
1535
1761
|
if (!level || !time || !name || !message)
|
|
1536
1762
|
return;
|
|
1763
|
+
// Remove ANSI escape codes from the message
|
|
1764
|
+
// eslint-disable-next-line no-control-regex
|
|
1537
1765
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
1766
|
+
// Remove leading asterisks from the message
|
|
1538
1767
|
message = message.replace(/^\*+/, '');
|
|
1768
|
+
// Replace all occurrences of \t and \n
|
|
1539
1769
|
message = message.replace(/[\t\n]/g, '');
|
|
1770
|
+
// Remove non-printable characters
|
|
1771
|
+
// eslint-disable-next-line no-control-regex
|
|
1540
1772
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1773
|
+
// Replace all occurrences of \" with "
|
|
1541
1774
|
message = message.replace(/\\"/g, '"');
|
|
1775
|
+
// Replace all occurrences of angle-brackets with < and >"
|
|
1542
1776
|
message = message.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1777
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1543
1778
|
const maxContinuousLength = 100;
|
|
1544
1779
|
const keepStartLength = 20;
|
|
1545
1780
|
const keepEndLength = 20;
|
|
1781
|
+
// Split the message into words
|
|
1546
1782
|
message = message
|
|
1547
1783
|
.split(' ')
|
|
1548
1784
|
.map((word) => {
|
|
1785
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1549
1786
|
if (word.length > maxContinuousLength) {
|
|
1550
1787
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1551
1788
|
}
|
|
1552
1789
|
return word;
|
|
1553
1790
|
})
|
|
1554
1791
|
.join(' ');
|
|
1792
|
+
// Send the message to all connected clients
|
|
1555
1793
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1556
1794
|
if (client.readyState === WebSocket.OPEN) {
|
|
1557
1795
|
client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
|
|
1558
1796
|
}
|
|
1559
1797
|
});
|
|
1560
1798
|
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
1801
|
+
*
|
|
1802
|
+
* @param {string} changed - The changed value. If null, the whole page will be refreshed.
|
|
1803
|
+
* possible values:
|
|
1804
|
+
* - 'matterbridgeLatestVersion'
|
|
1805
|
+
* - 'matterbridgeAdvertise'
|
|
1806
|
+
* - 'online'
|
|
1807
|
+
* - 'offline'
|
|
1808
|
+
* - 'reachability'
|
|
1809
|
+
* - 'settings'
|
|
1810
|
+
* - 'plugins'
|
|
1811
|
+
* - 'pluginsRestart'
|
|
1812
|
+
* - 'devices'
|
|
1813
|
+
* - 'fabrics'
|
|
1814
|
+
* - 'sessions'
|
|
1815
|
+
*/
|
|
1561
1816
|
wssSendRefreshRequired(changed = null) {
|
|
1562
1817
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
1818
|
+
// Send the message to all connected clients
|
|
1563
1819
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1564
1820
|
if (client.readyState === WebSocket.OPEN) {
|
|
1565
1821
|
client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
|
|
1566
1822
|
}
|
|
1567
1823
|
});
|
|
1568
1824
|
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
1827
|
+
*
|
|
1828
|
+
*/
|
|
1569
1829
|
wssSendRestartRequired(snackbar = true) {
|
|
1570
1830
|
this.log.debug('Sending a restart required message to all connected clients');
|
|
1571
1831
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1572
1832
|
if (snackbar === true)
|
|
1573
1833
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
1834
|
+
// Send the message to all connected clients
|
|
1574
1835
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1575
1836
|
if (client.readyState === WebSocket.OPEN) {
|
|
1576
1837
|
client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
|
|
1577
1838
|
}
|
|
1578
1839
|
});
|
|
1579
1840
|
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
1843
|
+
*
|
|
1844
|
+
*/
|
|
1580
1845
|
wssSendUpdateRequired() {
|
|
1581
1846
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1582
1847
|
this.matterbridge.matterbridgeInformation.updateRequired = true;
|
|
1848
|
+
// Send the message to all connected clients
|
|
1583
1849
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1584
1850
|
if (client.readyState === WebSocket.OPEN) {
|
|
1585
1851
|
client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
|
|
1586
1852
|
}
|
|
1587
1853
|
});
|
|
1588
1854
|
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Sends a cpu update message to all connected clients.
|
|
1857
|
+
*
|
|
1858
|
+
*/
|
|
1589
1859
|
wssSendCpuUpdate(cpuUsage) {
|
|
1590
1860
|
if (hasParameter('debug'))
|
|
1591
1861
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
1862
|
+
// Send the message to all connected clients
|
|
1592
1863
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1593
1864
|
if (client.readyState === WebSocket.OPEN) {
|
|
1594
1865
|
client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
|
|
1595
1866
|
}
|
|
1596
1867
|
});
|
|
1597
1868
|
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Sends a memory update message to all connected clients.
|
|
1871
|
+
*
|
|
1872
|
+
*/
|
|
1598
1873
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1599
1874
|
if (hasParameter('debug'))
|
|
1600
1875
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
1876
|
+
// Send the message to all connected clients
|
|
1601
1877
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1602
1878
|
if (client.readyState === WebSocket.OPEN) {
|
|
1603
1879
|
client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
|
|
1604
1880
|
}
|
|
1605
1881
|
});
|
|
1606
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Sends an uptime update message to all connected clients.
|
|
1885
|
+
*
|
|
1886
|
+
*/
|
|
1607
1887
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1608
1888
|
if (hasParameter('debug'))
|
|
1609
1889
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
1890
|
+
// Send the message to all connected clients
|
|
1610
1891
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1611
1892
|
if (client.readyState === WebSocket.OPEN) {
|
|
1612
1893
|
client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
|
|
1613
1894
|
}
|
|
1614
1895
|
});
|
|
1615
1896
|
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Sends an open snackbar message to all connected clients.
|
|
1899
|
+
* @param {string} message - The message to send.
|
|
1900
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message.
|
|
1901
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
|
|
1902
|
+
*
|
|
1903
|
+
*/
|
|
1616
1904
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1617
1905
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
1906
|
+
// Send the message to all connected clients
|
|
1618
1907
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1619
1908
|
if (client.readyState === WebSocket.OPEN) {
|
|
1620
1909
|
client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
|
|
1621
1910
|
}
|
|
1622
1911
|
});
|
|
1623
1912
|
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Sends a close snackbar message to all connected clients.
|
|
1915
|
+
* @param {string} message - The message to send.
|
|
1916
|
+
*
|
|
1917
|
+
*/
|
|
1624
1918
|
wssSendCloseSnackbarMessage(message) {
|
|
1625
1919
|
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
1920
|
+
// Send the message to all connected clients
|
|
1626
1921
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1627
1922
|
if (client.readyState === WebSocket.OPEN) {
|
|
1628
1923
|
client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
|
|
1629
1924
|
}
|
|
1630
1925
|
});
|
|
1631
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
1929
|
+
*
|
|
1930
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
1931
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
1932
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
1933
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
1934
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
1935
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
1936
|
+
*
|
|
1937
|
+
* @remarks
|
|
1938
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
1939
|
+
* with the updated attribute information.
|
|
1940
|
+
*/
|
|
1632
1941
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
|
|
1633
1942
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
1943
|
+
// Send the message to all connected clients
|
|
1634
1944
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1635
1945
|
if (client.readyState === WebSocket.OPEN) {
|
|
1636
1946
|
client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
|
|
1637
1947
|
}
|
|
1638
1948
|
});
|
|
1639
1949
|
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Sends a message to all connected clients.
|
|
1952
|
+
* @param {number} id - The message id.
|
|
1953
|
+
* @param {string} method - The message method.
|
|
1954
|
+
* @param {Record<string, string | number | boolean>} params - The message parameters.
|
|
1955
|
+
*
|
|
1956
|
+
*/
|
|
1640
1957
|
wssBroadcastMessage(id, method, params) {
|
|
1641
1958
|
this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
|
|
1959
|
+
// Send the message to all connected clients
|
|
1642
1960
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1643
1961
|
if (client.readyState === WebSocket.OPEN) {
|
|
1644
1962
|
client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
|
|
@@ -1646,3 +1964,4 @@ export class Frontend {
|
|
|
1646
1964
|
});
|
|
1647
1965
|
}
|
|
1648
1966
|
}
|
|
1967
|
+
//# sourceMappingURL=frontend.js.map
|