@blueharford/scrypted-spatial-awareness 0.6.17 → 0.6.20

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",
@@ -36536,6 +36540,7 @@ Use the mount height to help estimate distances - objects at ground level will a
36536
36540
  coverage: typeof z.coverage === 'number' ? z.coverage : 0.5,
36537
36541
  description: z.description || '',
36538
36542
  boundingBox: z.boundingBox,
36543
+ distance: this.mapDistance(z.distance), // Parse distance for zones too
36539
36544
  }));
36540
36545
  }
36541
36546
  if (parsed.edges && typeof parsed.edges === 'object') {
@@ -36657,6 +36662,26 @@ Use the mount height to help estimate distances - objects at ground level will a
36657
36662
  return 'distant';
36658
36663
  return 'medium'; // Default to medium if not specified
36659
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
+ }
36660
36685
  /** Try to parse JSON with recovery for truncated responses */
36661
36686
  parseJsonWithRecovery(jsonStr, context) {
36662
36687
  // First, try direct parse
@@ -36950,6 +36975,9 @@ Use the mount height to help estimate distances - objects at ground level will a
36950
36975
  // Generate zone suggestions (even for smaller coverage - 10% is enough)
36951
36976
  for (const zone of analysis.zones) {
36952
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);
36953
36981
  const suggestion = {
36954
36982
  id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36955
36983
  type: 'zone',
@@ -36957,10 +36985,14 @@ Use the mount height to help estimate distances - objects at ground level will a
36957
36985
  sourceCameras: [analysis.cameraId],
36958
36986
  confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
36959
36987
  status: 'pending',
36960
- zone: zone,
36988
+ zone: {
36989
+ ...zone,
36990
+ // Include distance metadata for positioning
36991
+ distanceFeet: distanceFeet,
36992
+ },
36961
36993
  };
36962
36994
  this.suggestions.set(suggestion.id, suggestion);
36963
- 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)`);
36964
36996
  }
36965
36997
  }
36966
36998
  }
@@ -39995,6 +40027,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
39995
40027
  if (request.method === 'GET') {
39996
40028
  const topologyJson = this.storage.getItem('topology');
39997
40029
  const topology = topologyJson ? JSON.parse(topologyJson) : (0, topology_1.createEmptyTopology)();
40030
+ this.console.log(`[Topology API] GET - drawnZones: ${topology.drawnZones?.length || 0}`);
39998
40031
  response.send(JSON.stringify(topology), {
39999
40032
  headers: { 'Content-Type': 'application/json' },
40000
40033
  });
@@ -40002,6 +40035,10 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40002
40035
  else if (request.method === 'PUT' || request.method === 'POST') {
40003
40036
  try {
40004
40037
  const topology = JSON.parse(request.body);
40038
+ this.console.log(`[Topology API] PUT received - drawnZones: ${topology.drawnZones?.length || 0}`);
40039
+ if (topology.drawnZones?.length) {
40040
+ this.console.log(`[Topology API] Zone names: ${topology.drawnZones.map(z => z.name).join(', ')}`);
40041
+ }
40005
40042
  this.storage.setItem('topology', JSON.stringify(topology));
40006
40043
  await this.startTrackingEngine(topology);
40007
40044
  response.send(JSON.stringify({ success: true }), {
@@ -40009,6 +40046,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40009
40046
  });
40010
40047
  }
40011
40048
  catch (e) {
40049
+ this.console.error(`[Topology API] PUT error:`, e);
40012
40050
  response.send(JSON.stringify({ error: 'Invalid topology JSON' }), {
40013
40051
  code: 400,
40014
40052
  headers: { 'Content-Type': 'application/json' },
@@ -40784,61 +40822,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40784
40822
  // Convert direction to radians (0 = up/north, 90 = right/east)
40785
40823
  const dirRad = (direction - 90) * Math.PI / 180;
40786
40824
  const halfFov = (fovAngle / 2) * Math.PI / 180;
40787
- // Use landmark TYPE to determine distance - some things are inherently far away
40788
- // regardless of where they appear in the camera frame
40789
- const landmarkType = suggestion.landmark.type;
40790
- const farTypes = ['neighbor', 'boundary', 'street']; // Always place at edge of FOV
40791
- const isFarType = farTypes.includes(landmarkType || '');
40792
- // Use bounding box if available for horizontal positioning
40793
- // boundingBox format: [x, y, width, height] normalized 0-1
40794
- const bbox = suggestion.landmark.boundingBox;
40825
+ // Get floor plan scale (pixels per foot) - default 5
40826
+ const floorPlanScale = topology.floorPlanScale || 5;
40827
+ // Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
40828
+ // The LLM estimates distance based on object size, perspective, and camera context
40829
+ const landmarkData = suggestion.landmark;
40830
+ const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
40831
+ const distanceInPixels = distanceFeet * floorPlanScale;
40832
+ // Use bounding box for horizontal positioning within the FOV
40833
+ const bbox = landmarkData.boundingBox;
40795
40834
  let angleOffset;
40796
- let distanceMultiplier;
40797
40835
  if (bbox && bbox.length >= 2) {
40798
40836
  // Use bounding box X for horizontal position in FOV
40799
40837
  const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
40800
40838
  // Map X position to angle within FOV
40801
40839
  angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
40802
- // For distance: use TYPE first, then bbox Y as hint
40803
- if (isFarType) {
40804
- // Neighbors, boundaries, streets are BEYOND the camera's normal range
40805
- // Place at 150-200% of range to indicate they're distant background features
40806
- distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
40807
- this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
40808
- }
40809
- else {
40810
- // For other types, use bbox Y as a hint (but cap minimum distance)
40811
- const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
40812
- // Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
40813
- distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
40814
- }
40815
- this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
40840
+ this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
40816
40841
  }
40817
40842
  else {
40818
- // No bounding box - use type and spread pattern
40843
+ // No bounding box - spread horizontally based on existing landmarks
40819
40844
  const cameraDeviceId = camera.deviceId;
40820
40845
  const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
40821
40846
  l.visibleFromCameras?.includes(camera.name)).length;
40822
40847
  // Spread horizontally across FOV
40823
40848
  angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
40824
- // Distance based on type
40825
- if (isFarType) {
40826
- // Neighbors, boundaries, streets are BEYOND the camera's normal range
40827
- distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
40828
- this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
40829
- }
40830
- else {
40831
- distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
40832
- this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
40833
- }
40849
+ this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
40834
40850
  }
40835
40851
  const finalAngle = dirRad + angleOffset;
40836
- const distance = range * distanceMultiplier;
40837
40852
  position = {
40838
- x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distance,
40839
- y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distance,
40853
+ x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
40854
+ y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
40840
40855
  };
40841
- 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`);
40856
+ 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)}°`);
40842
40857
  }
40843
40858
  else {
40844
40859
  // Position in a grid pattern starting from center
@@ -40891,48 +40906,53 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40891
40906
  let polygon = [];
40892
40907
  const timestamp = Date.now();
40893
40908
  if (camera?.floorPlanPosition) {
40894
- // Get camera's FOV direction and range (cast to any for flexible access)
40909
+ // Get camera's FOV direction (cast to any for flexible access)
40895
40910
  const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
40896
40911
  const direction = fov.direction || 0;
40897
- const range = fov.range || 80;
40898
40912
  const fovAngle = fov.angle || 90;
40913
+ // Get floor plan scale (pixels per foot)
40914
+ const floorPlanScale = topology.floorPlanScale || 5;
40899
40915
  // Convert direction to radians (0 = up/north, 90 = right/east)
40900
40916
  const dirRad = (direction - 90) * Math.PI / 180;
40901
40917
  const halfFov = (fovAngle / 2) * Math.PI / 180;
40902
40918
  const camX = camera.floorPlanPosition.x;
40903
40919
  const camY = camera.floorPlanPosition.y;
40904
- // Use bounding box if available to position zone accurately within FOV
40920
+ // Use distanceFeet from the zone metadata for accurate positioning
40921
+ const zoneData = zone;
40922
+ const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
40923
+ const distanceInPixels = distanceFeet * floorPlanScale;
40924
+ // Zone size based on coverage (larger coverage = wider zone)
40925
+ const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
40926
+ const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
40927
+ const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
40928
+ // Use bounding box for horizontal positioning if available
40905
40929
  const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
40906
- let innerRadius;
40907
- let outerRadius;
40908
40930
  let angleStart;
40909
40931
  let angleEnd;
40932
+ let innerRadius;
40933
+ let outerRadius;
40910
40934
  if (bbox && bbox.length >= 4) {
40911
- // Map bounding box to position within FOV
40935
+ // Map bounding box X to angle within FOV
40912
40936
  const bboxLeft = bbox[0];
40913
40937
  const bboxRight = bbox[0] + bbox[2];
40914
- const bboxTop = bbox[1];
40915
- const bboxBottom = bbox[1] + bbox[3];
40916
- // Map X to angle within FOV (0 = left edge, 1 = right edge)
40917
40938
  angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
40918
40939
  angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
40919
- // Map Y to distance (0 = far, 1 = close)
40920
- innerRadius = range * (0.9 - bboxBottom * 0.6);
40921
- outerRadius = range * (0.9 - bboxTop * 0.6);
40922
- // Ensure min size
40923
- if (outerRadius - innerRadius < 20) {
40924
- outerRadius = innerRadius + 20;
40925
- }
40926
- 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)}°`);
40940
+ // Use distanceFeet for depth, with a spread based on coverage
40941
+ innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
40942
+ outerRadius = distanceInPixels + zoneDepthPixels / 2;
40943
+ this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
40927
40944
  }
40928
40945
  else {
40929
- // Fallback: wedge-shaped zone offset by existing count
40946
+ // Fallback: wedge-shaped zone covering portion of FOV
40930
40947
  const existingFromCamera = (topology.drawnZones || []).filter((z) => z.linkedCameras?.includes(sourceCameras[0])).length;
40931
- innerRadius = range * 0.3 + existingFromCamera * 20;
40932
- outerRadius = range * 0.8 + existingFromCamera * 20;
40933
- angleStart = dirRad - halfFov * 0.7;
40934
- angleEnd = dirRad + halfFov * 0.7;
40935
- this.console.log(`[Discovery] Zone "${zone.name}" using fallback spread (existing: ${existingFromCamera})`);
40948
+ // Spread horizontally based on existing zones
40949
+ const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
40950
+ angleStart = dirRad + offset - halfFov * 0.3;
40951
+ angleEnd = dirRad + offset + halfFov * 0.3;
40952
+ // Use distanceFeet for depth
40953
+ innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
40954
+ outerRadius = distanceInPixels + zoneDepthPixels / 2;
40955
+ this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
40936
40956
  }
40937
40957
  // Create arc polygon
40938
40958
  const steps = 8;
@@ -40952,7 +40972,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40952
40972
  y: camY + Math.sin(angle) * outerRadius,
40953
40973
  });
40954
40974
  }
40955
- this.console.log(`[Discovery] Creating zone "${zone.name}" in ${camera.name}'s FOV: dir=${direction}°`);
40975
+ this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
40956
40976
  }
40957
40977
  else {
40958
40978
  // Fallback: rectangular zone at default location
@@ -42554,6 +42574,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42554
42574
  const response = await fetch('../api/topology');
42555
42575
  if (response.ok) {
42556
42576
  topology = await response.json();
42577
+ console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
42557
42578
  if (!topology.drawings) topology.drawings = [];
42558
42579
  // Load floor plan scale if saved
42559
42580
  if (topology.floorPlanScale) {
@@ -43071,14 +43092,24 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43071
43092
  async function saveTopology() {
43072
43093
  try {
43073
43094
  setStatus('Saving...', 'warning');
43095
+ console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
43074
43096
  const response = await fetch('../api/topology', {
43075
43097
  method: 'PUT',
43076
43098
  headers: { 'Content-Type': 'application/json' },
43077
43099
  body: JSON.stringify(topology)
43078
43100
  });
43079
- if (response.ok) { setStatus('Saved successfully', 'success'); }
43080
- else { setStatus('Failed to save', 'error'); }
43081
- } catch (e) { console.error('Failed to save topology:', e); setStatus('Failed to save', 'error'); }
43101
+ if (response.ok) {
43102
+ console.log('[Save] Save successful');
43103
+ setStatus('Saved successfully', 'success');
43104
+ } else {
43105
+ const errorText = await response.text();
43106
+ console.error('[Save] Save failed:', response.status, errorText);
43107
+ setStatus('Failed to save: ' + errorText, 'error');
43108
+ }
43109
+ } catch (e) {
43110
+ console.error('Failed to save topology:', e);
43111
+ setStatus('Failed to save', 'error');
43112
+ }
43082
43113
  }
43083
43114
 
43084
43115
  function resizeCanvas() {
@@ -43698,9 +43729,11 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43698
43729
  // Zone list
43699
43730
  const zoneList = document.getElementById('zone-list');
43700
43731
  const zones = topology.drawnZones || [];
43732
+ console.log('[updateUI] Zone list update - topology.drawnZones:', zones.length, 'zones');
43701
43733
  if (zones.length === 0) {
43702
43734
  zoneList.innerHTML = '<div class="zone-item" style="color: #666; text-align: center; cursor: default; padding: 8px;">No zones drawn</div>';
43703
43735
  } else {
43736
+ console.log('[updateUI] Rendering', zones.length, 'zones:', zones.map(z => z.name).join(', '));
43704
43737
  zoneList.innerHTML = zones.map(z => {
43705
43738
  const color = ZONE_STROKE_COLORS[z.type] || ZONE_STROKE_COLORS.custom;
43706
43739
  return '<div class="camera-item ' + (selectedItem?.type === 'zone' && selectedItem?.id === z.id ? 'selected' : '') + '" onclick="selectZone(\\'' + z.id + '\\')" style="border-left: 3px solid ' + color + ';"><div class="camera-name">' + z.name + '</div><div class="camera-info">' + z.type + ' | ' + z.polygon.length + ' points</div></div>';
@@ -43878,9 +43911,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43878
43911
  polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
43879
43912
  };
43880
43913
 
43914
+ console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
43915
+
43881
43916
  if (!topology.drawnZones) topology.drawnZones = [];
43882
43917
  topology.drawnZones.push(zone);
43883
43918
 
43919
+ console.log('[Zone] Total zones now:', topology.drawnZones.length);
43920
+
43884
43921
  // Reset state
43885
43922
  zoneDrawingMode = false;
43886
43923
  currentZonePoints = [];
@@ -43889,7 +43926,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43889
43926
  setTool('select');
43890
43927
  updateUI();
43891
43928
  render();
43892
- setStatus('Zone "' + zone.name + '" created with ' + zone.polygon.length + ' points', 'success');
43929
+
43930
+ console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
43931
+
43932
+ setStatus('Zone "' + zone.name + '" created - saving...', 'success');
43933
+
43934
+ // Auto-save after creating a zone
43935
+ saveTopology().then(() => {
43936
+ console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
43937
+ setStatus('Zone "' + zone.name + '" saved', 'success');
43938
+ }).catch(err => {
43939
+ console.error('Failed to auto-save zone:', err);
43940
+ setStatus('Zone created but auto-save failed - click Save', 'warning');
43941
+ });
43893
43942
  }
43894
43943
 
43895
43944
  function selectZone(id) {
@@ -43914,9 +43963,9 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43914
43963
  '<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
43915
43964
  }
43916
43965
 
43917
- function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); } }
43918
- function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); } }
43919
- function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; } }
43966
+ function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
43967
+ function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
43968
+ function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
43920
43969
  function deleteZone(id) {
43921
43970
  if (!confirm('Delete this zone?')) return;
43922
43971
  topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
@@ -43924,7 +43973,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43924
43973
  document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
43925
43974
  updateUI();
43926
43975
  render();
43927
- setStatus('Zone deleted', 'success');
43976
+ setStatus('Zone deleted - saving...', 'success');
43977
+ saveTopology().then(() => setStatus('Zone deleted', 'success'));
43928
43978
  }
43929
43979
 
43930
43980
  function useBlankCanvas() {