@blueharford/scrypted-spatial-awareness 0.2.1 → 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/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
 
@@ -115,6 +117,41 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
115
117
  description: 'Use LLM plugin (if installed) to generate descriptive alerts like "Man walking from garage towards front door"',
116
118
  group: 'AI & Spatial Reasoning',
117
119
  },
120
+ llmDebounceInterval: {
121
+ title: 'LLM Rate Limit (seconds)',
122
+ type: 'number',
123
+ defaultValue: 10,
124
+ description: 'Minimum time between LLM calls to prevent API overload (0 = no limit)',
125
+ group: 'AI & Spatial Reasoning',
126
+ },
127
+ llmFallbackEnabled: {
128
+ title: 'Fallback to Basic Notifications',
129
+ type: 'boolean',
130
+ defaultValue: true,
131
+ description: 'When LLM is rate-limited or slow, fall back to basic notifications immediately',
132
+ group: 'AI & Spatial Reasoning',
133
+ },
134
+ llmFallbackTimeout: {
135
+ title: 'LLM Timeout (seconds)',
136
+ type: 'number',
137
+ defaultValue: 3,
138
+ description: 'Maximum time to wait for LLM response before falling back to basic notification',
139
+ group: 'AI & Spatial Reasoning',
140
+ },
141
+ enableTransitTimeLearning: {
142
+ title: 'Learn Transit Times',
143
+ type: 'boolean',
144
+ defaultValue: true,
145
+ description: 'Automatically adjust connection transit times based on observed movement patterns',
146
+ group: 'AI & Spatial Reasoning',
147
+ },
148
+ enableConnectionSuggestions: {
149
+ title: 'Suggest Camera Connections',
150
+ type: 'boolean',
151
+ defaultValue: true,
152
+ description: 'Automatically suggest new camera connections based on observed movement patterns',
153
+ group: 'AI & Spatial Reasoning',
154
+ },
118
155
  enableLandmarkLearning: {
119
156
  title: 'Learn Landmarks from AI',
120
157
  type: 'boolean',
@@ -291,6 +328,11 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
291
328
  loiteringThreshold: (this.storageSettings.values.loiteringThreshold as number || 3) * 1000,
292
329
  objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown as number || 30) * 1000,
293
330
  useLlmDescriptions: this.storageSettings.values.useLlmDescriptions as boolean ?? true,
331
+ llmDebounceInterval: (this.storageSettings.values.llmDebounceInterval as number || 10) * 1000,
332
+ llmFallbackEnabled: this.storageSettings.values.llmFallbackEnabled as boolean ?? true,
333
+ llmFallbackTimeout: (this.storageSettings.values.llmFallbackTimeout as number || 3) * 1000,
334
+ enableTransitTimeLearning: this.storageSettings.values.enableTransitTimeLearning as boolean ?? true,
335
+ enableConnectionSuggestions: this.storageSettings.values.enableConnectionSuggestions as boolean ?? true,
294
336
  enableLandmarkLearning: this.storageSettings.values.enableLandmarkLearning as boolean ?? true,
295
337
  landmarkConfidenceThreshold: this.storageSettings.values.landmarkConfidenceThreshold as number ?? 0.7,
296
338
  };
@@ -392,8 +434,91 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
392
434
  async getSettings(): Promise<Setting[]> {
393
435
  const settings = await this.storageSettings.getSettings();
394
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
+
395
520
  // Topology editor button that opens modal overlay (appended to body for proper z-index)
396
- 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.9);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:#1a1a2e;border-radius:12px;overflow:hidden;position:relative;box-shadow:0 25px 50px rgba(0,0,0,0.5);';var b=document.createElement('button');b.innerHTML='×';b.style.cssText='position:absolute;top:15px;right:15px;z-index:2147483647;background:#e94560;color:white;border:none;width:40px;height:40px;border-radius:50%;font-size:24px;cursor:pointer;';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);})()`;
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);})()`;
397
522
 
398
523
  settings.push({
399
524
  key: 'topologyEditor',
@@ -402,39 +527,41 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
402
527
  value: `
403
528
  <style>
404
529
  .sa-open-btn {
405
- background: linear-gradient(135deg, #e94560 0%, #0f3460 100%);
406
- color: white;
530
+ background: #4fc3f7;
531
+ color: #000;
407
532
  border: none;
408
- padding: 14px 28px;
409
- border-radius: 8px;
410
- font-size: 15px;
411
- font-weight: 600;
533
+ padding: 10px 20px;
534
+ border-radius: 4px;
535
+ font-size: 14px;
536
+ font-weight: 500;
412
537
  cursor: pointer;
413
538
  display: inline-flex;
414
539
  align-items: center;
415
- gap: 10px;
416
- transition: transform 0.2s, box-shadow 0.2s;
540
+ gap: 8px;
541
+ transition: background 0.2s;
542
+ font-family: inherit;
417
543
  }
418
544
  .sa-open-btn:hover {
419
- transform: translateY(-2px);
420
- box-shadow: 0 8px 20px rgba(233, 69, 96, 0.4);
545
+ background: #81d4fa;
421
546
  }
422
547
  .sa-btn-container {
423
- padding: 20px;
424
- background: #16213e;
425
- border-radius: 8px;
548
+ padding: 16px;
549
+ background: rgba(255,255,255,0.03);
550
+ border-radius: 4px;
426
551
  text-align: center;
552
+ border: 1px solid rgba(255,255,255,0.08);
427
553
  }
428
554
  .sa-btn-desc {
429
- color: #888;
430
- margin-bottom: 15px;
431
- font-size: 14px;
555
+ color: rgba(255,255,255,0.6);
556
+ margin-bottom: 12px;
557
+ font-size: 13px;
558
+ font-family: inherit;
432
559
  }
433
560
  </style>
434
561
  <div class="sa-btn-container">
435
562
  <p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
436
563
  <button class="sa-open-btn" onclick="${onclickCode}">
437
- <span>&#9881;</span> Open Topology Editor
564
+ Open Topology Editor
438
565
  </button>
439
566
  </div>
440
567
  `,
@@ -576,6 +703,11 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
576
703
  key === 'loiteringThreshold' ||
577
704
  key === 'objectAlertCooldown' ||
578
705
  key === 'useLlmDescriptions' ||
706
+ key === 'llmDebounceInterval' ||
707
+ key === 'llmFallbackEnabled' ||
708
+ key === 'llmFallbackTimeout' ||
709
+ key === 'enableTransitTimeLearning' ||
710
+ key === 'enableConnectionSuggestions' ||
579
711
  key === 'enableLandmarkLearning' ||
580
712
  key === 'landmarkConfidenceThreshold'
581
713
  ) {
@@ -668,11 +800,61 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
668
800
  return this.handleInferRelationshipsRequest(response);
669
801
  }
670
802
 
803
+ // Connection suggestions
804
+ if (path.endsWith('/api/connection-suggestions')) {
805
+ return this.handleConnectionSuggestionsRequest(request, response);
806
+ }
807
+
808
+ if (path.match(/\/api\/connection-suggestions\/[\w->]+\/(accept|reject)$/)) {
809
+ const parts = path.split('/');
810
+ const action = parts.pop()!;
811
+ const suggestionId = parts.pop()!;
812
+ return this.handleConnectionSuggestionActionRequest(suggestionId, action, response);
813
+ }
814
+
815
+ // Live tracking state
816
+ if (path.endsWith('/api/live-tracking')) {
817
+ return this.handleLiveTrackingRequest(response);
818
+ }
819
+
820
+ // Journey visualization
821
+ if (path.match(/\/api\/journey-path\/[\w-]+$/)) {
822
+ const globalId = path.split('/').pop()!;
823
+ return this.handleJourneyPathRequest(globalId, response);
824
+ }
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
+
671
849
  // UI Routes
672
850
  if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
673
851
  return this.serveEditorUI(response);
674
852
  }
675
853
 
854
+ if (path.endsWith('/ui/training') || path.endsWith('/ui/training/')) {
855
+ return this.serveTrainingUI(response);
856
+ }
857
+
676
858
  if (path.includes('/ui/')) {
677
859
  return this.serveStaticFile(path, response);
678
860
  }
@@ -680,17 +862,31 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
680
862
  // Default: return info page
681
863
  response.send(JSON.stringify({
682
864
  name: 'Spatial Awareness Plugin',
683
- version: '0.1.0',
865
+ version: '0.4.0',
684
866
  endpoints: {
685
867
  api: {
686
868
  trackedObjects: '/api/tracked-objects',
687
869
  journey: '/api/journey/{globalId}',
870
+ journeyPath: '/api/journey-path/{globalId}',
688
871
  topology: '/api/topology',
689
872
  alerts: '/api/alerts',
690
873
  floorPlan: '/api/floor-plan',
874
+ liveTracking: '/api/live-tracking',
875
+ connectionSuggestions: '/api/connection-suggestions',
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
+ },
691
886
  },
692
887
  ui: {
693
888
  editor: '/ui/editor',
889
+ training: '/ui/training',
694
890
  },
695
891
  },
696
892
  }), {
@@ -1054,12 +1250,289 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1054
1250
  });
1055
1251
  }
1056
1252
 
1253
+ private handleConnectionSuggestionsRequest(request: HttpRequest, response: HttpResponse): void {
1254
+ if (!this.trackingEngine) {
1255
+ response.send(JSON.stringify({ suggestions: [] }), {
1256
+ headers: { 'Content-Type': 'application/json' },
1257
+ });
1258
+ return;
1259
+ }
1260
+
1261
+ const suggestions = this.trackingEngine.getConnectionSuggestions();
1262
+ response.send(JSON.stringify({
1263
+ suggestions,
1264
+ count: suggestions.length,
1265
+ }), {
1266
+ headers: { 'Content-Type': 'application/json' },
1267
+ });
1268
+ }
1269
+
1270
+ private handleConnectionSuggestionActionRequest(
1271
+ suggestionId: string,
1272
+ action: string,
1273
+ response: HttpResponse
1274
+ ): void {
1275
+ if (!this.trackingEngine) {
1276
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1277
+ code: 500,
1278
+ headers: { 'Content-Type': 'application/json' },
1279
+ });
1280
+ return;
1281
+ }
1282
+
1283
+ if (action === 'accept') {
1284
+ const connection = this.trackingEngine.acceptConnectionSuggestion(suggestionId);
1285
+ if (connection) {
1286
+ // Save updated topology
1287
+ const topology = this.trackingEngine.getTopology();
1288
+ this.storage.setItem('topology', JSON.stringify(topology));
1289
+
1290
+ response.send(JSON.stringify({ success: true, connection }), {
1291
+ headers: { 'Content-Type': 'application/json' },
1292
+ });
1293
+ } else {
1294
+ response.send(JSON.stringify({ error: 'Suggestion not found' }), {
1295
+ code: 404,
1296
+ headers: { 'Content-Type': 'application/json' },
1297
+ });
1298
+ }
1299
+ } else if (action === 'reject') {
1300
+ const success = this.trackingEngine.rejectConnectionSuggestion(suggestionId);
1301
+ if (success) {
1302
+ response.send(JSON.stringify({ success: true }), {
1303
+ headers: { 'Content-Type': 'application/json' },
1304
+ });
1305
+ } else {
1306
+ response.send(JSON.stringify({ error: 'Suggestion not found' }), {
1307
+ code: 404,
1308
+ headers: { 'Content-Type': 'application/json' },
1309
+ });
1310
+ }
1311
+ } else {
1312
+ response.send(JSON.stringify({ error: 'Invalid action' }), {
1313
+ code: 400,
1314
+ headers: { 'Content-Type': 'application/json' },
1315
+ });
1316
+ }
1317
+ }
1318
+
1319
+ private handleLiveTrackingRequest(response: HttpResponse): void {
1320
+ if (!this.trackingEngine) {
1321
+ response.send(JSON.stringify({ objects: [], timestamp: Date.now() }), {
1322
+ headers: { 'Content-Type': 'application/json' },
1323
+ });
1324
+ return;
1325
+ }
1326
+
1327
+ const liveState = this.trackingEngine.getLiveTrackingState();
1328
+ response.send(JSON.stringify(liveState), {
1329
+ headers: { 'Content-Type': 'application/json' },
1330
+ });
1331
+ }
1332
+
1333
+ private handleJourneyPathRequest(globalId: string, response: HttpResponse): void {
1334
+ if (!this.trackingEngine) {
1335
+ response.send(JSON.stringify({ error: 'Tracking engine not running' }), {
1336
+ code: 500,
1337
+ headers: { 'Content-Type': 'application/json' },
1338
+ });
1339
+ return;
1340
+ }
1341
+
1342
+ const journeyPath = this.trackingEngine.getJourneyPath(globalId);
1343
+ if (journeyPath) {
1344
+ response.send(JSON.stringify(journeyPath), {
1345
+ headers: { 'Content-Type': 'application/json' },
1346
+ });
1347
+ } else {
1348
+ response.send(JSON.stringify({ error: 'Object not found' }), {
1349
+ code: 404,
1350
+ headers: { 'Content-Type': 'application/json' },
1351
+ });
1352
+ }
1353
+ }
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
+
1057
1524
  private serveEditorUI(response: HttpResponse): void {
1058
1525
  response.send(EDITOR_HTML, {
1059
1526
  headers: { 'Content-Type': 'text/html' },
1060
1527
  });
1061
1528
  }
1062
1529
 
1530
+ private serveTrainingUI(response: HttpResponse): void {
1531
+ response.send(TRAINING_HTML, {
1532
+ headers: { 'Content-Type': 'text/html' },
1533
+ });
1534
+ }
1535
+
1063
1536
  private serveStaticFile(path: string, response: HttpResponse): void {
1064
1537
  // Serve static files for the UI
1065
1538
  response.send('Not found', { code: 404 });