@blueharford/scrypted-spatial-awareness 0.6.16 → 0.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.zip CHANGED
Binary file
@@ -36236,7 +36236,10 @@ For EACH landmark, estimate its DISTANCE from the camera:
36236
36236
  - Street/road
36237
36237
  - Neighbor's yard
36238
36238
 
36239
- For each zone, estimate what percentage of the image it covers (0.0 to 1.0).
36239
+ For each zone, provide:
36240
+ - coverage: percentage of the image it covers (0.0 to 1.0)
36241
+ - distance: how far the CENTER of the zone is from camera ("close", "near", "medium", "far", "distant")
36242
+ - boundingBox: [x, y, width, height] in normalized coordinates (0-1) where the zone appears in the image
36240
36243
 
36241
36244
  ## 3. EDGES - What's at each edge of the frame:
36242
36245
  This helps understand what's just out of view.
@@ -36254,8 +36257,9 @@ Respond with ONLY valid JSON:
36254
36257
  {"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
36255
36258
  ],
36256
36259
  "zones": [
36257
- {"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
36258
- {"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
36260
+ {"name": "Front Yard", "type": "yard", "coverage": 0.5, "distance": "medium", "boundingBox": [0.2, 0.4, 0.6, 0.4], "description": "Grass lawn with some bare patches"},
36261
+ {"name": "Driveway", "type": "driveway", "coverage": 0.25, "distance": "near", "boundingBox": [0.6, 0.5, 0.3, 0.4], "description": "Concrete driveway leading to garage"},
36262
+ {"name": "Street", "type": "street", "coverage": 0.1, "distance": "distant", "boundingBox": [0.0, 0.1, 1.0, 0.15], "description": "Public road beyond property line"}
36259
36263
  ],
36260
36264
  "edges": {
36261
36265
  "top": "sky, tree canopy",
@@ -36481,13 +36485,26 @@ class TopologyDiscoveryEngine {
36481
36485
  for (const formatType of formatsToTry) {
36482
36486
  try {
36483
36487
  this.console.log(`[Discovery] Trying ${formatType} image format for ${cameraName}...`);
36488
+ // Build prompt with camera context (height)
36489
+ const cameraNode = this.topology ? (0, topology_1.findCamera)(this.topology, cameraId) : null;
36490
+ const mountHeight = cameraNode?.context?.mountHeight || 8;
36491
+ const cameraRange = cameraNode?.fov?.range || 80;
36492
+ // Add camera-specific context to the prompt
36493
+ const contextPrefix = `CAMERA INFORMATION:
36494
+ - Camera Name: ${cameraName}
36495
+ - Mount Height: ${mountHeight} feet above ground
36496
+ - Approximate viewing range: ${cameraRange} feet
36497
+
36498
+ Use the mount height to help estimate distances - objects at ground level will appear at different angles depending on distance from a camera mounted at ${mountHeight} feet.
36499
+
36500
+ `;
36484
36501
  // Build multimodal message with provider-specific image format
36485
36502
  const result = await llm.getChatCompletion({
36486
36503
  messages: [
36487
36504
  {
36488
36505
  role: 'user',
36489
36506
  content: [
36490
- { type: 'text', text: SCENE_ANALYSIS_PROMPT },
36507
+ { type: 'text', text: contextPrefix + SCENE_ANALYSIS_PROMPT },
36491
36508
  (0, spatial_reasoning_1.buildImageContent)(imageData, formatType),
36492
36509
  ],
36493
36510
  },
@@ -36523,6 +36540,7 @@ class TopologyDiscoveryEngine {
36523
36540
  coverage: typeof z.coverage === 'number' ? z.coverage : 0.5,
36524
36541
  description: z.description || '',
36525
36542
  boundingBox: z.boundingBox,
36543
+ distance: this.mapDistance(z.distance), // Parse distance for zones too
36526
36544
  }));
36527
36545
  }
36528
36546
  if (parsed.edges && typeof parsed.edges === 'object') {
@@ -36644,6 +36662,26 @@ class TopologyDiscoveryEngine {
36644
36662
  return 'distant';
36645
36663
  return 'medium'; // Default to medium if not specified
36646
36664
  }
36665
+ /** Get default distance in feet based on zone type */
36666
+ getDefaultZoneDistance(zoneType) {
36667
+ switch (zoneType) {
36668
+ case 'patio':
36669
+ case 'walkway':
36670
+ return 10; // Close zones
36671
+ case 'driveway':
36672
+ case 'parking':
36673
+ return 25; // Near zones
36674
+ case 'yard':
36675
+ case 'garden':
36676
+ case 'pool':
36677
+ return 40; // Medium zones
36678
+ case 'street':
36679
+ return 100; // Far zones
36680
+ case 'unknown':
36681
+ default:
36682
+ return 50; // Default to medium distance
36683
+ }
36684
+ }
36647
36685
  /** Try to parse JSON with recovery for truncated responses */
36648
36686
  parseJsonWithRecovery(jsonStr, context) {
36649
36687
  // First, try direct parse
@@ -36937,6 +36975,9 @@ class TopologyDiscoveryEngine {
36937
36975
  // Generate zone suggestions (even for smaller coverage - 10% is enough)
36938
36976
  for (const zone of analysis.zones) {
36939
36977
  if (zone.coverage >= 0.1) {
36978
+ // Calculate distance in feet from distance estimate (for zones with distance info)
36979
+ const zoneWithDist = zone;
36980
+ const distanceFeet = zoneWithDist.distance ? (0, discovery_1.distanceToFeet)(zoneWithDist.distance) : this.getDefaultZoneDistance(zone.type);
36940
36981
  const suggestion = {
36941
36982
  id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36942
36983
  type: 'zone',
@@ -36944,10 +36985,14 @@ class TopologyDiscoveryEngine {
36944
36985
  sourceCameras: [analysis.cameraId],
36945
36986
  confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
36946
36987
  status: 'pending',
36947
- zone: zone,
36988
+ zone: {
36989
+ ...zone,
36990
+ // Include distance metadata for positioning
36991
+ distanceFeet: distanceFeet,
36992
+ },
36948
36993
  };
36949
36994
  this.suggestions.set(suggestion.id, suggestion);
36950
- this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
36995
+ this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage, ~${distanceFeet}ft)`);
36951
36996
  }
36952
36997
  }
36953
36998
  }
@@ -40771,61 +40816,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40771
40816
  // Convert direction to radians (0 = up/north, 90 = right/east)
40772
40817
  const dirRad = (direction - 90) * Math.PI / 180;
40773
40818
  const halfFov = (fovAngle / 2) * Math.PI / 180;
40774
- // Use landmark TYPE to determine distance - some things are inherently far away
40775
- // regardless of where they appear in the camera frame
40776
- const landmarkType = suggestion.landmark.type;
40777
- const farTypes = ['neighbor', 'boundary', 'street']; // Always place at edge of FOV
40778
- const isFarType = farTypes.includes(landmarkType || '');
40779
- // Use bounding box if available for horizontal positioning
40780
- // boundingBox format: [x, y, width, height] normalized 0-1
40781
- const bbox = suggestion.landmark.boundingBox;
40819
+ // Get floor plan scale (pixels per foot) - default 5
40820
+ const floorPlanScale = topology.floorPlanScale || 5;
40821
+ // Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
40822
+ // The LLM estimates distance based on object size, perspective, and camera context
40823
+ const landmarkData = suggestion.landmark;
40824
+ const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
40825
+ const distanceInPixels = distanceFeet * floorPlanScale;
40826
+ // Use bounding box for horizontal positioning within the FOV
40827
+ const bbox = landmarkData.boundingBox;
40782
40828
  let angleOffset;
40783
- let distanceMultiplier;
40784
40829
  if (bbox && bbox.length >= 2) {
40785
40830
  // Use bounding box X for horizontal position in FOV
40786
40831
  const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
40787
40832
  // Map X position to angle within FOV
40788
40833
  angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
40789
- // For distance: use TYPE first, then bbox Y as hint
40790
- if (isFarType) {
40791
- // Neighbors, boundaries, streets are BEYOND the camera's normal range
40792
- // Place at 150-200% of range to indicate they're distant background features
40793
- distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
40794
- this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
40795
- }
40796
- else {
40797
- // For other types, use bbox Y as a hint (but cap minimum distance)
40798
- const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
40799
- // Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
40800
- distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
40801
- }
40802
- this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
40834
+ this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
40803
40835
  }
40804
40836
  else {
40805
- // No bounding box - use type and spread pattern
40837
+ // No bounding box - spread horizontally based on existing landmarks
40806
40838
  const cameraDeviceId = camera.deviceId;
40807
40839
  const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
40808
40840
  l.visibleFromCameras?.includes(camera.name)).length;
40809
40841
  // Spread horizontally across FOV
40810
40842
  angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
40811
- // Distance based on type
40812
- if (isFarType) {
40813
- // Neighbors, boundaries, streets are BEYOND the camera's normal range
40814
- distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
40815
- this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
40816
- }
40817
- else {
40818
- distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
40819
- this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
40820
- }
40843
+ this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
40821
40844
  }
40822
40845
  const finalAngle = dirRad + angleOffset;
40823
- const distance = range * distanceMultiplier;
40824
40846
  position = {
40825
- x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distance,
40826
- y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distance,
40847
+ x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
40848
+ y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
40827
40849
  };
40828
- this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" in ${camera.name}'s FOV: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°, dist=${distance.toFixed(0)}px`);
40850
+ this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°`);
40829
40851
  }
40830
40852
  else {
40831
40853
  // Position in a grid pattern starting from center
@@ -40878,48 +40900,53 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40878
40900
  let polygon = [];
40879
40901
  const timestamp = Date.now();
40880
40902
  if (camera?.floorPlanPosition) {
40881
- // Get camera's FOV direction and range (cast to any for flexible access)
40903
+ // Get camera's FOV direction (cast to any for flexible access)
40882
40904
  const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
40883
40905
  const direction = fov.direction || 0;
40884
- const range = fov.range || 80;
40885
40906
  const fovAngle = fov.angle || 90;
40907
+ // Get floor plan scale (pixels per foot)
40908
+ const floorPlanScale = topology.floorPlanScale || 5;
40886
40909
  // Convert direction to radians (0 = up/north, 90 = right/east)
40887
40910
  const dirRad = (direction - 90) * Math.PI / 180;
40888
40911
  const halfFov = (fovAngle / 2) * Math.PI / 180;
40889
40912
  const camX = camera.floorPlanPosition.x;
40890
40913
  const camY = camera.floorPlanPosition.y;
40891
- // Use bounding box if available to position zone accurately within FOV
40914
+ // Use distanceFeet from the zone metadata for accurate positioning
40915
+ const zoneData = zone;
40916
+ const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
40917
+ const distanceInPixels = distanceFeet * floorPlanScale;
40918
+ // Zone size based on coverage (larger coverage = wider zone)
40919
+ const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
40920
+ const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
40921
+ const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
40922
+ // Use bounding box for horizontal positioning if available
40892
40923
  const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
40893
- let innerRadius;
40894
- let outerRadius;
40895
40924
  let angleStart;
40896
40925
  let angleEnd;
40926
+ let innerRadius;
40927
+ let outerRadius;
40897
40928
  if (bbox && bbox.length >= 4) {
40898
- // Map bounding box to position within FOV
40929
+ // Map bounding box X to angle within FOV
40899
40930
  const bboxLeft = bbox[0];
40900
40931
  const bboxRight = bbox[0] + bbox[2];
40901
- const bboxTop = bbox[1];
40902
- const bboxBottom = bbox[1] + bbox[3];
40903
- // Map X to angle within FOV (0 = left edge, 1 = right edge)
40904
40932
  angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
40905
40933
  angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
40906
- // Map Y to distance (0 = far, 1 = close)
40907
- innerRadius = range * (0.9 - bboxBottom * 0.6);
40908
- outerRadius = range * (0.9 - bboxTop * 0.6);
40909
- // Ensure min size
40910
- if (outerRadius - innerRadius < 20) {
40911
- outerRadius = innerRadius + 20;
40912
- }
40913
- this.console.log(`[Discovery] Zone "${zone.name}" using bbox [${bbox.join(',')}] → angles ${(angleStart * 180 / Math.PI).toFixed(1)}° to ${(angleEnd * 180 / Math.PI).toFixed(1)}°`);
40934
+ // Use distanceFeet for depth, with a spread based on coverage
40935
+ innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
40936
+ outerRadius = distanceInPixels + zoneDepthPixels / 2;
40937
+ this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
40914
40938
  }
40915
40939
  else {
40916
- // Fallback: wedge-shaped zone offset by existing count
40940
+ // Fallback: wedge-shaped zone covering portion of FOV
40917
40941
  const existingFromCamera = (topology.drawnZones || []).filter((z) => z.linkedCameras?.includes(sourceCameras[0])).length;
40918
- innerRadius = range * 0.3 + existingFromCamera * 20;
40919
- outerRadius = range * 0.8 + existingFromCamera * 20;
40920
- angleStart = dirRad - halfFov * 0.7;
40921
- angleEnd = dirRad + halfFov * 0.7;
40922
- this.console.log(`[Discovery] Zone "${zone.name}" using fallback spread (existing: ${existingFromCamera})`);
40942
+ // Spread horizontally based on existing zones
40943
+ const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
40944
+ angleStart = dirRad + offset - halfFov * 0.3;
40945
+ angleEnd = dirRad + offset + halfFov * 0.3;
40946
+ // Use distanceFeet for depth
40947
+ innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
40948
+ outerRadius = distanceInPixels + zoneDepthPixels / 2;
40949
+ this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
40923
40950
  }
40924
40951
  // Create arc polygon
40925
40952
  const steps = 8;
@@ -40939,7 +40966,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40939
40966
  y: camY + Math.sin(angle) * outerRadius,
40940
40967
  });
40941
40968
  }
40942
- this.console.log(`[Discovery] Creating zone "${zone.name}" in ${camera.name}'s FOV: dir=${direction}°`);
40969
+ this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
40943
40970
  }
40944
40971
  else {
40945
40972
  // Fallback: rectangular zone at default location
@@ -42541,6 +42568,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42541
42568
  const response = await fetch('../api/topology');
42542
42569
  if (response.ok) {
42543
42570
  topology = await response.json();
42571
+ console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
42544
42572
  if (!topology.drawings) topology.drawings = [];
42545
42573
  // Load floor plan scale if saved
42546
42574
  if (topology.floorPlanScale) {
@@ -43058,14 +43086,24 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43058
43086
  async function saveTopology() {
43059
43087
  try {
43060
43088
  setStatus('Saving...', 'warning');
43089
+ console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
43061
43090
  const response = await fetch('../api/topology', {
43062
43091
  method: 'PUT',
43063
43092
  headers: { 'Content-Type': 'application/json' },
43064
43093
  body: JSON.stringify(topology)
43065
43094
  });
43066
- if (response.ok) { setStatus('Saved successfully', 'success'); }
43067
- else { setStatus('Failed to save', 'error'); }
43068
- } catch (e) { console.error('Failed to save topology:', e); setStatus('Failed to save', 'error'); }
43095
+ if (response.ok) {
43096
+ console.log('[Save] Save successful');
43097
+ setStatus('Saved successfully', 'success');
43098
+ } else {
43099
+ const errorText = await response.text();
43100
+ console.error('[Save] Save failed:', response.status, errorText);
43101
+ setStatus('Failed to save: ' + errorText, 'error');
43102
+ }
43103
+ } catch (e) {
43104
+ console.error('Failed to save topology:', e);
43105
+ setStatus('Failed to save', 'error');
43106
+ }
43069
43107
  }
43070
43108
 
43071
43109
  function resizeCanvas() {
@@ -43719,8 +43757,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43719
43757
  const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
43720
43758
  // Convert stored pixel range to feet for display
43721
43759
  const rangeInFeet = Math.round(pixelsToFeet(fov.range || 80));
43760
+ // Get mount height from context or default to 8
43761
+ const mountHeight = camera.context?.mountHeight || 8;
43722
43762
  panel.innerHTML = '<h3>Camera Properties</h3>' +
43723
43763
  '<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
43764
+ '<div class="form-group"><label>Mount Height (feet)</label><input type="number" value="' + mountHeight + '" min="1" max="40" step="0.5" onchange="updateCameraMountHeight(\\'' + camera.deviceId + '\\', this.value)"></div>' +
43765
+ '<div style="font-size: 11px; color: #666; margin-top: -10px; margin-bottom: 10px;">Height affects distance estimation in discovery</div>' +
43724
43766
  '<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
43725
43767
  '<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
43726
43768
  '<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
@@ -43739,6 +43781,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43739
43781
  function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
43740
43782
  function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
43741
43783
  function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
43784
+ function updateCameraMountHeight(id, value) {
43785
+ const camera = topology.cameras.find(c => c.deviceId === id);
43786
+ if (!camera) return;
43787
+ if (!camera.context) camera.context = {};
43788
+ camera.context.mountHeight = parseFloat(value) || 8;
43789
+ }
43742
43790
  function updateCameraFov(id, field, value) {
43743
43791
  const camera = topology.cameras.find(c => c.deviceId === id);
43744
43792
  if (!camera) return;
@@ -43855,9 +43903,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43855
43903
  polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
43856
43904
  };
43857
43905
 
43906
+ console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
43907
+
43858
43908
  if (!topology.drawnZones) topology.drawnZones = [];
43859
43909
  topology.drawnZones.push(zone);
43860
43910
 
43911
+ console.log('[Zone] Total zones now:', topology.drawnZones.length);
43912
+
43861
43913
  // Reset state
43862
43914
  zoneDrawingMode = false;
43863
43915
  currentZonePoints = [];
@@ -43866,7 +43918,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43866
43918
  setTool('select');
43867
43919
  updateUI();
43868
43920
  render();
43869
- setStatus('Zone "' + zone.name + '" created with ' + zone.polygon.length + ' points', 'success');
43921
+
43922
+ console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
43923
+
43924
+ setStatus('Zone "' + zone.name + '" created - saving...', 'success');
43925
+
43926
+ // Auto-save after creating a zone
43927
+ saveTopology().then(() => {
43928
+ console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
43929
+ setStatus('Zone "' + zone.name + '" saved', 'success');
43930
+ }).catch(err => {
43931
+ console.error('Failed to auto-save zone:', err);
43932
+ setStatus('Zone created but auto-save failed - click Save', 'warning');
43933
+ });
43870
43934
  }
43871
43935
 
43872
43936
  function selectZone(id) {
@@ -43891,9 +43955,9 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43891
43955
  '<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
43892
43956
  }
43893
43957
 
43894
- function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); } }
43895
- function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); } }
43896
- function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; } }
43958
+ function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
43959
+ function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
43960
+ function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
43897
43961
  function deleteZone(id) {
43898
43962
  if (!confirm('Delete this zone?')) return;
43899
43963
  topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
@@ -43901,7 +43965,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43901
43965
  document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
43902
43966
  updateUI();
43903
43967
  render();
43904
- setStatus('Zone deleted', 'success');
43968
+ setStatus('Zone deleted - saving...', 'success');
43969
+ saveTopology().then(() => setStatus('Zone deleted', 'success'));
43905
43970
  }
43906
43971
 
43907
43972
  function useBlankCanvas() {