matterbridge 2.1.5-dev.7 → 2.1.5
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/CHANGELOG.md +4 -2
- package/README-DOCKER.md +6 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +26 -0
- 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 +143 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +270 -24
- package/dist/frontend.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- 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 +409 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +750 -41
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +39 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +33 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +1056 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +32 -1
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +177 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +112 -11
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +39 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +33 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +835 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +690 -6
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +2275 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +117 -9
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +159 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +121 -5
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +169 -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 +236 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +231 -4
- package/dist/pluginManager.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/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/export.d.ts +3 -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/utils.d.ts +231 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +264 -10
- package/dist/utils/utils.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/frontend.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
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 } from '@matter/main';
|
|
25
|
+
// Node modules
|
|
2
26
|
import { createServer } from 'http';
|
|
3
27
|
import https from 'https';
|
|
4
28
|
import express from 'express';
|
|
@@ -6,16 +30,47 @@ import WebSocket, { WebSocketServer } from 'ws';
|
|
|
6
30
|
import os from 'os';
|
|
7
31
|
import path from 'path';
|
|
8
32
|
import { promises as fs } from 'fs';
|
|
33
|
+
// AnsiLogger module
|
|
9
34
|
import { AnsiLogger, CYAN, db, debugStringify, er, nf, rs, stringify, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from './logger/export.js';
|
|
35
|
+
// Matterbridge
|
|
10
36
|
import { createZip, deepCopy, getIntParameter, hasParameter, isValidNumber, isValidObject, isValidString } from './utils/utils.js';
|
|
11
37
|
import { plg } from './matterbridgeTypes.js';
|
|
12
38
|
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
|
|
39
|
+
/**
|
|
40
|
+
* Websocket message ID for logging.
|
|
41
|
+
* @constant {number}
|
|
42
|
+
*/
|
|
13
43
|
export const WS_ID_LOG = 0;
|
|
44
|
+
/**
|
|
45
|
+
* Websocket message ID indicating a refresh is needed.
|
|
46
|
+
* @constant {number}
|
|
47
|
+
*/
|
|
14
48
|
export const WS_ID_REFRESH_NEEDED = 1;
|
|
49
|
+
/**
|
|
50
|
+
* Websocket message ID indicating a restart is needed.
|
|
51
|
+
* @constant {number}
|
|
52
|
+
*/
|
|
15
53
|
export const WS_ID_RESTART_NEEDED = 2;
|
|
54
|
+
/**
|
|
55
|
+
* Websocket message ID indicating a cpu update.
|
|
56
|
+
* @constant {number}
|
|
57
|
+
*/
|
|
16
58
|
export const WS_ID_CPU_UPDATE = 3;
|
|
59
|
+
/**
|
|
60
|
+
* Websocket message ID indicating a memory update.
|
|
61
|
+
* @constant {number}
|
|
62
|
+
*/
|
|
17
63
|
export const WS_ID_MEMORY_UPDATE = 4;
|
|
64
|
+
/**
|
|
65
|
+
* Websocket message ID indicating a memory update.
|
|
66
|
+
* @constant {number}
|
|
67
|
+
*/
|
|
18
68
|
export const WS_ID_SNACKBAR = 5;
|
|
69
|
+
/**
|
|
70
|
+
* Initializes the frontend of Matterbridge.
|
|
71
|
+
*
|
|
72
|
+
* @param port The port number to run the frontend server on. Default is 8283.
|
|
73
|
+
*/
|
|
19
74
|
export class Frontend {
|
|
20
75
|
matterbridge;
|
|
21
76
|
log;
|
|
@@ -32,7 +87,7 @@ export class Frontend {
|
|
|
32
87
|
memoryTimeout;
|
|
33
88
|
constructor(matterbridge) {
|
|
34
89
|
this.matterbridge = matterbridge;
|
|
35
|
-
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4
|
|
90
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
36
91
|
}
|
|
37
92
|
set logLevel(logLevel) {
|
|
38
93
|
this.log.logLevel = logLevel;
|
|
@@ -40,10 +95,21 @@ export class Frontend {
|
|
|
40
95
|
async start(port = 8283) {
|
|
41
96
|
this.port = port;
|
|
42
97
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
98
|
+
// Create the express app that serves the frontend
|
|
43
99
|
this.expressApp = express();
|
|
100
|
+
// Log all requests to the server for debugging
|
|
101
|
+
/*
|
|
102
|
+
this.expressApp.use((req, res, next) => {
|
|
103
|
+
this.log.debug(`Received request on expressApp: ${req.method} ${req.url}`);
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
*/
|
|
107
|
+
// Serve static files from '/static' endpoint
|
|
44
108
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
45
109
|
if (!hasParameter('ssl')) {
|
|
110
|
+
// Create an HTTP server and attach the express app
|
|
46
111
|
this.httpServer = createServer(this.expressApp);
|
|
112
|
+
// Listen on the specified port
|
|
47
113
|
if (hasParameter('ingress')) {
|
|
48
114
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
49
115
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -57,6 +123,7 @@ export class Frontend {
|
|
|
57
123
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
58
124
|
});
|
|
59
125
|
}
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
127
|
this.httpServer.on('error', (error) => {
|
|
61
128
|
this.log.error(`Frontend http server error listening on ${this.port}`);
|
|
62
129
|
switch (error.code) {
|
|
@@ -72,6 +139,7 @@ export class Frontend {
|
|
|
72
139
|
});
|
|
73
140
|
}
|
|
74
141
|
else {
|
|
142
|
+
// Load the SSL certificate, the private key and optionally the CA certificate
|
|
75
143
|
let cert;
|
|
76
144
|
try {
|
|
77
145
|
cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
@@ -99,7 +167,9 @@ export class Frontend {
|
|
|
99
167
|
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
|
|
100
168
|
}
|
|
101
169
|
const serverOptions = { cert, key, ca };
|
|
170
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
102
171
|
this.httpsServer = https.createServer(serverOptions, this.expressApp);
|
|
172
|
+
// Listen on the specified port
|
|
103
173
|
if (hasParameter('ingress')) {
|
|
104
174
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
105
175
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -113,6 +183,7 @@ export class Frontend {
|
|
|
113
183
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
114
184
|
});
|
|
115
185
|
}
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
187
|
this.httpsServer.on('error', (error) => {
|
|
117
188
|
this.log.error(`Frontend https server error listening on ${this.port}`);
|
|
118
189
|
switch (error.code) {
|
|
@@ -129,12 +200,20 @@ export class Frontend {
|
|
|
129
200
|
}
|
|
130
201
|
if (this.initializeError)
|
|
131
202
|
return;
|
|
203
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
132
204
|
const wssPort = this.port;
|
|
133
205
|
const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
|
|
134
206
|
this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
|
|
135
207
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
136
208
|
const clientIp = request.socket.remoteAddress;
|
|
137
|
-
|
|
209
|
+
// Set the global logger callback for the WebSocketServer
|
|
210
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
211
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
|
|
212
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
213
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
|
|
214
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
215
|
+
AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
|
|
216
|
+
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
138
217
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
139
218
|
ws.on('message', (message) => {
|
|
140
219
|
this.wsMessageHandler(ws, message);
|
|
@@ -166,9 +245,11 @@ export class Frontend {
|
|
|
166
245
|
this.webSocketServer.on('error', (ws, error) => {
|
|
167
246
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
168
247
|
});
|
|
248
|
+
// Start the memory dump interval
|
|
169
249
|
if (hasParameter('memorydump')) {
|
|
170
250
|
this.startCpuMemoryDump();
|
|
171
251
|
}
|
|
252
|
+
// Endpoint to validate login code
|
|
172
253
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
173
254
|
const { password } = req.body;
|
|
174
255
|
this.log.debug('The frontend sent /api/login', password);
|
|
@@ -187,23 +268,27 @@ export class Frontend {
|
|
|
187
268
|
this.log.warn('/api/login error wrong password');
|
|
188
269
|
res.json({ valid: false });
|
|
189
270
|
}
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
190
272
|
}
|
|
191
273
|
catch (error) {
|
|
192
274
|
this.log.error('/api/login error getting password');
|
|
193
275
|
res.json({ valid: false });
|
|
194
276
|
}
|
|
195
277
|
});
|
|
278
|
+
// Endpoint to provide health check
|
|
196
279
|
this.expressApp.get('/health', (req, res) => {
|
|
197
280
|
this.log.debug('Express received /health');
|
|
198
281
|
const healthStatus = {
|
|
199
|
-
status: 'ok',
|
|
200
|
-
uptime: process.uptime(),
|
|
201
|
-
timestamp: new Date().toISOString(),
|
|
282
|
+
status: 'ok', // Indicate service is healthy
|
|
283
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
284
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
202
285
|
};
|
|
203
286
|
res.status(200).json(healthStatus);
|
|
204
287
|
});
|
|
288
|
+
// Endpoint to provide memory usage details
|
|
205
289
|
this.expressApp.get('/memory', async (req, res) => {
|
|
206
290
|
this.log.debug('Express received /memory');
|
|
291
|
+
// Memory usage from process
|
|
207
292
|
const memoryUsageRaw = process.memoryUsage();
|
|
208
293
|
const memoryUsage = {
|
|
209
294
|
rss: this.formatMemoryUsage(memoryUsageRaw.rss),
|
|
@@ -212,10 +297,13 @@ export class Frontend {
|
|
|
212
297
|
external: this.formatMemoryUsage(memoryUsageRaw.external),
|
|
213
298
|
arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
|
|
214
299
|
};
|
|
300
|
+
// V8 heap statistics
|
|
215
301
|
const { default: v8 } = await import('node:v8');
|
|
216
302
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
217
303
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
304
|
+
// Format heapStats
|
|
218
305
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
|
|
306
|
+
// Format heapSpaces
|
|
219
307
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
220
308
|
...space,
|
|
221
309
|
space_size: this.formatMemoryUsage(space.space_size),
|
|
@@ -225,6 +313,23 @@ export class Frontend {
|
|
|
225
313
|
}));
|
|
226
314
|
const { default: module } = await import('module');
|
|
227
315
|
const loadedModules = module._cache ? Object.keys(module._cache).sort() : [];
|
|
316
|
+
/*
|
|
317
|
+
if (req.query.heapdump === 'true') {
|
|
318
|
+
const { default: heapdump } = await import('heapdump');
|
|
319
|
+
const filename = `heapdump-${Date.now()}.heapsnapshot`;
|
|
320
|
+
|
|
321
|
+
heapdump.writeSnapshot(filename, (err, dumpFilename) => {
|
|
322
|
+
if (err) {
|
|
323
|
+
this.log.error(`Heap dump error: ${err.message}`);
|
|
324
|
+
return res.status(500).json({ error: 'Heap dump failed', details: err.message });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.log.info(`Heap dump written to ${dumpFilename}`);
|
|
328
|
+
return res.status(200).json({ ...memoryReport, heapdump: dumpFilename });
|
|
329
|
+
});
|
|
330
|
+
return; // Exit early since heapdump response is handled inside callback
|
|
331
|
+
}
|
|
332
|
+
*/
|
|
228
333
|
const memoryReport = {
|
|
229
334
|
memoryUsage,
|
|
230
335
|
heapStats,
|
|
@@ -233,6 +338,7 @@ export class Frontend {
|
|
|
233
338
|
};
|
|
234
339
|
res.status(200).json(memoryReport);
|
|
235
340
|
});
|
|
341
|
+
// Endpoint to start advertising the server node
|
|
236
342
|
this.expressApp.get('/api/advertise', express.json(), async (req, res) => {
|
|
237
343
|
const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
|
|
238
344
|
if (pairingCodes) {
|
|
@@ -243,19 +349,24 @@ export class Frontend {
|
|
|
243
349
|
res.status(500).json({ error: 'Failed to generate pairing codes' });
|
|
244
350
|
}
|
|
245
351
|
});
|
|
352
|
+
// Endpoint to provide settings
|
|
246
353
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
247
354
|
this.log.debug('The frontend sent /api/settings');
|
|
248
355
|
res.json(await this.getApiSettings());
|
|
249
356
|
});
|
|
357
|
+
// Endpoint to provide plugins
|
|
250
358
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
251
359
|
this.log.debug('The frontend sent /api/plugins');
|
|
252
360
|
const response = this.getBaseRegisteredPlugins();
|
|
361
|
+
// this.log.debug('Response:', debugStringify(response));
|
|
253
362
|
res.json(response);
|
|
254
363
|
});
|
|
364
|
+
// Endpoint to provide devices
|
|
255
365
|
this.expressApp.get('/api/devices', (req, res) => {
|
|
256
366
|
this.log.debug('The frontend sent /api/devices');
|
|
257
367
|
const devices = [];
|
|
258
368
|
this.matterbridge.devices.forEach(async (device) => {
|
|
369
|
+
// Check if the device has the required properties
|
|
259
370
|
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
|
|
260
371
|
return;
|
|
261
372
|
const cluster = this.getClusterTextFromDevice(device);
|
|
@@ -271,8 +382,10 @@ export class Frontend {
|
|
|
271
382
|
cluster: cluster,
|
|
272
383
|
});
|
|
273
384
|
});
|
|
385
|
+
// this.log.debug('Response:', debugStringify(data));
|
|
274
386
|
res.json(devices);
|
|
275
387
|
});
|
|
388
|
+
// Endpoint to provide the cluster servers of the devices
|
|
276
389
|
this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
|
|
277
390
|
const selectedPluginName = req.params.selectedPluginName;
|
|
278
391
|
const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
|
|
@@ -345,6 +458,7 @@ export class Frontend {
|
|
|
345
458
|
});
|
|
346
459
|
res.json(data);
|
|
347
460
|
});
|
|
461
|
+
// Endpoint to view the log
|
|
348
462
|
this.expressApp.get('/api/view-log', async (req, res) => {
|
|
349
463
|
this.log.debug('The frontend sent /api/log');
|
|
350
464
|
try {
|
|
@@ -357,10 +471,12 @@ export class Frontend {
|
|
|
357
471
|
res.status(500).send('Error reading log file');
|
|
358
472
|
}
|
|
359
473
|
});
|
|
474
|
+
// Endpoint to download the matterbridge log
|
|
360
475
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
361
476
|
this.log.debug('The frontend sent /api/download-mblog');
|
|
362
477
|
try {
|
|
363
478
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
|
|
479
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
364
480
|
}
|
|
365
481
|
catch (error) {
|
|
366
482
|
fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
|
|
@@ -372,10 +488,12 @@ export class Frontend {
|
|
|
372
488
|
}
|
|
373
489
|
});
|
|
374
490
|
});
|
|
491
|
+
// Endpoint to download the matter log
|
|
375
492
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
376
493
|
this.log.debug('The frontend sent /api/download-mjlog');
|
|
377
494
|
try {
|
|
378
495
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
|
|
496
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
379
497
|
}
|
|
380
498
|
catch (error) {
|
|
381
499
|
fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
|
|
@@ -387,6 +505,7 @@ export class Frontend {
|
|
|
387
505
|
}
|
|
388
506
|
});
|
|
389
507
|
});
|
|
508
|
+
// Endpoint to download the matter storage file
|
|
390
509
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
391
510
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
392
511
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
|
|
@@ -397,6 +516,7 @@ export class Frontend {
|
|
|
397
516
|
}
|
|
398
517
|
});
|
|
399
518
|
});
|
|
519
|
+
// Endpoint to download the matterbridge storage directory
|
|
400
520
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
401
521
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
402
522
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
|
|
@@ -407,6 +527,7 @@ export class Frontend {
|
|
|
407
527
|
}
|
|
408
528
|
});
|
|
409
529
|
});
|
|
530
|
+
// Endpoint to download the matterbridge plugin directory
|
|
410
531
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
411
532
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
412
533
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
@@ -417,9 +538,11 @@ export class Frontend {
|
|
|
417
538
|
}
|
|
418
539
|
});
|
|
419
540
|
});
|
|
541
|
+
// Endpoint to download the matterbridge plugin config files
|
|
420
542
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
421
543
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
422
544
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
545
|
+
// await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
423
546
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
424
547
|
if (error) {
|
|
425
548
|
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
@@ -427,6 +550,7 @@ export class Frontend {
|
|
|
427
550
|
}
|
|
428
551
|
});
|
|
429
552
|
});
|
|
553
|
+
// Endpoint to download the matterbridge plugin config files
|
|
430
554
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
431
555
|
this.log.debug('The frontend sent /api/download-backup');
|
|
432
556
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
@@ -436,6 +560,7 @@ export class Frontend {
|
|
|
436
560
|
}
|
|
437
561
|
});
|
|
438
562
|
});
|
|
563
|
+
// Endpoint to receive commands
|
|
439
564
|
this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
|
|
440
565
|
const command = req.params.command;
|
|
441
566
|
let param = req.params.param;
|
|
@@ -445,13 +570,15 @@ export class Frontend {
|
|
|
445
570
|
return;
|
|
446
571
|
}
|
|
447
572
|
this.log.debug(`Received frontend command: ${command}:${param}`);
|
|
573
|
+
// Handle the command setpassword from Settings
|
|
448
574
|
if (command === 'setpassword') {
|
|
449
|
-
const password = param.slice(1, -1);
|
|
575
|
+
const password = param.slice(1, -1); // Remove the first and last characters
|
|
450
576
|
this.log.debug('setpassword', param, password);
|
|
451
577
|
await this.matterbridge.nodeContext?.set('password', password);
|
|
452
578
|
res.json({ message: 'Command received' });
|
|
453
579
|
return;
|
|
454
580
|
}
|
|
581
|
+
// Handle the command setbridgemode from Settings
|
|
455
582
|
if (command === 'setbridgemode') {
|
|
456
583
|
this.log.debug(`setbridgemode: ${param}`);
|
|
457
584
|
this.wssSendRestartRequired();
|
|
@@ -459,6 +586,7 @@ export class Frontend {
|
|
|
459
586
|
res.json({ message: 'Command received' });
|
|
460
587
|
return;
|
|
461
588
|
}
|
|
589
|
+
// Handle the command backup from Settings
|
|
462
590
|
if (command === 'backup') {
|
|
463
591
|
this.log.notice(`Prepairing the backup...`);
|
|
464
592
|
await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory));
|
|
@@ -467,25 +595,26 @@ export class Frontend {
|
|
|
467
595
|
res.json({ message: 'Command received' });
|
|
468
596
|
return;
|
|
469
597
|
}
|
|
598
|
+
// Handle the command setmbloglevel from Settings
|
|
470
599
|
if (command === 'setmbloglevel') {
|
|
471
600
|
this.log.debug('Matterbridge log level:', param);
|
|
472
601
|
if (param === 'Debug') {
|
|
473
|
-
this.log.logLevel = "debug"
|
|
602
|
+
this.log.logLevel = "debug" /* LogLevel.DEBUG */;
|
|
474
603
|
}
|
|
475
604
|
else if (param === 'Info') {
|
|
476
|
-
this.log.logLevel = "info"
|
|
605
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
477
606
|
}
|
|
478
607
|
else if (param === 'Notice') {
|
|
479
|
-
this.log.logLevel = "notice"
|
|
608
|
+
this.log.logLevel = "notice" /* LogLevel.NOTICE */;
|
|
480
609
|
}
|
|
481
610
|
else if (param === 'Warn') {
|
|
482
|
-
this.log.logLevel = "warn"
|
|
611
|
+
this.log.logLevel = "warn" /* LogLevel.WARN */;
|
|
483
612
|
}
|
|
484
613
|
else if (param === 'Error') {
|
|
485
|
-
this.log.logLevel = "error"
|
|
614
|
+
this.log.logLevel = "error" /* LogLevel.ERROR */;
|
|
486
615
|
}
|
|
487
616
|
else if (param === 'Fatal') {
|
|
488
|
-
this.log.logLevel = "fatal"
|
|
617
|
+
this.log.logLevel = "fatal" /* LogLevel.FATAL */;
|
|
489
618
|
}
|
|
490
619
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
491
620
|
this.matterbridge.log.logLevel = this.log.logLevel;
|
|
@@ -495,12 +624,13 @@ export class Frontend {
|
|
|
495
624
|
for (const plugin of this.matterbridge.plugins) {
|
|
496
625
|
if (!plugin.platform || !plugin.platform.config)
|
|
497
626
|
continue;
|
|
498
|
-
plugin.platform.log.logLevel = plugin.platform.config.debug ? "debug" : this.log.logLevel;
|
|
499
|
-
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? "debug" : this.log.logLevel);
|
|
627
|
+
plugin.platform.log.logLevel = plugin.platform.config.debug ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel;
|
|
628
|
+
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel);
|
|
500
629
|
}
|
|
501
630
|
res.json({ message: 'Command received' });
|
|
502
631
|
return;
|
|
503
632
|
}
|
|
633
|
+
// Handle the command setmbloglevel from Settings
|
|
504
634
|
if (command === 'setmjloglevel') {
|
|
505
635
|
this.log.debug('Matter.js log level:', param);
|
|
506
636
|
if (param === 'Debug') {
|
|
@@ -525,30 +655,34 @@ export class Frontend {
|
|
|
525
655
|
res.json({ message: 'Command received' });
|
|
526
656
|
return;
|
|
527
657
|
}
|
|
658
|
+
// Handle the command setmdnsinterface from Settings
|
|
528
659
|
if (command === 'setmdnsinterface') {
|
|
529
|
-
param = param.slice(1, -1);
|
|
660
|
+
param = param.slice(1, -1); // Remove the first and last characters *mdns*
|
|
530
661
|
this.matterbridge.matterbridgeInformation.mattermdnsinterface = param;
|
|
531
662
|
this.log.debug('Matter.js mdns interface:', param === '' ? 'All interfaces' : param);
|
|
532
663
|
await this.matterbridge.nodeContext?.set('mattermdnsinterface', param);
|
|
533
664
|
res.json({ message: 'Command received' });
|
|
534
665
|
return;
|
|
535
666
|
}
|
|
667
|
+
// Handle the command setipv4address from Settings
|
|
536
668
|
if (command === 'setipv4address') {
|
|
537
|
-
param = param.slice(1, -1);
|
|
669
|
+
param = param.slice(1, -1); // Remove the first and last characters *ip*
|
|
538
670
|
this.matterbridge.matterbridgeInformation.matteripv4address = param;
|
|
539
671
|
this.log.debug('Matter.js ipv4 address:', param === '' ? 'All ipv4 addresses' : param);
|
|
540
672
|
await this.matterbridge.nodeContext?.set('matteripv4address', param);
|
|
541
673
|
res.json({ message: 'Command received' });
|
|
542
674
|
return;
|
|
543
675
|
}
|
|
676
|
+
// Handle the command setipv6address from Settings
|
|
544
677
|
if (command === 'setipv6address') {
|
|
545
|
-
param = param.slice(1, -1);
|
|
678
|
+
param = param.slice(1, -1); // Remove the first and last characters *ip*
|
|
546
679
|
this.matterbridge.matterbridgeInformation.matteripv6address = param;
|
|
547
680
|
this.log.debug('Matter.js ipv6 address:', param === '' ? 'All ipv6 addresses' : param);
|
|
548
681
|
await this.matterbridge.nodeContext?.set('matteripv6address', param);
|
|
549
682
|
res.json({ message: 'Command received' });
|
|
550
683
|
return;
|
|
551
684
|
}
|
|
685
|
+
// Handle the command setmatterport from Settings
|
|
552
686
|
if (command === 'setmatterport') {
|
|
553
687
|
const port = Math.min(Math.max(parseInt(param), 5540), 5560);
|
|
554
688
|
this.matterbridge.matterbridgeInformation.matterPort = port;
|
|
@@ -557,6 +691,7 @@ export class Frontend {
|
|
|
557
691
|
res.json({ message: 'Command received' });
|
|
558
692
|
return;
|
|
559
693
|
}
|
|
694
|
+
// Handle the command setmatterdiscriminator from Settings
|
|
560
695
|
if (command === 'setmatterdiscriminator') {
|
|
561
696
|
const discriminator = Math.min(Math.max(parseInt(param), 1000), 4095);
|
|
562
697
|
this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator;
|
|
@@ -565,6 +700,7 @@ export class Frontend {
|
|
|
565
700
|
res.json({ message: 'Command received' });
|
|
566
701
|
return;
|
|
567
702
|
}
|
|
703
|
+
// Handle the command setmatterpasscode from Settings
|
|
568
704
|
if (command === 'setmatterpasscode') {
|
|
569
705
|
const passcode = Math.min(Math.max(parseInt(param), 10000000), 90000000);
|
|
570
706
|
this.matterbridge.matterbridgeInformation.matterPasscode = passcode;
|
|
@@ -573,17 +709,20 @@ export class Frontend {
|
|
|
573
709
|
res.json({ message: 'Command received' });
|
|
574
710
|
return;
|
|
575
711
|
}
|
|
712
|
+
// Handle the command setmbloglevel from Settings
|
|
576
713
|
if (command === 'setmblogfile') {
|
|
577
714
|
this.log.debug('Matterbridge file log:', param);
|
|
578
715
|
this.matterbridge.matterbridgeInformation.fileLogger = param === 'true';
|
|
579
716
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true');
|
|
717
|
+
// Create the file logger for matterbridge
|
|
580
718
|
if (param === 'true')
|
|
581
|
-
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug"
|
|
719
|
+
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug" /* LogLevel.DEBUG */, true);
|
|
582
720
|
else
|
|
583
721
|
AnsiLogger.setGlobalLogfile(undefined);
|
|
584
722
|
res.json({ message: 'Command received' });
|
|
585
723
|
return;
|
|
586
724
|
}
|
|
725
|
+
// Handle the command setmbloglevel from Settings
|
|
587
726
|
if (command === 'setmjlogfile') {
|
|
588
727
|
this.log.debug('Matter file log:', param);
|
|
589
728
|
this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true';
|
|
@@ -610,40 +749,48 @@ export class Frontend {
|
|
|
610
749
|
res.json({ message: 'Command received' });
|
|
611
750
|
return;
|
|
612
751
|
}
|
|
752
|
+
// Handle the command unregister from Settings
|
|
613
753
|
if (command === 'unregister') {
|
|
614
754
|
await this.matterbridge.unregisterAndShutdownProcess();
|
|
615
755
|
res.json({ message: 'Command received' });
|
|
616
756
|
return;
|
|
617
757
|
}
|
|
758
|
+
// Handle the command reset from Settings
|
|
618
759
|
if (command === 'reset') {
|
|
619
760
|
await this.matterbridge.shutdownProcessAndReset();
|
|
620
761
|
res.json({ message: 'Command received' });
|
|
621
762
|
return;
|
|
622
763
|
}
|
|
764
|
+
// Handle the command factoryreset from Settings
|
|
623
765
|
if (command === 'factoryreset') {
|
|
624
766
|
await this.matterbridge.shutdownProcessAndFactoryReset();
|
|
625
767
|
res.json({ message: 'Command received' });
|
|
626
768
|
return;
|
|
627
769
|
}
|
|
770
|
+
// Handle the command shutdown from Header
|
|
628
771
|
if (command === 'shutdown') {
|
|
629
772
|
await this.matterbridge.shutdownProcess();
|
|
630
773
|
res.json({ message: 'Command received' });
|
|
631
774
|
return;
|
|
632
775
|
}
|
|
776
|
+
// Handle the command restart from Header
|
|
633
777
|
if (command === 'restart') {
|
|
634
778
|
await this.matterbridge.restartProcess();
|
|
635
779
|
res.json({ message: 'Command received' });
|
|
636
780
|
return;
|
|
637
781
|
}
|
|
782
|
+
// Handle the command update from Header
|
|
638
783
|
if (command === 'update') {
|
|
639
784
|
await this.matterbridge.updateProcess();
|
|
640
785
|
this.wssSendRestartRequired();
|
|
641
786
|
res.json({ message: 'Command received' });
|
|
642
787
|
return;
|
|
643
788
|
}
|
|
789
|
+
// Handle the command saveconfig from Home
|
|
644
790
|
if (command === 'saveconfig') {
|
|
645
791
|
param = param.replace(/\*/g, '\\');
|
|
646
792
|
this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
|
|
793
|
+
// console.log('Req.body:', JSON.stringify(req.body, null, 2));
|
|
647
794
|
if (!this.matterbridge.plugins.has(param)) {
|
|
648
795
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
649
796
|
}
|
|
@@ -657,49 +804,58 @@ export class Frontend {
|
|
|
657
804
|
res.json({ message: 'Command received' });
|
|
658
805
|
return;
|
|
659
806
|
}
|
|
807
|
+
// Handle the command installplugin from Home
|
|
660
808
|
if (command === 'installplugin') {
|
|
661
809
|
param = param.replace(/\*/g, '\\');
|
|
662
810
|
this.log.info(`Installing plugin ${plg}${param}${nf}...`);
|
|
663
811
|
try {
|
|
664
812
|
await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
|
|
665
813
|
this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
|
|
814
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
666
815
|
}
|
|
667
816
|
catch (error) {
|
|
668
817
|
this.log.error(`Error installing plugin ${plg}${param}${er}`);
|
|
669
818
|
}
|
|
670
819
|
this.wssSendRestartRequired();
|
|
671
820
|
param = param.split('@')[0];
|
|
821
|
+
// Also add the plugin to matterbridge so no return!
|
|
672
822
|
if (param === 'matterbridge') {
|
|
823
|
+
// If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge
|
|
673
824
|
res.json({ message: 'Command received' });
|
|
674
825
|
return;
|
|
675
826
|
}
|
|
676
827
|
}
|
|
828
|
+
// Handle the command addplugin from Home
|
|
677
829
|
if (command === 'addplugin' || command === 'installplugin') {
|
|
678
830
|
param = param.replace(/\*/g, '\\');
|
|
679
831
|
const plugin = await this.matterbridge.plugins.add(param);
|
|
680
832
|
if (plugin) {
|
|
681
833
|
if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
834
|
+
// We don't know now if the plugin is a dynamic platform or an accessory platform so we create the server node and the aggregator node
|
|
835
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
682
836
|
this.matterbridge.createDynamicPlugin(plugin, true);
|
|
683
837
|
}
|
|
684
|
-
this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true);
|
|
838
|
+
this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true); // No await do it in the background
|
|
685
839
|
}
|
|
686
840
|
res.json({ message: 'Command received' });
|
|
687
841
|
this.wssSendRefreshRequired();
|
|
688
842
|
return;
|
|
689
843
|
}
|
|
844
|
+
// Handle the command removeplugin from Home
|
|
690
845
|
if (command === 'removeplugin') {
|
|
691
846
|
if (!this.matterbridge.plugins.has(param)) {
|
|
692
847
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
693
848
|
}
|
|
694
849
|
else {
|
|
695
850
|
const plugin = this.matterbridge.plugins.get(param);
|
|
696
|
-
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
851
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true); // This will also close the server node in childbridge mode
|
|
697
852
|
await this.matterbridge.plugins.remove(param);
|
|
698
853
|
}
|
|
699
854
|
res.json({ message: 'Command received' });
|
|
700
855
|
this.wssSendRefreshRequired();
|
|
701
856
|
return;
|
|
702
857
|
}
|
|
858
|
+
// Handle the command enableplugin from Home
|
|
703
859
|
if (command === 'enableplugin') {
|
|
704
860
|
if (!this.matterbridge.plugins.has(param)) {
|
|
705
861
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
@@ -717,15 +873,17 @@ export class Frontend {
|
|
|
717
873
|
plugin.addedDevices = undefined;
|
|
718
874
|
await this.matterbridge.plugins.enable(param);
|
|
719
875
|
if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
|
|
876
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
720
877
|
this.matterbridge.createDynamicPlugin(plugin, true);
|
|
721
878
|
}
|
|
722
|
-
this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true);
|
|
879
|
+
this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true); // No await do it in the background since the server node is already started
|
|
723
880
|
}
|
|
724
881
|
}
|
|
725
882
|
res.json({ message: 'Command received' });
|
|
726
883
|
this.wssSendRefreshRequired();
|
|
727
884
|
return;
|
|
728
885
|
}
|
|
886
|
+
// Handle the command disableplugin from Home
|
|
729
887
|
if (command === 'disableplugin') {
|
|
730
888
|
if (!this.matterbridge.plugins.has(param)) {
|
|
731
889
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
@@ -733,7 +891,7 @@ export class Frontend {
|
|
|
733
891
|
else {
|
|
734
892
|
const plugin = this.matterbridge.plugins.get(param);
|
|
735
893
|
if (plugin && plugin.enabled) {
|
|
736
|
-
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
|
|
894
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true); // This will also close the server node in childbridge mode
|
|
737
895
|
await this.matterbridge.plugins.disable(param);
|
|
738
896
|
}
|
|
739
897
|
}
|
|
@@ -742,6 +900,7 @@ export class Frontend {
|
|
|
742
900
|
return;
|
|
743
901
|
}
|
|
744
902
|
});
|
|
903
|
+
// Fallback for routing (must be the last route)
|
|
745
904
|
this.expressApp.get('*', (req, res) => {
|
|
746
905
|
this.log.debug('The frontend sent:', req.url);
|
|
747
906
|
this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
@@ -750,6 +909,7 @@ export class Frontend {
|
|
|
750
909
|
this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
|
|
751
910
|
}
|
|
752
911
|
async stop() {
|
|
912
|
+
// Start the memory check. This will not allow the process to exit but will log the memory usage for 5 minutes.
|
|
753
913
|
if (hasParameter('memorycheck')) {
|
|
754
914
|
this.wssSendSnackbarMessage('Memory check started', getIntParameter('memorycheck') ?? 5 * 60 * 1000);
|
|
755
915
|
await new Promise((resolve) => {
|
|
@@ -761,24 +921,29 @@ export class Frontend {
|
|
|
761
921
|
}, getIntParameter('memorycheck') ?? 5 * 60 * 1000);
|
|
762
922
|
});
|
|
763
923
|
}
|
|
924
|
+
// Close the http server
|
|
764
925
|
if (this.httpServer) {
|
|
765
926
|
this.httpServer.close();
|
|
766
927
|
this.httpServer.removeAllListeners();
|
|
767
928
|
this.httpServer = undefined;
|
|
768
929
|
this.log.debug('Frontend http server closed successfully');
|
|
769
930
|
}
|
|
931
|
+
// Close the https server
|
|
770
932
|
if (this.httpsServer) {
|
|
771
933
|
this.httpsServer.close();
|
|
772
934
|
this.httpsServer.removeAllListeners();
|
|
773
935
|
this.httpsServer = undefined;
|
|
774
936
|
this.log.debug('Frontend https server closed successfully');
|
|
775
937
|
}
|
|
938
|
+
// Remove listeners from the express app
|
|
776
939
|
if (this.expressApp) {
|
|
777
940
|
this.expressApp.removeAllListeners();
|
|
778
941
|
this.expressApp = undefined;
|
|
779
942
|
this.log.debug('Frontend app closed successfully');
|
|
780
943
|
}
|
|
944
|
+
// Close the WebSocket server
|
|
781
945
|
if (this.webSocketServer) {
|
|
946
|
+
// Close all active connections
|
|
782
947
|
this.webSocketServer.clients.forEach((client) => {
|
|
783
948
|
if (client.readyState === WebSocket.OPEN) {
|
|
784
949
|
client.close();
|
|
@@ -794,10 +959,12 @@ export class Frontend {
|
|
|
794
959
|
});
|
|
795
960
|
this.webSocketServer = undefined;
|
|
796
961
|
}
|
|
962
|
+
// Stop the memory dump interval
|
|
797
963
|
if (hasParameter('memorydump')) {
|
|
798
964
|
this.stopCpuMemoryDump();
|
|
799
965
|
}
|
|
800
966
|
}
|
|
967
|
+
// Function to format bytes to KB, MB, or GB
|
|
801
968
|
formatMemoryUsage = (bytes) => {
|
|
802
969
|
if (bytes >= 1024 ** 3) {
|
|
803
970
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -809,6 +976,7 @@ export class Frontend {
|
|
|
809
976
|
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
810
977
|
}
|
|
811
978
|
};
|
|
979
|
+
// Function to format system uptime with only the most significant unit
|
|
812
980
|
formatOsUpTime = () => {
|
|
813
981
|
const seconds = os.uptime();
|
|
814
982
|
if (seconds >= 86400) {
|
|
@@ -828,11 +996,12 @@ export class Frontend {
|
|
|
828
996
|
getCpuUsage = () => {
|
|
829
997
|
const currCpus = os.cpus();
|
|
830
998
|
if (currCpus.length !== this.prevCpus.length) {
|
|
831
|
-
this.prevCpus = deepCopy(currCpus);
|
|
999
|
+
this.prevCpus = deepCopy(currCpus); // Reset the previous cpus
|
|
832
1000
|
this.log.debug(`***Cpu usage reset. Current cpus: ${currCpus.length}. Previous cpus: ${this.prevCpus.length}.`);
|
|
833
1001
|
return this.lastCpuUsage.toFixed(2);
|
|
834
1002
|
}
|
|
835
1003
|
let totalIdle = 0, totalTick = 0;
|
|
1004
|
+
// Get the cpu usage
|
|
836
1005
|
this.prevCpus.forEach((prevCpu, i) => {
|
|
837
1006
|
const currCpu = currCpus[i];
|
|
838
1007
|
const idleDiff = currCpu.times.idle - prevCpu.times.idle;
|
|
@@ -853,7 +1022,9 @@ export class Frontend {
|
|
|
853
1022
|
clearInterval(this.memoryInterval);
|
|
854
1023
|
clearTimeout(this.memoryTimeout);
|
|
855
1024
|
const interval = () => {
|
|
1025
|
+
// Get the cpu usage
|
|
856
1026
|
const cpuUsage = this.getCpuUsage();
|
|
1027
|
+
// Get the memory usage
|
|
857
1028
|
const memoryUsageRaw = process.memoryUsage();
|
|
858
1029
|
this.memoryData.push({ ...memoryUsageRaw, cpu: cpuUsage });
|
|
859
1030
|
const memoryUsage = {
|
|
@@ -864,6 +1035,7 @@ export class Frontend {
|
|
|
864
1035
|
arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
|
|
865
1036
|
};
|
|
866
1037
|
this.log.debug(`***Cpu usage: ${CYAN}${cpuUsage.padStart(6, ' ')} %${db} - Memory usage: rss ${CYAN}${memoryUsage.rss}${db} heapTotal ${CYAN}${memoryUsage.heapTotal}${db} heapUsed ${CYAN}${memoryUsage.heapUsed}${db} external ${memoryUsage.external} arrayBuffers ${memoryUsage.arrayBuffers}`);
|
|
1038
|
+
// Update the system information
|
|
867
1039
|
this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
|
|
868
1040
|
this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
|
|
869
1041
|
this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime();
|
|
@@ -875,7 +1047,7 @@ export class Frontend {
|
|
|
875
1047
|
this.wssSendMemoryUpdate(this.matterbridge.systemInformation.freeMemory, this.matterbridge.systemInformation.totalMemory, this.matterbridge.systemInformation.systemUptime, this.matterbridge.systemInformation.rss, this.matterbridge.systemInformation.heapUsed, this.matterbridge.systemInformation.heapTotal);
|
|
876
1048
|
};
|
|
877
1049
|
interval();
|
|
878
|
-
this.memoryInterval = setInterval(interval, getIntParameter('memoryinterval') ?? 1000);
|
|
1050
|
+
this.memoryInterval = setInterval(interval, getIntParameter('memoryinterval') ?? 1000); // 1 second
|
|
879
1051
|
this.memoryInterval.unref();
|
|
880
1052
|
this.memoryTimeout = setTimeout(() => {
|
|
881
1053
|
this.stopCpuMemoryDump();
|
|
@@ -895,12 +1067,18 @@ export class Frontend {
|
|
|
895
1067
|
external: this.formatMemoryUsage(memory.external),
|
|
896
1068
|
arrayBuffers: this.formatMemoryUsage(memory.arrayBuffers),
|
|
897
1069
|
};
|
|
1070
|
+
// eslint-disable-next-line no-console
|
|
898
1071
|
console.log(`${YELLOW}Cpu usage:${db} ${CYAN}${memory.cpu.padStart(6, ' ')} %${db} - ${YELLOW}Memory usage:${db} rss ${CYAN}${memoryUsage.rss}${db} heapTotal ${CYAN}${memoryUsage.heapTotal}${db} heapUsed ${CYAN}${memoryUsage.heapUsed}${db} external ${memoryUsage.external} arrayBuffers ${memoryUsage.arrayBuffers}${rs}`);
|
|
899
1072
|
}
|
|
900
1073
|
this.memoryData = [];
|
|
901
1074
|
this.prevCpus = [];
|
|
902
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Retrieves the api settings data.
|
|
1078
|
+
* @returns {Promise<object>} A promise that resolve in the api settings object.
|
|
1079
|
+
*/
|
|
903
1080
|
async getApiSettings() {
|
|
1081
|
+
// Update the system information
|
|
904
1082
|
this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
|
|
905
1083
|
this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
|
|
906
1084
|
this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime();
|
|
@@ -908,6 +1086,7 @@ export class Frontend {
|
|
|
908
1086
|
this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
|
|
909
1087
|
this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
|
|
910
1088
|
this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
|
|
1089
|
+
// Update the matterbridge information
|
|
911
1090
|
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
912
1091
|
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
913
1092
|
this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
|
|
@@ -926,6 +1105,11 @@ export class Frontend {
|
|
|
926
1105
|
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
927
1106
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
928
1107
|
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Retrieves the cluster text description from a given device.
|
|
1110
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
1111
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
1112
|
+
*/
|
|
929
1113
|
getClusterTextFromDevice(device) {
|
|
930
1114
|
const getAttribute = (device, cluster, attribute) => {
|
|
931
1115
|
let value = undefined;
|
|
@@ -964,6 +1148,7 @@ export class Frontend {
|
|
|
964
1148
|
};
|
|
965
1149
|
let attributes = '';
|
|
966
1150
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1151
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
967
1152
|
if (typeof attributeValue === 'undefined')
|
|
968
1153
|
return;
|
|
969
1154
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -1041,8 +1226,13 @@ export class Frontend {
|
|
|
1041
1226
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
1042
1227
|
attributes += `${getUserLabel(device)} `;
|
|
1043
1228
|
});
|
|
1229
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
1044
1230
|
return attributes.trimStart().trimEnd();
|
|
1045
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
1234
|
+
* @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
|
|
1235
|
+
*/
|
|
1046
1236
|
getBaseRegisteredPlugins() {
|
|
1047
1237
|
const baseRegisteredPlugins = [];
|
|
1048
1238
|
for (const plugin of this.matterbridge.plugins) {
|
|
@@ -1073,6 +1263,14 @@ export class Frontend {
|
|
|
1073
1263
|
}
|
|
1074
1264
|
return baseRegisteredPlugins;
|
|
1075
1265
|
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Handles incoming websocket messages for the Matterbridge.
|
|
1268
|
+
*
|
|
1269
|
+
* @param {Matterbridge} this - The Matterbridge instance.
|
|
1270
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1271
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1272
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1273
|
+
*/
|
|
1076
1274
|
async wsMessageHandler(client, message) {
|
|
1077
1275
|
let data;
|
|
1078
1276
|
try {
|
|
@@ -1160,8 +1358,10 @@ export class Frontend {
|
|
|
1160
1358
|
else if (data.method === '/api/devices') {
|
|
1161
1359
|
const devices = [];
|
|
1162
1360
|
this.matterbridge.devices.forEach(async (device) => {
|
|
1361
|
+
// Filter by pluginName if provided
|
|
1163
1362
|
if (data.params.pluginName && data.params.pluginName !== device.plugin)
|
|
1164
1363
|
return;
|
|
1364
|
+
// Check if the device has the required properties
|
|
1165
1365
|
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
|
|
1166
1366
|
return;
|
|
1167
1367
|
const cluster = this.getClusterTextFromDevice(device);
|
|
@@ -1244,6 +1444,7 @@ export class Frontend {
|
|
|
1244
1444
|
});
|
|
1245
1445
|
endpointServer.getChildEndpoints().forEach((childEndpoint) => {
|
|
1246
1446
|
deviceTypes = [];
|
|
1447
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1247
1448
|
const name = childEndpoint.endpoint?.id;
|
|
1248
1449
|
const clusterServers = childEndpoint.getAllClusterServers();
|
|
1249
1450
|
clusterServers.forEach((clusterServer) => {
|
|
@@ -1326,70 +1527,114 @@ export class Frontend {
|
|
|
1326
1527
|
return;
|
|
1327
1528
|
}
|
|
1328
1529
|
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Sends a WebSocket message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
1532
|
+
*
|
|
1533
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
1534
|
+
* @param {string} time - The time string of the message
|
|
1535
|
+
* @param {string} name - The logger name of the message
|
|
1536
|
+
* @param {string} message - The content of the message.
|
|
1537
|
+
*/
|
|
1329
1538
|
wssSendMessage(level, time, name, message) {
|
|
1330
1539
|
if (!level || !time || !name || !message)
|
|
1331
1540
|
return;
|
|
1541
|
+
// Remove ANSI escape codes from the message
|
|
1542
|
+
// eslint-disable-next-line no-control-regex
|
|
1332
1543
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
1544
|
+
// Remove leading asterisks from the message
|
|
1333
1545
|
message = message.replace(/^\*+/, '');
|
|
1546
|
+
// Replace all occurrences of \t and \n
|
|
1334
1547
|
message = message.replace(/[\t\n]/g, '');
|
|
1548
|
+
// Remove non-printable characters
|
|
1549
|
+
// eslint-disable-next-line no-control-regex
|
|
1335
1550
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1551
|
+
// Replace all occurrences of \" with "
|
|
1336
1552
|
message = message.replace(/\\"/g, '"');
|
|
1553
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1337
1554
|
const maxContinuousLength = 100;
|
|
1338
1555
|
const keepStartLength = 20;
|
|
1339
1556
|
const keepEndLength = 20;
|
|
1557
|
+
// Split the message into words
|
|
1340
1558
|
message = message
|
|
1341
1559
|
.split(' ')
|
|
1342
1560
|
.map((word) => {
|
|
1561
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1343
1562
|
if (word.length > maxContinuousLength) {
|
|
1344
1563
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1345
1564
|
}
|
|
1346
1565
|
return word;
|
|
1347
1566
|
})
|
|
1348
1567
|
.join(' ');
|
|
1568
|
+
// Send the message to all connected clients
|
|
1349
1569
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1350
1570
|
if (client.readyState === WebSocket.OPEN) {
|
|
1351
1571
|
client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
|
|
1352
1572
|
}
|
|
1353
1573
|
});
|
|
1354
1574
|
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
1577
|
+
*
|
|
1578
|
+
*/
|
|
1355
1579
|
wssSendRefreshRequired() {
|
|
1356
1580
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
1357
1581
|
this.matterbridge.matterbridgeInformation.refreshRequired = true;
|
|
1582
|
+
// Send the message to all connected clients
|
|
1358
1583
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1359
1584
|
if (client.readyState === WebSocket.OPEN) {
|
|
1360
1585
|
client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: {} }));
|
|
1361
1586
|
}
|
|
1362
1587
|
});
|
|
1363
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
1591
|
+
*
|
|
1592
|
+
*/
|
|
1364
1593
|
wssSendRestartRequired() {
|
|
1365
1594
|
this.log.debug('Sending a restart required message to all connected clients');
|
|
1366
1595
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1596
|
+
// Send the message to all connected clients
|
|
1367
1597
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1368
1598
|
if (client.readyState === WebSocket.OPEN) {
|
|
1369
1599
|
client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
|
|
1370
1600
|
}
|
|
1371
1601
|
});
|
|
1372
1602
|
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Sends a memory update message to all connected clients.
|
|
1605
|
+
*
|
|
1606
|
+
*/
|
|
1373
1607
|
wssSendCpuUpdate(cpuUsed) {
|
|
1374
1608
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
1375
1609
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1610
|
+
// Send the message to all connected clients
|
|
1376
1611
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1377
1612
|
if (client.readyState === WebSocket.OPEN) {
|
|
1378
1613
|
client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsed } }));
|
|
1379
1614
|
}
|
|
1380
1615
|
});
|
|
1381
1616
|
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Sends a cpu update message to all connected clients.
|
|
1619
|
+
*
|
|
1620
|
+
*/
|
|
1382
1621
|
wssSendMemoryUpdate(freeMemory, totalMemory, systemUptime, rss, heapUsed, heapTotal) {
|
|
1383
1622
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
1384
1623
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1624
|
+
// Send the message to all connected clients
|
|
1385
1625
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1386
1626
|
if (client.readyState === WebSocket.OPEN) {
|
|
1387
1627
|
client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { freeMemory, totalMemory, systemUptime, rss, heapUsed, heapTotal } }));
|
|
1388
1628
|
}
|
|
1389
1629
|
});
|
|
1390
1630
|
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Sends a cpu update message to all connected clients.
|
|
1633
|
+
*
|
|
1634
|
+
*/
|
|
1391
1635
|
wssSendSnackbarMessage(message, timeout = 5) {
|
|
1392
1636
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
1637
|
+
// Send the message to all connected clients
|
|
1393
1638
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1394
1639
|
if (client.readyState === WebSocket.OPEN) {
|
|
1395
1640
|
client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { message, timeout } }));
|
|
@@ -1397,3 +1642,4 @@ export class Frontend {
|
|
|
1397
1642
|
});
|
|
1398
1643
|
}
|
|
1399
1644
|
}
|
|
1645
|
+
//# sourceMappingURL=frontend.js.map
|