@blueharford/scrypted-spatial-awareness 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -3
- package/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +1869 -19
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/tracking-engine.ts +588 -1
- package/src/main.ts +318 -19
- package/src/models/training.ts +300 -0
- package/src/ui/training-html.ts +1007 -0
package/src/main.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { GlobalTrackerSensor } from './devices/global-tracker-sensor';
|
|
|
32
32
|
import { TrackingZone } from './devices/tracking-zone';
|
|
33
33
|
import { MqttPublisher, MqttConfig } from './integrations/mqtt-publisher';
|
|
34
34
|
import { EDITOR_HTML } from './ui/editor-html';
|
|
35
|
+
import { TRAINING_HTML } from './ui/training-html';
|
|
36
|
+
import { TrainingConfig, TrainingLandmark } from './models/training';
|
|
35
37
|
|
|
36
38
|
const { deviceManager, systemManager } = sdk;
|
|
37
39
|
|
|
@@ -432,8 +434,91 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
432
434
|
async getSettings(): Promise<Setting[]> {
|
|
433
435
|
const settings = await this.storageSettings.getSettings();
|
|
434
436
|
|
|
437
|
+
// Training Mode button that opens mobile-friendly training UI in modal
|
|
438
|
+
const trainingOnclickCode = `(function(){var e=document.getElementById('sa-training-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-training-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:min(420px,95vw);height:92vh;max-height:900px;background:#121212;border-radius:8px;overflow:hidden;position:relative;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:8px;right:8px;z-index:2147483647;background:rgba(255,255,255,0.1);color:white;border:none;width:32px;height:32px;border-radius:4px;font-size:18px;cursor:pointer;line-height:1;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/training';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
|
|
439
|
+
|
|
440
|
+
settings.push({
|
|
441
|
+
key: 'trainingMode',
|
|
442
|
+
title: 'Training Mode',
|
|
443
|
+
type: 'html' as any,
|
|
444
|
+
value: `
|
|
445
|
+
<style>
|
|
446
|
+
.sa-training-container {
|
|
447
|
+
padding: 16px;
|
|
448
|
+
background: rgba(255,255,255,0.03);
|
|
449
|
+
border-radius: 4px;
|
|
450
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
451
|
+
}
|
|
452
|
+
.sa-training-title {
|
|
453
|
+
color: #4fc3f7;
|
|
454
|
+
font-size: 14px;
|
|
455
|
+
font-weight: 500;
|
|
456
|
+
margin-bottom: 8px;
|
|
457
|
+
font-family: inherit;
|
|
458
|
+
}
|
|
459
|
+
.sa-training-desc {
|
|
460
|
+
color: rgba(255,255,255,0.6);
|
|
461
|
+
margin-bottom: 12px;
|
|
462
|
+
font-size: 13px;
|
|
463
|
+
line-height: 1.5;
|
|
464
|
+
font-family: inherit;
|
|
465
|
+
}
|
|
466
|
+
.sa-training-btn {
|
|
467
|
+
background: #4fc3f7;
|
|
468
|
+
color: #000;
|
|
469
|
+
border: none;
|
|
470
|
+
padding: 10px 20px;
|
|
471
|
+
border-radius: 4px;
|
|
472
|
+
font-size: 14px;
|
|
473
|
+
font-weight: 500;
|
|
474
|
+
cursor: pointer;
|
|
475
|
+
display: inline-flex;
|
|
476
|
+
align-items: center;
|
|
477
|
+
gap: 8px;
|
|
478
|
+
transition: background 0.2s;
|
|
479
|
+
font-family: inherit;
|
|
480
|
+
}
|
|
481
|
+
.sa-training-btn:hover {
|
|
482
|
+
background: #81d4fa;
|
|
483
|
+
}
|
|
484
|
+
.sa-training-steps {
|
|
485
|
+
color: rgba(255,255,255,0.5);
|
|
486
|
+
font-size: 12px;
|
|
487
|
+
margin-top: 12px;
|
|
488
|
+
padding-top: 12px;
|
|
489
|
+
border-top: 1px solid rgba(255,255,255,0.05);
|
|
490
|
+
font-family: inherit;
|
|
491
|
+
}
|
|
492
|
+
.sa-training-steps ol {
|
|
493
|
+
margin: 6px 0 0 16px;
|
|
494
|
+
padding: 0;
|
|
495
|
+
}
|
|
496
|
+
.sa-training-steps li {
|
|
497
|
+
margin-bottom: 2px;
|
|
498
|
+
}
|
|
499
|
+
</style>
|
|
500
|
+
<div class="sa-training-container">
|
|
501
|
+
<div class="sa-training-title">Guided Property Training</div>
|
|
502
|
+
<p class="sa-training-desc">Walk your property while the system learns your camera layout, transit times, and landmarks automatically.</p>
|
|
503
|
+
<button class="sa-training-btn" onclick="${trainingOnclickCode}">
|
|
504
|
+
Start Training Mode
|
|
505
|
+
</button>
|
|
506
|
+
<div class="sa-training-steps">
|
|
507
|
+
<strong>How it works:</strong>
|
|
508
|
+
<ol>
|
|
509
|
+
<li>Start training and walk to each camera</li>
|
|
510
|
+
<li>System auto-detects you and records transit times</li>
|
|
511
|
+
<li>Mark landmarks as you encounter them</li>
|
|
512
|
+
<li>Apply results to generate your topology</li>
|
|
513
|
+
</ol>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
`,
|
|
517
|
+
group: 'Getting Started',
|
|
518
|
+
});
|
|
519
|
+
|
|
435
520
|
// Topology editor button that opens modal overlay (appended to body for proper z-index)
|
|
436
|
-
const onclickCode = `(function(){var e=document.getElementById('sa-topology-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-topology-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.
|
|
521
|
+
const onclickCode = `(function(){var e=document.getElementById('sa-topology-modal');if(e)e.remove();var m=document.createElement('div');m.id='sa-topology-modal';m.style.cssText='position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';var c=document.createElement('div');c.style.cssText='width:95vw;height:92vh;max-width:1800px;background:#121212;border-radius:8px;overflow:hidden;position:relative;box-shadow:0 8px 32px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:8px;right:8px;z-index:2147483647;background:rgba(255,255,255,0.1);color:white;border:none;width:32px;height:32px;border-radius:4px;font-size:18px;cursor:pointer;line-height:1;';b.onclick=function(){m.remove();};var f=document.createElement('iframe');f.src='/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor';f.style.cssText='width:100%;height:100%;border:none;';c.appendChild(b);c.appendChild(f);m.appendChild(c);m.onclick=function(ev){if(ev.target===m)m.remove();};document.body.appendChild(m);})()`;
|
|
437
522
|
|
|
438
523
|
settings.push({
|
|
439
524
|
key: 'topologyEditor',
|
|
@@ -442,39 +527,41 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
442
527
|
value: `
|
|
443
528
|
<style>
|
|
444
529
|
.sa-open-btn {
|
|
445
|
-
background:
|
|
446
|
-
color:
|
|
530
|
+
background: #4fc3f7;
|
|
531
|
+
color: #000;
|
|
447
532
|
border: none;
|
|
448
|
-
padding:
|
|
449
|
-
border-radius:
|
|
450
|
-
font-size:
|
|
451
|
-
font-weight:
|
|
533
|
+
padding: 10px 20px;
|
|
534
|
+
border-radius: 4px;
|
|
535
|
+
font-size: 14px;
|
|
536
|
+
font-weight: 500;
|
|
452
537
|
cursor: pointer;
|
|
453
538
|
display: inline-flex;
|
|
454
539
|
align-items: center;
|
|
455
|
-
gap:
|
|
456
|
-
transition:
|
|
540
|
+
gap: 8px;
|
|
541
|
+
transition: background 0.2s;
|
|
542
|
+
font-family: inherit;
|
|
457
543
|
}
|
|
458
544
|
.sa-open-btn:hover {
|
|
459
|
-
|
|
460
|
-
box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
|
|
545
|
+
background: #81d4fa;
|
|
461
546
|
}
|
|
462
547
|
.sa-btn-container {
|
|
463
|
-
padding:
|
|
464
|
-
background:
|
|
465
|
-
border-radius:
|
|
548
|
+
padding: 16px;
|
|
549
|
+
background: rgba(255,255,255,0.03);
|
|
550
|
+
border-radius: 4px;
|
|
466
551
|
text-align: center;
|
|
552
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
467
553
|
}
|
|
468
554
|
.sa-btn-desc {
|
|
469
|
-
color:
|
|
470
|
-
margin-bottom:
|
|
471
|
-
font-size:
|
|
555
|
+
color: rgba(255,255,255,0.6);
|
|
556
|
+
margin-bottom: 12px;
|
|
557
|
+
font-size: 13px;
|
|
558
|
+
font-family: inherit;
|
|
472
559
|
}
|
|
473
560
|
</style>
|
|
474
561
|
<div class="sa-btn-container">
|
|
475
562
|
<p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
|
|
476
563
|
<button class="sa-open-btn" onclick="${onclickCode}">
|
|
477
|
-
|
|
564
|
+
Open Topology Editor
|
|
478
565
|
</button>
|
|
479
566
|
</div>
|
|
480
567
|
`,
|
|
@@ -736,11 +823,38 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
736
823
|
return this.handleJourneyPathRequest(globalId, response);
|
|
737
824
|
}
|
|
738
825
|
|
|
826
|
+
// Training Mode endpoints
|
|
827
|
+
if (path.endsWith('/api/training/start')) {
|
|
828
|
+
return this.handleTrainingStartRequest(request, response);
|
|
829
|
+
}
|
|
830
|
+
if (path.endsWith('/api/training/pause')) {
|
|
831
|
+
return this.handleTrainingPauseRequest(response);
|
|
832
|
+
}
|
|
833
|
+
if (path.endsWith('/api/training/resume')) {
|
|
834
|
+
return this.handleTrainingResumeRequest(response);
|
|
835
|
+
}
|
|
836
|
+
if (path.endsWith('/api/training/end')) {
|
|
837
|
+
return this.handleTrainingEndRequest(response);
|
|
838
|
+
}
|
|
839
|
+
if (path.endsWith('/api/training/status')) {
|
|
840
|
+
return this.handleTrainingStatusRequest(response);
|
|
841
|
+
}
|
|
842
|
+
if (path.endsWith('/api/training/landmark')) {
|
|
843
|
+
return this.handleTrainingLandmarkRequest(request, response);
|
|
844
|
+
}
|
|
845
|
+
if (path.endsWith('/api/training/apply')) {
|
|
846
|
+
return this.handleTrainingApplyRequest(response);
|
|
847
|
+
}
|
|
848
|
+
|
|
739
849
|
// UI Routes
|
|
740
850
|
if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
|
|
741
851
|
return this.serveEditorUI(response);
|
|
742
852
|
}
|
|
743
853
|
|
|
854
|
+
if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
|
|
855
|
+
return this.serveTrainingUI(response);
|
|
856
|
+
}
|
|
857
|
+
|
|
744
858
|
if (path.includes('/ui/')) {
|
|
745
859
|
return this.serveStaticFile(path, response);
|
|
746
860
|
}
|
|
@@ -748,7 +862,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
748
862
|
// Default: return info page
|
|
749
863
|
response.send(JSON.stringify({
|
|
750
864
|
name: 'Spatial Awareness Plugin',
|
|
751
|
-
version: '0.
|
|
865
|
+
version: '0.4.0',
|
|
752
866
|
endpoints: {
|
|
753
867
|
api: {
|
|
754
868
|
trackedObjects: '/api/tracked-objects',
|
|
@@ -760,9 +874,19 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
760
874
|
liveTracking: '/api/live-tracking',
|
|
761
875
|
connectionSuggestions: '/api/connection-suggestions',
|
|
762
876
|
landmarkSuggestions: '/api/landmark-suggestions',
|
|
877
|
+
training: {
|
|
878
|
+
start: '/api/training/start',
|
|
879
|
+
pause: '/api/training/pause',
|
|
880
|
+
resume: '/api/training/resume',
|
|
881
|
+
end: '/api/training/end',
|
|
882
|
+
status: '/api/training/status',
|
|
883
|
+
landmark: '/api/training/landmark',
|
|
884
|
+
apply: '/api/training/apply',
|
|
885
|
+
},
|
|
763
886
|
},
|
|
764
887
|
ui: {
|
|
765
888
|
editor: '/ui/editor',
|
|
889
|
+
training: '/ui/training',
|
|
766
890
|
},
|
|
767
891
|
},
|
|
768
892
|
}), {
|
|
@@ -1228,12 +1352,187 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1228
1352
|
}
|
|
1229
1353
|
}
|
|
1230
1354
|
|
|
1355
|
+
// ==================== Training Mode Handlers ====================
|
|
1356
|
+
|
|
1357
|
+
private handleTrainingStartRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1358
|
+
if (!this.trackingEngine) {
|
|
1359
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running. Configure topology first.' }), {
|
|
1360
|
+
code: 500,
|
|
1361
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1362
|
+
});
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
try {
|
|
1367
|
+
let config: Partial<TrainingConfig> | undefined;
|
|
1368
|
+
let trainerName: string | undefined;
|
|
1369
|
+
|
|
1370
|
+
if (request.body) {
|
|
1371
|
+
const body = JSON.parse(request.body);
|
|
1372
|
+
trainerName = body.trainerName;
|
|
1373
|
+
config = body.config;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const session = this.trackingEngine.startTrainingSession(trainerName, config);
|
|
1377
|
+
response.send(JSON.stringify(session), {
|
|
1378
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1379
|
+
});
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
response.send(JSON.stringify({ error: (e as Error).message }), {
|
|
1382
|
+
code: 500,
|
|
1383
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private handleTrainingPauseRequest(response: HttpResponse): void {
|
|
1389
|
+
if (!this.trackingEngine) {
|
|
1390
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1391
|
+
code: 500,
|
|
1392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1393
|
+
});
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const success = this.trackingEngine.pauseTrainingSession();
|
|
1398
|
+
if (success) {
|
|
1399
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1401
|
+
});
|
|
1402
|
+
} else {
|
|
1403
|
+
response.send(JSON.stringify({ error: 'No active training session to pause' }), {
|
|
1404
|
+
code: 400,
|
|
1405
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private handleTrainingResumeRequest(response: HttpResponse): void {
|
|
1411
|
+
if (!this.trackingEngine) {
|
|
1412
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1413
|
+
code: 500,
|
|
1414
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1415
|
+
});
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const success = this.trackingEngine.resumeTrainingSession();
|
|
1420
|
+
if (success) {
|
|
1421
|
+
response.send(JSON.stringify({ success: true }), {
|
|
1422
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1423
|
+
});
|
|
1424
|
+
} else {
|
|
1425
|
+
response.send(JSON.stringify({ error: 'No paused training session to resume' }), {
|
|
1426
|
+
code: 400,
|
|
1427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
private handleTrainingEndRequest(response: HttpResponse): void {
|
|
1433
|
+
if (!this.trackingEngine) {
|
|
1434
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1435
|
+
code: 500,
|
|
1436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1437
|
+
});
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const session = this.trackingEngine.endTrainingSession();
|
|
1442
|
+
if (session) {
|
|
1443
|
+
response.send(JSON.stringify(session), {
|
|
1444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
+
});
|
|
1446
|
+
} else {
|
|
1447
|
+
response.send(JSON.stringify({ error: 'No training session to end' }), {
|
|
1448
|
+
code: 400,
|
|
1449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private handleTrainingStatusRequest(response: HttpResponse): void {
|
|
1455
|
+
if (!this.trackingEngine) {
|
|
1456
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
1457
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1458
|
+
});
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const status = this.trackingEngine.getTrainingStatus();
|
|
1463
|
+
if (status) {
|
|
1464
|
+
response.send(JSON.stringify(status), {
|
|
1465
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
response.send(JSON.stringify({ state: 'idle', stats: null }), {
|
|
1469
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private handleTrainingLandmarkRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1475
|
+
if (!this.trackingEngine) {
|
|
1476
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1477
|
+
code: 500,
|
|
1478
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1479
|
+
});
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
try {
|
|
1484
|
+
const body = JSON.parse(request.body!) as Omit<TrainingLandmark, 'id' | 'markedAt'>;
|
|
1485
|
+
const landmark = this.trackingEngine.markTrainingLandmark(body);
|
|
1486
|
+
if (landmark) {
|
|
1487
|
+
response.send(JSON.stringify({ success: true, landmark }), {
|
|
1488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1489
|
+
});
|
|
1490
|
+
} else {
|
|
1491
|
+
response.send(JSON.stringify({ error: 'No active training session' }), {
|
|
1492
|
+
code: 400,
|
|
1493
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
response.send(JSON.stringify({ error: 'Invalid request body' }), {
|
|
1498
|
+
code: 400,
|
|
1499
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
private handleTrainingApplyRequest(response: HttpResponse): void {
|
|
1505
|
+
if (!this.trackingEngine) {
|
|
1506
|
+
response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
|
|
1507
|
+
code: 500,
|
|
1508
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1509
|
+
});
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const result = this.trackingEngine.applyTrainingToTopology();
|
|
1514
|
+
if (result.success) {
|
|
1515
|
+
// Save the updated topology
|
|
1516
|
+
const topology = this.trackingEngine.getTopology();
|
|
1517
|
+
this.storage.setItem('topology', JSON.stringify(topology));
|
|
1518
|
+
}
|
|
1519
|
+
response.send(JSON.stringify(result), {
|
|
1520
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1231
1524
|
private serveEditorUI(response: HttpResponse): void {
|
|
1232
1525
|
response.send(EDITOR_HTML, {
|
|
1233
1526
|
headers: { 'Content-Type': 'text/html' },
|
|
1234
1527
|
});
|
|
1235
1528
|
}
|
|
1236
1529
|
|
|
1530
|
+
private serveTrainingUI(response: HttpResponse): void {
|
|
1531
|
+
response.send(TRAINING_HTML, {
|
|
1532
|
+
headers: { 'Content-Type': 'text/html' },
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1237
1536
|
private serveStaticFile(path: string, response: HttpResponse): void {
|
|
1238
1537
|
// Serve static files for the UI
|
|
1239
1538
|
response.send('Not found', { code: 404 });
|