lightman-agent 1.0.5 → 1.0.6
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/agent.config.template.json +30 -30
- package/package.json +52 -52
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -0
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -0
- package/public/assets/index-legacy-DWtNM8y7.js +41 -0
- package/public/assets/museum-map-CwVDA2z1.svg +4182 -0
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -0
- package/public/index.html +7 -2
- package/public/templates/custom08/elements/back-button.svg +20 -0
- package/public/templates/custom08/elements/base-map-background.svg +37 -0
- package/public/templates/custom08/elements/base-map.svg +1191 -0
- package/public/templates/custom08/elements/gallery-1-2-3-info-panel.svg +236 -0
- package/public/templates/custom08/elements/gallery-4-5-6-7-info-panel.svg +266 -0
- package/public/templates/custom08/elements/gallery-8-9-info-panel.svg +274 -0
- package/public/templates/custom08/elements/gallery-labels/_nav-map-styles.css +554 -0
- package/public/templates/custom08/elements/gallery-labels/_styles.css +556 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-1.svg +35 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-2.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-3.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-4.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-5.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-6.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-7.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-8.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-9.svg +34 -0
- package/public/templates/custom08/elements/hand-hint.png +0 -0
- package/public/templates/custom08/elements/idle-screen-bg.svg +5 -0
- package/public/templates/custom08/elements/idle-screen-map.svg +627 -0
- package/public/templates/custom08/elements/idle-screen-text.svg +350 -0
- package/public/templates/custom08/elements/key-map-1.svg +986 -0
- package/public/templates/custom08/elements/key-map-2.svg +1018 -0
- package/public/templates/custom08/elements/key-map-3.svg +1019 -0
- package/public/templates/custom08/elements/key-map-combined.svg +1001 -0
- package/public/templates/custom08/elements/map-highlight-marker.svg +11 -0
- package/public/templates/custom08/elements/map-pin-marker.svg +15 -0
- package/public/templates/custom08/elements/map-teardrop-star-marker.svg +13 -0
- package/public/templates/custom08/elements/nav-circle-galleries-1-3.svg +21 -0
- package/public/templates/custom08/elements/nav-circle-galleries-4-7.svg +24 -0
- package/public/templates/custom08/elements/nav-circle-galleries-8-9.svg +20 -0
- package/public/templates/custom08/elements/section1-map.svg +1435 -0
- package/public/templates/custom08/elements/section2-map.svg +1724 -0
- package/public/templates/custom08/elements/section3-map.svg +1295 -0
- package/public/templates/custom08/fonts/CabinetGrotesk-Variable.ttf +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.12_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.56_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.24.24_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.31.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.36_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.48_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.59_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.15_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.27_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.34_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.42_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.50_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.04_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.20_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.57_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.03_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.16_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.23_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/prologue-highlight.png +0 -0
- package/scripts/guardian.ps1 +75 -75
- package/scripts/install-linux.sh +134 -134
- package/scripts/install-rpi.sh +117 -117
- package/scripts/install-windows.ps1 +505 -505
- package/scripts/launch-kiosk.vbs +101 -101
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/lightman-shell.bat +107 -107
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +116 -116
- package/scripts/setup.sh +115 -115
- package/scripts/sync-display.mjs +20 -0
- package/scripts/uninstall-linux.sh +50 -50
- package/scripts/uninstall-windows.ps1 +54 -54
- package/src/commands/display.ts +177 -177
- package/src/commands/kiosk.ts +113 -113
- package/src/commands/maintenance.ts +106 -106
- package/src/commands/network.ts +129 -129
- package/src/commands/power.ts +163 -163
- package/src/commands/rpi.ts +45 -45
- package/src/commands/screenshot.ts +166 -166
- package/src/commands/serial.ts +17 -17
- package/src/commands/update.ts +124 -124
- package/src/index.ts +652 -652
- package/src/lib/config.ts +69 -69
- package/src/lib/identity.ts +40 -40
- package/src/lib/logger.ts +137 -137
- package/src/lib/platform.ts +10 -10
- package/src/lib/rpi.ts +180 -180
- package/src/lib/screens.ts +128 -128
- package/src/lib/types.ts +176 -176
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/kiosk.ts +384 -384
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +324 -324
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -120
- package/src/services/serialBridge.ts +230 -230
- package/src/services/serviceLauncher.ts +183 -183
- package/src/services/staticServer.ts +226 -226
- package/src/services/updater.ts +249 -249
- package/src/services/watchdog.ts +310 -310
- package/src/services/websocket.ts +152 -152
- package/tsconfig.json +28 -28
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
import { createServer, request as httpRequest, type IncomingMessage, type ServerResponse } from 'http';
|
|
2
|
-
import { createReadStream, statSync, existsSync } from 'fs';
|
|
3
|
-
import { resolve, extname, join } from 'path';
|
|
4
|
-
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
|
-
import type { Logger } from '../lib/logger.js';
|
|
6
|
-
|
|
7
|
-
const MIME: Record<string, string> = {
|
|
8
|
-
'.html': 'text/html; charset=utf-8',
|
|
9
|
-
'.js': 'application/javascript',
|
|
10
|
-
'.css': 'text/css',
|
|
11
|
-
'.json': 'application/json',
|
|
12
|
-
'.png': 'image/png',
|
|
13
|
-
'.jpg': 'image/jpeg',
|
|
14
|
-
'.svg': 'image/svg+xml',
|
|
15
|
-
'.ico': 'image/x-icon',
|
|
16
|
-
'.woff': 'font/woff',
|
|
17
|
-
'.woff2': 'font/woff2',
|
|
18
|
-
'.ttf': 'font/ttf',
|
|
19
|
-
'.mp4': 'video/mp4',
|
|
20
|
-
'.webm': 'video/webm',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export class StaticServer {
|
|
24
|
-
private port: number;
|
|
25
|
-
private distPath: string;
|
|
26
|
-
private serverUrl: string;
|
|
27
|
-
private logger: Logger;
|
|
28
|
-
private server: ReturnType<typeof createServer> | null = null;
|
|
29
|
-
|
|
30
|
-
constructor(port: number, distPath: string, serverUrl: string, logger: Logger) {
|
|
31
|
-
this.port = port;
|
|
32
|
-
this.distPath = distPath;
|
|
33
|
-
this.serverUrl = serverUrl;
|
|
34
|
-
this.logger = logger;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
start(): void {
|
|
38
|
-
if (!existsSync(this.distPath)) {
|
|
39
|
-
this.logger.warn(`StaticServer: dist path not found: ${this.distPath}`);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this.server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
44
|
-
this.handle(req, res);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Proxy WebSocket upgrades (/ws/*) to the real server
|
|
48
|
-
this.server.on('upgrade', (req, socket, head) => {
|
|
49
|
-
const upstream = new URL(req.url || '/', this.serverUrl);
|
|
50
|
-
const wsUrl = upstream.toString().replace(/^http/, 'ws');
|
|
51
|
-
this.logger.debug(`WS proxy: ${req.url} → ${wsUrl}`);
|
|
52
|
-
|
|
53
|
-
const upstreamWs = new WebSocket(wsUrl, {
|
|
54
|
-
headers: { ...req.headers, host: new URL(this.serverUrl).host },
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
upstreamWs.on('open', () => {
|
|
58
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
59
|
-
wss.handleUpgrade(req, socket, head, (clientWs) => {
|
|
60
|
-
// Pipe client ↔ upstream
|
|
61
|
-
clientWs.on('message', (data, isBinary) => {
|
|
62
|
-
if (upstreamWs.readyState === WebSocket.OPEN) {
|
|
63
|
-
upstreamWs.send(data, { binary: isBinary });
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
upstreamWs.on('message', (data, isBinary) => {
|
|
67
|
-
if (clientWs.readyState === WebSocket.OPEN) {
|
|
68
|
-
clientWs.send(data, { binary: isBinary });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
clientWs.on('close', () => upstreamWs.close());
|
|
72
|
-
upstreamWs.on('close', () => clientWs.close());
|
|
73
|
-
clientWs.on('error', () => upstreamWs.close());
|
|
74
|
-
upstreamWs.on('error', () => clientWs.close());
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
upstreamWs.on('error', (err) => {
|
|
79
|
-
this.logger.warn(`WS proxy error: ${err.message}`);
|
|
80
|
-
socket.destroy();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
this.server.listen(this.port, '0.0.0.0', () => {
|
|
85
|
-
this.logger.info(`Display static server listening on port ${this.port} (proxy → ${this.serverUrl})`);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
this.server.on('error', (err) => {
|
|
89
|
-
this.logger.error(`StaticServer error: ${err.message}`);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
stop(): void {
|
|
94
|
-
if (this.server) {
|
|
95
|
-
this.server.close();
|
|
96
|
-
this.server = null;
|
|
97
|
-
this.logger.info('Display static server stopped');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private handle(req: IncomingMessage, res: ServerResponse): void {
|
|
102
|
-
const rawUrl = req.url || '/';
|
|
103
|
-
const path = rawUrl.split('?')[0];
|
|
104
|
-
|
|
105
|
-
// Proxy /api/* and /storage/* to the real server
|
|
106
|
-
if (path.startsWith('/api/') || path.startsWith('/storage/')) {
|
|
107
|
-
this.proxyHttp(req, res);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Proxy /display/* to the server first (always get latest build).
|
|
112
|
-
// Only fall back to local files if server is unreachable.
|
|
113
|
-
if (path.startsWith('/display')) {
|
|
114
|
-
this.proxyWithFallback(req, res);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Serve static files for non-display paths
|
|
119
|
-
this.serveStatic(req, res);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private proxyHttp(req: IncomingMessage, res: ServerResponse): void {
|
|
123
|
-
const target = new URL(this.serverUrl);
|
|
124
|
-
const options = {
|
|
125
|
-
hostname: target.hostname,
|
|
126
|
-
port: target.port || 80,
|
|
127
|
-
path: req.url,
|
|
128
|
-
method: req.method,
|
|
129
|
-
headers: { ...req.headers, host: target.host },
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const proxy = httpRequest(options, (upstreamRes) => {
|
|
133
|
-
res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
|
|
134
|
-
upstreamRes.pipe(res);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
proxy.on('error', (err) => {
|
|
138
|
-
this.logger.warn(`HTTP proxy error: ${err.message}`);
|
|
139
|
-
if (!res.headersSent) {
|
|
140
|
-
res.writeHead(502);
|
|
141
|
-
res.end('Bad Gateway');
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
req.pipe(proxy);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Try to proxy the request to the real server (latest display build).
|
|
150
|
-
* If the server is unreachable, fall back to local static files.
|
|
151
|
-
*/
|
|
152
|
-
private proxyWithFallback(req: IncomingMessage, res: ServerResponse): void {
|
|
153
|
-
const target = new URL(this.serverUrl);
|
|
154
|
-
let fell = false;
|
|
155
|
-
const fallback = () => {
|
|
156
|
-
if (fell || res.headersSent) return;
|
|
157
|
-
fell = true;
|
|
158
|
-
this.logger.debug(`Proxy failed for ${req.url}, serving from local files`);
|
|
159
|
-
this.serveStatic(req, res);
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const options = {
|
|
163
|
-
hostname: target.hostname,
|
|
164
|
-
port: target.port || 80,
|
|
165
|
-
path: req.url,
|
|
166
|
-
method: req.method,
|
|
167
|
-
headers: { ...req.headers, host: target.host },
|
|
168
|
-
timeout: 3000,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const proxy = httpRequest(options, (upstreamRes) => {
|
|
172
|
-
const statusCode = upstreamRes.statusCode || 502;
|
|
173
|
-
|
|
174
|
-
// If upstream display route is unavailable/misconfigured,
|
|
175
|
-
// serve the local bundled display instead of surfacing server errors.
|
|
176
|
-
if (statusCode >= 400) {
|
|
177
|
-
upstreamRes.resume();
|
|
178
|
-
fallback();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
res.writeHead(statusCode, upstreamRes.headers);
|
|
183
|
-
upstreamRes.pipe(res);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
proxy.on('error', fallback);
|
|
187
|
-
proxy.on('timeout', () => { proxy.destroy(); fallback(); });
|
|
188
|
-
|
|
189
|
-
req.pipe(proxy);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private serveStatic(req: IncomingMessage, res: ServerResponse): void {
|
|
193
|
-
const rawUrl = (req.url || '/').split('?')[0];
|
|
194
|
-
|
|
195
|
-
// Strip /display prefix to map to dist files
|
|
196
|
-
let filePath = rawUrl.startsWith('/display')
|
|
197
|
-
? rawUrl.slice('/display'.length) || '/'
|
|
198
|
-
: rawUrl;
|
|
199
|
-
|
|
200
|
-
let absPath = resolve(this.distPath, filePath.replace(/^\//, ''));
|
|
201
|
-
|
|
202
|
-
// Security: stay inside distPath
|
|
203
|
-
if (!absPath.startsWith(this.distPath)) {
|
|
204
|
-
res.writeHead(403);
|
|
205
|
-
res.end('Forbidden');
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// SPA fallback
|
|
210
|
-
if (!existsSync(absPath) || statSync(absPath).isDirectory()) {
|
|
211
|
-
absPath = join(this.distPath, 'index.html');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (!existsSync(absPath)) {
|
|
215
|
-
res.writeHead(404);
|
|
216
|
-
res.end('Not Found');
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const ext = extname(absPath).toLowerCase();
|
|
221
|
-
const mime = MIME[ext] || 'application/octet-stream';
|
|
222
|
-
|
|
223
|
-
res.writeHead(200, { 'Content-Type': mime });
|
|
224
|
-
createReadStream(absPath).pipe(res);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
1
|
+
import { createServer, request as httpRequest, type IncomingMessage, type ServerResponse } from 'http';
|
|
2
|
+
import { createReadStream, statSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve, extname, join } from 'path';
|
|
4
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
|
+
import type { Logger } from '../lib/logger.js';
|
|
6
|
+
|
|
7
|
+
const MIME: Record<string, string> = {
|
|
8
|
+
'.html': 'text/html; charset=utf-8',
|
|
9
|
+
'.js': 'application/javascript',
|
|
10
|
+
'.css': 'text/css',
|
|
11
|
+
'.json': 'application/json',
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.ico': 'image/x-icon',
|
|
16
|
+
'.woff': 'font/woff',
|
|
17
|
+
'.woff2': 'font/woff2',
|
|
18
|
+
'.ttf': 'font/ttf',
|
|
19
|
+
'.mp4': 'video/mp4',
|
|
20
|
+
'.webm': 'video/webm',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class StaticServer {
|
|
24
|
+
private port: number;
|
|
25
|
+
private distPath: string;
|
|
26
|
+
private serverUrl: string;
|
|
27
|
+
private logger: Logger;
|
|
28
|
+
private server: ReturnType<typeof createServer> | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(port: number, distPath: string, serverUrl: string, logger: Logger) {
|
|
31
|
+
this.port = port;
|
|
32
|
+
this.distPath = distPath;
|
|
33
|
+
this.serverUrl = serverUrl;
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
start(): void {
|
|
38
|
+
if (!existsSync(this.distPath)) {
|
|
39
|
+
this.logger.warn(`StaticServer: dist path not found: ${this.distPath}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
44
|
+
this.handle(req, res);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Proxy WebSocket upgrades (/ws/*) to the real server
|
|
48
|
+
this.server.on('upgrade', (req, socket, head) => {
|
|
49
|
+
const upstream = new URL(req.url || '/', this.serverUrl);
|
|
50
|
+
const wsUrl = upstream.toString().replace(/^http/, 'ws');
|
|
51
|
+
this.logger.debug(`WS proxy: ${req.url} → ${wsUrl}`);
|
|
52
|
+
|
|
53
|
+
const upstreamWs = new WebSocket(wsUrl, {
|
|
54
|
+
headers: { ...req.headers, host: new URL(this.serverUrl).host },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
upstreamWs.on('open', () => {
|
|
58
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
59
|
+
wss.handleUpgrade(req, socket, head, (clientWs) => {
|
|
60
|
+
// Pipe client ↔ upstream
|
|
61
|
+
clientWs.on('message', (data, isBinary) => {
|
|
62
|
+
if (upstreamWs.readyState === WebSocket.OPEN) {
|
|
63
|
+
upstreamWs.send(data, { binary: isBinary });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
upstreamWs.on('message', (data, isBinary) => {
|
|
67
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
68
|
+
clientWs.send(data, { binary: isBinary });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
clientWs.on('close', () => upstreamWs.close());
|
|
72
|
+
upstreamWs.on('close', () => clientWs.close());
|
|
73
|
+
clientWs.on('error', () => upstreamWs.close());
|
|
74
|
+
upstreamWs.on('error', () => clientWs.close());
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
upstreamWs.on('error', (err) => {
|
|
79
|
+
this.logger.warn(`WS proxy error: ${err.message}`);
|
|
80
|
+
socket.destroy();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.server.listen(this.port, '0.0.0.0', () => {
|
|
85
|
+
this.logger.info(`Display static server listening on port ${this.port} (proxy → ${this.serverUrl})`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.server.on('error', (err) => {
|
|
89
|
+
this.logger.error(`StaticServer error: ${err.message}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
stop(): void {
|
|
94
|
+
if (this.server) {
|
|
95
|
+
this.server.close();
|
|
96
|
+
this.server = null;
|
|
97
|
+
this.logger.info('Display static server stopped');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private handle(req: IncomingMessage, res: ServerResponse): void {
|
|
102
|
+
const rawUrl = req.url || '/';
|
|
103
|
+
const path = rawUrl.split('?')[0];
|
|
104
|
+
|
|
105
|
+
// Proxy /api/* and /storage/* to the real server
|
|
106
|
+
if (path.startsWith('/api/') || path.startsWith('/storage/')) {
|
|
107
|
+
this.proxyHttp(req, res);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Proxy /display/* to the server first (always get latest build).
|
|
112
|
+
// Only fall back to local files if server is unreachable.
|
|
113
|
+
if (path.startsWith('/display')) {
|
|
114
|
+
this.proxyWithFallback(req, res);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Serve static files for non-display paths
|
|
119
|
+
this.serveStatic(req, res);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private proxyHttp(req: IncomingMessage, res: ServerResponse): void {
|
|
123
|
+
const target = new URL(this.serverUrl);
|
|
124
|
+
const options = {
|
|
125
|
+
hostname: target.hostname,
|
|
126
|
+
port: target.port || 80,
|
|
127
|
+
path: req.url,
|
|
128
|
+
method: req.method,
|
|
129
|
+
headers: { ...req.headers, host: target.host },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const proxy = httpRequest(options, (upstreamRes) => {
|
|
133
|
+
res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
|
|
134
|
+
upstreamRes.pipe(res);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
proxy.on('error', (err) => {
|
|
138
|
+
this.logger.warn(`HTTP proxy error: ${err.message}`);
|
|
139
|
+
if (!res.headersSent) {
|
|
140
|
+
res.writeHead(502);
|
|
141
|
+
res.end('Bad Gateway');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
req.pipe(proxy);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Try to proxy the request to the real server (latest display build).
|
|
150
|
+
* If the server is unreachable, fall back to local static files.
|
|
151
|
+
*/
|
|
152
|
+
private proxyWithFallback(req: IncomingMessage, res: ServerResponse): void {
|
|
153
|
+
const target = new URL(this.serverUrl);
|
|
154
|
+
let fell = false;
|
|
155
|
+
const fallback = () => {
|
|
156
|
+
if (fell || res.headersSent) return;
|
|
157
|
+
fell = true;
|
|
158
|
+
this.logger.debug(`Proxy failed for ${req.url}, serving from local files`);
|
|
159
|
+
this.serveStatic(req, res);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const options = {
|
|
163
|
+
hostname: target.hostname,
|
|
164
|
+
port: target.port || 80,
|
|
165
|
+
path: req.url,
|
|
166
|
+
method: req.method,
|
|
167
|
+
headers: { ...req.headers, host: target.host },
|
|
168
|
+
timeout: 3000,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const proxy = httpRequest(options, (upstreamRes) => {
|
|
172
|
+
const statusCode = upstreamRes.statusCode || 502;
|
|
173
|
+
|
|
174
|
+
// If upstream display route is unavailable/misconfigured,
|
|
175
|
+
// serve the local bundled display instead of surfacing server errors.
|
|
176
|
+
if (statusCode >= 400) {
|
|
177
|
+
upstreamRes.resume();
|
|
178
|
+
fallback();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
res.writeHead(statusCode, upstreamRes.headers);
|
|
183
|
+
upstreamRes.pipe(res);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
proxy.on('error', fallback);
|
|
187
|
+
proxy.on('timeout', () => { proxy.destroy(); fallback(); });
|
|
188
|
+
|
|
189
|
+
req.pipe(proxy);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private serveStatic(req: IncomingMessage, res: ServerResponse): void {
|
|
193
|
+
const rawUrl = (req.url || '/').split('?')[0];
|
|
194
|
+
|
|
195
|
+
// Strip /display prefix to map to dist files
|
|
196
|
+
let filePath = rawUrl.startsWith('/display')
|
|
197
|
+
? rawUrl.slice('/display'.length) || '/'
|
|
198
|
+
: rawUrl;
|
|
199
|
+
|
|
200
|
+
let absPath = resolve(this.distPath, filePath.replace(/^\//, ''));
|
|
201
|
+
|
|
202
|
+
// Security: stay inside distPath
|
|
203
|
+
if (!absPath.startsWith(this.distPath)) {
|
|
204
|
+
res.writeHead(403);
|
|
205
|
+
res.end('Forbidden');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// SPA fallback
|
|
210
|
+
if (!existsSync(absPath) || statSync(absPath).isDirectory()) {
|
|
211
|
+
absPath = join(this.distPath, 'index.html');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!existsSync(absPath)) {
|
|
215
|
+
res.writeHead(404);
|
|
216
|
+
res.end('Not Found');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const ext = extname(absPath).toLowerCase();
|
|
221
|
+
const mime = MIME[ext] || 'application/octet-stream';
|
|
222
|
+
|
|
223
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
224
|
+
createReadStream(absPath).pipe(res);
|
|
225
|
+
}
|
|
226
|
+
}
|