Files
electrical-chart/public/js/app.js
T

759 lines
25 KiB
JavaScript

'use strict';
// Application State
const state = {
panels: [],
breakers: [],
devices: [],
structures: [],
edges: [],
stats: {},
activePanel: 'subpanel',
activeBreakerId: null, // If set, Painter Mode is active!
selectedDeviceId: null,
highlightedBreakerId: null, // Filter mode
orientation: 'landscape', // 'landscape' (matching field doc) or 'native'
pan: { x: 40, y: 40 },
zoom: 0.22,
isDragging: false,
dragStart: { x: 0, y: 0 },
layers: {
lights: true,
outlets: true,
switches: true,
lines: true,
unmappedHalos: true,
rooms: true,
subpanelBoundary: true
}
};
// Coordinate transformation helpers
// Native drawio: x: 0..5000, y: 0..4800
// Landscape doc: ls_x = 4800 - y, ls_y = x
function transformCoords(x, y, w, h) {
if (state.orientation === 'landscape') {
// 90 deg rotation matching physical field doc
const ls_x = 4800 - (y + h);
const ls_y = x;
return { x: ls_x, y: ls_y, w: h, h: w, cx: ls_x + h / 2, cy: ls_y + w / 2 };
} else {
return { x, y, w, h, cx: x + w / 2, cy: y + h / 2 };
}
}
// Initialize Application
document.addEventListener('DOMContentLoaded', () => {
setupPanZoom();
setupToolbar();
setupSidebarTabs();
loadState();
});
// Load state from API
async function loadState() {
try {
const res = await fetch('/api/state');
const json = await res.json();
if (!json.success) throw new Error(json.error);
state.panels = json.data.panels;
state.breakers = json.data.breakers;
state.devices = json.data.devices;
state.structures = json.data.structures;
state.edges = json.data.edges;
state.stats = json.data.stats;
updateStatsUI();
renderBreakersList();
renderMap();
if (state.selectedDeviceId) {
updateInspector();
}
} catch (err) {
console.error('Failed to load state:', err);
}
}
// Update Top Progress Bar
function updateStatsUI() {
const { totalDevices, mappedDevices, unmappedDevices, progressPercent } = state.stats;
document.getElementById('progress-fill').style.width = `${progressPercent}%`;
document.getElementById('progress-text').textContent = `${mappedDevices} / ${totalDevices} Mapped (${progressPercent}%)`;
}
// Render Sidebar Breakers
function renderBreakersList() {
const container = document.getElementById('breakers-container');
container.innerHTML = '';
const currentBreakers = state.breakers.filter(b => b.panel_id === state.activePanel);
if (state.activePanel === 'subpanel') {
// Render as 2-column physical panel schedule
const leftColBreakers = currentBreakers.filter(b => b.slot % 2 !== 0);
const rightColBreakers = currentBreakers.filter(b => b.slot % 2 === 0);
const grid = document.createElement('div');
grid.className = 'breakers-grid';
// Left Column (Odd)
const leftCol = document.createElement('div');
leftCol.style.display = 'flex';
leftCol.style.flexDirection = 'column';
leftCol.style.gap = '8px';
leftColBreakers.forEach(b => leftCol.appendChild(createBreakerCard(b)));
// Right Column (Even)
const rightCol = document.createElement('div');
rightCol.style.display = 'flex';
rightCol.style.flexDirection = 'column';
rightCol.style.gap = '8px';
rightColBreakers.forEach(b => rightCol.appendChild(createBreakerCard(b)));
grid.appendChild(leftCol);
grid.appendChild(rightCol);
container.appendChild(grid);
} else {
// Main Panel list
const list = document.createElement('div');
list.style.display = 'flex';
list.style.flexDirection = 'column';
list.style.gap = '8px';
currentBreakers.forEach(b => list.appendChild(createBreakerCard(b)));
container.appendChild(list);
}
}
function createBreakerCard(b) {
const card = document.createElement('div');
card.className = 'breaker-card';
card.id = `card-${b.id}`;
if (state.activeBreakerId === b.id) {
card.classList.add('selected-painter');
}
if (state.highlightedBreakerId === b.id) {
card.classList.add('highlighted-filter');
}
card.innerHTML = `
<div class="breaker-header">
<span class="breaker-tag" style="background-color: ${b.color};">${b.label}</span>
<div class="breaker-meta">
<span>${b.amps}A</span>
<span>•</span>
<span>${b.poles}P ${b.voltage}V</span>
</div>
</div>
<div class="breaker-desc" title="${b.description || ''}">${b.description || 'Spare'}</div>
<div class="breaker-footer">
<span class="device-badge" id="count-${b.id}">${b.device_count || 0} dev</span>
<div style="display: flex; gap: 4px;">
<button class="btn-tool" style="padding: 2px 6px; font-size: 0.7rem;" title="Filter this circuit on map" onclick="event.stopPropagation(); toggleHighlightBreaker('${b.id}')">
<i class="fa-solid fa-eye"></i>
</button>
<button class="btn-tool" style="padding: 2px 6px; font-size: 0.7rem;" title="Edit breaker info" onclick="event.stopPropagation(); editBreakerPrompt('${b.id}')">
<i class="fa-solid fa-pen"></i>
</button>
</div>
</div>
`;
card.addEventListener('click', () => {
togglePainterMode(b.id);
});
return card;
}
// Toggle Painter Mode
function togglePainterMode(breakerId) {
if (state.activeBreakerId === breakerId) {
// Turn off painter mode
state.activeBreakerId = null;
} else {
state.activeBreakerId = breakerId;
}
updatePainterBadgeUI();
renderBreakersList();
renderMap();
}
function updatePainterBadgeUI() {
const badge = document.getElementById('active-painter-badge');
const svgElem = document.getElementById('map-svg');
if (state.activeBreakerId) {
const breaker = state.breakers.find(b => b.id === state.activeBreakerId);
badge.classList.add('active');
badge.style.backgroundColor = breaker.color;
document.getElementById('painter-label').textContent = `Painter: [${breaker.label}] ${breaker.description || ''}`;
svgElem.classList.add('painter-cursor');
} else {
badge.classList.remove('active');
svgElem.classList.remove('painter-cursor');
}
}
// Toggle Circuit Highlight Filter
function toggleHighlightBreaker(breakerId) {
if (state.highlightedBreakerId === breakerId) {
state.highlightedBreakerId = null;
} else {
state.highlightedBreakerId = breakerId;
}
renderBreakersList();
renderMap();
}
// Render SVG Map
function renderMap() {
const svg = document.getElementById('viewport-g');
svg.innerHTML = '';
const { orientation, highlightedBreakerId, layers } = state;
// 1. Draw Structures / Rooms / Walls
if (layers.rooms) {
const structGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
structGroup.setAttribute('id', 'layer-structures');
state.structures.forEach(s => {
// Ignore the extreme side black bar in clean rendering
if (s.x < 400 && s.w < 400 && s.h > 4000) return;
const t = transformCoords(s.x, s.y, s.w, s.h);
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', t.x);
rect.setAttribute('y', t.y);
rect.setAttribute('width', t.w);
rect.setAttribute('height', t.h);
rect.setAttribute('class', 'svg-room');
rect.setAttribute('rx', '8');
structGroup.appendChild(rect);
if (s.label) {
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', t.cx);
text.setAttribute('y', t.cy + 10);
text.setAttribute('class', 'svg-room-label');
text.textContent = s.label.replace('&nbsp;', ' ');
structGroup.appendChild(text);
}
});
// Subpanel West Wing Boundary Span
if (layers.subpanelBoundary) {
// In landscape, West wing is x: 400..1300, y: 2400..4350
// In native drawio, x: 2400..4350, y: 3450..4400
let subX = 400, subY = 2400, subW = 900, subH = 2000;
if (orientation === 'native') {
subX = 2400; subY = 3400; subW = 2000; subH = 950;
}
const subRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
subRect.setAttribute('x', subX);
subRect.setAttribute('y', subY);
subRect.setAttribute('width', subW);
subRect.setAttribute('height', subH);
subRect.setAttribute('class', 'svg-subpanel-boundary');
subRect.setAttribute('rx', '16');
structGroup.appendChild(subRect);
const subLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
subLabel.setAttribute('x', subX + subW / 2);
subLabel.setAttribute('y', subY + 60);
subLabel.setAttribute('class', 'svg-subpanel-tag');
subLabel.textContent = '⚡ SUBPANEL ZONE (WEST WING)';
structGroup.appendChild(subLabel);
}
svg.appendChild(structGroup);
}
// 2. Circuit Run Lines (lines connecting all devices on the same breaker)
if (layers.lines) {
const linesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
linesGroup.setAttribute('id', 'layer-circuit-lines');
// Group devices by breaker
const breakerGroups = {};
state.devices.forEach(d => {
if (d.breaker_id) {
if (!breakerGroups[d.breaker_id]) breakerGroups[d.breaker_id] = [];
breakerGroups[d.breaker_id].push(d);
}
});
Object.keys(breakerGroups).forEach(bId => {
// If a breaker is highlighted, only render that one line with prominence
if (highlightedBreakerId && highlightedBreakerId !== bId) return;
const groupDevices = breakerGroups[bId];
if (groupDevices.length > 1) {
const breaker = state.breakers.find(b => b.id === bId);
const color = breaker ? breaker.color : '#3b82f6';
// Connect points
const points = groupDevices.map(d => {
const t = transformCoords(d.orig_x, d.orig_y, d.orig_w, d.orig_h);
return `${t.cx},${t.cy}`;
}).join(' ');
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
poly.setAttribute('points', points);
poly.setAttribute('class', 'svg-circuit-line');
poly.setAttribute('stroke', color);
if (highlightedBreakerId === bId) {
poly.setAttribute('stroke-width', '6px');
poly.setAttribute('stroke-dasharray', 'none');
}
linesGroup.appendChild(poly);
}
});
svg.appendChild(linesGroup);
}
// 3. Render Devices
const deviceGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
deviceGroup.setAttribute('id', 'layer-devices');
state.devices.forEach(d => {
// Layer filtering
if (d.type === 'light' && !layers.lights) return;
if (d.type === 'outlet' && !layers.outlets) return;
if (d.type === 'switch' && !layers.switches) return;
const t = transformCoords(d.orig_x, d.orig_y, d.orig_w, d.orig_h);
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'svg-device');
g.setAttribute('id', `dev-elem-${d.id}`);
// If highlighted filter is active and this device does not belong, dim it
if (highlightedBreakerId && d.breaker_id !== highlightedBreakerId) {
g.classList.add('dimmed');
}
if (state.selectedDeviceId === d.id) {
g.classList.add('selected');
}
const isMapped = d.breaker_id !== null;
const breaker = isMapped ? state.breakers.find(b => b.id === d.breaker_id) : null;
const deviceColor = breaker ? breaker.color : d.color;
// Unmapped pulsing halo
if (!isMapped && layers.unmappedHalos) {
const halo = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
halo.setAttribute('cx', t.cx);
halo.setAttribute('cy', t.cy);
halo.setAttribute('r', '32');
halo.setAttribute('fill', 'rgba(245, 159, 0, 0.25)');
halo.setAttribute('stroke', '#f59f00');
halo.setAttribute('stroke-width', '2');
halo.setAttribute('stroke-dasharray', '4 4');
halo.setAttribute('class', 'svg-unmapped-halo');
g.appendChild(halo);
}
if (d.type === 'light') {
// Circle shape
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', t.cx);
circle.setAttribute('cy', t.cy);
circle.setAttribute('r', '26');
circle.setAttribute('fill', isMapped ? deviceColor : '#1971c2');
circle.setAttribute('stroke', '#ffffff');
circle.setAttribute('stroke-width', isMapped ? '3' : '2');
g.appendChild(circle);
// Light icon / glyph inside
const bulb = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bulb.setAttribute('cx', t.cx);
bulb.setAttribute('cy', t.cy);
bulb.setAttribute('r', '10');
bulb.setAttribute('fill', '#ffffff');
bulb.setAttribute('opacity', '0.6');
g.appendChild(bulb);
} else if (d.type === 'outlet') {
// Green Diamond shape
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const r = 26;
const pts = `${t.cx},${t.cy - r} ${t.cx + r},${t.cy} ${t.cx},${t.cy + r} ${t.cx - r},${t.cy}`;
poly.setAttribute('points', pts);
poly.setAttribute('fill', isMapped ? deviceColor : '#2f9e44');
poly.setAttribute('stroke', '#ffffff');
poly.setAttribute('stroke-width', isMapped ? '3' : '2');
g.appendChild(poly);
} else if (d.type === 'switch') {
// Red Diamond shape
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const r = 24;
const pts = `${t.cx},${t.cy - r} ${t.cx + r},${t.cy} ${t.cx},${t.cy + r} ${t.cx - r},${t.cy}`;
poly.setAttribute('points', pts);
poly.setAttribute('fill', isMapped ? deviceColor : '#e03131');
poly.setAttribute('stroke', '#ffffff');
poly.setAttribute('stroke-width', isMapped ? '3' : '2');
g.appendChild(poly);
}
// Breaker Text Badge on device
if (isMapped && breaker) {
const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');
badge.setAttribute('x', t.cx);
badge.setAttribute('y', t.cy);
badge.setAttribute('class', 'svg-breaker-badge');
badge.textContent = breaker.label;
g.appendChild(badge);
}
// Interaction Events
g.addEventListener('mouseenter', (e) => showTooltip(e, d, breaker));
g.addEventListener('mousemove', (e) => moveTooltip(e));
g.addEventListener('mouseleave', hideTooltip);
g.addEventListener('click', (e) => {
e.stopPropagation();
handleDeviceClick(d);
});
g.addEventListener('contextmenu', (e) => {
e.preventDefault();
unassignDevice(d.id);
});
deviceGroup.appendChild(g);
});
svg.appendChild(deviceGroup);
}
// Tooltip Management
const tooltip = document.getElementById('device-tooltip');
function showTooltip(e, d, breaker) {
const statusHtml = breaker
? `<span style="color: ${breaker.color}; font-weight: 700;">[${breaker.label}] ${breaker.description || ''}</span>`
: `<span style="color: #f59f00; font-weight: 700;">⚠️ UNMAPPED</span>`;
tooltip.innerHTML = `
<div style="font-weight: 800; font-size: 0.85rem; margin-bottom: 2px;">${d.label || d.id} (${d.type.toUpperCase()})</div>
<div style="color: #94a3b8; font-size: 0.75rem;">Room: <strong>${d.room || 'Unknown'}</strong></div>
<div style="margin-top: 4px; font-size: 0.78rem;">Circuit: ${statusHtml}</div>
${d.notes ? `<div style="margin-top: 2px; font-size: 0.72rem; color: #cbd5e1;"><em>${d.notes}</em></div>` : ''}
<div style="margin-top: 6px; font-size: 0.68rem; color: #64748b;">Click to inspect / Right-click to unassign</div>
`;
tooltip.style.display = 'block';
moveTooltip(e);
}
function moveTooltip(e) {
tooltip.style.left = `${e.clientX + 14}px`;
tooltip.style.top = `${e.clientY + 14}px`;
}
function hideTooltip() {
tooltip.style.display = 'none';
}
// Device Click Handling (Painter Mode vs Select Mode)
async function handleDeviceClick(device) {
if (state.activeBreakerId) {
// PAINTER MODE: Instantly assign this device to the active breaker!
await assignDevices([device.id], state.activeBreakerId);
} else {
// SELECT MODE: Open Inspector Card
state.selectedDeviceId = device.id;
updateInspector();
renderMap();
}
}
// Assign Devices API Call
async function assignDevices(deviceIds, breakerId) {
try {
const res = await fetch('/api/devices/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deviceIds, breakerId })
});
const json = await res.json();
if (!json.success) throw new Error(json.error);
// Update local state
state.devices.forEach(d => {
if (deviceIds.includes(d.id)) {
d.breaker_id = breakerId;
}
});
// Update stats locally
const mapped = state.devices.filter(d => d.breaker_id !== null).length;
state.stats.mappedDevices = mapped;
state.stats.unmappedDevices = state.devices.length - mapped;
state.stats.progressPercent = Math.round((mapped / state.devices.length) * 100);
// Update breaker counts
state.breakers.forEach(b => {
b.device_count = state.devices.filter(d => d.breaker_id === b.id).length;
});
updateStatsUI();
renderBreakersList();
renderMap();
if (state.selectedDeviceId && deviceIds.includes(state.selectedDeviceId)) {
updateInspector();
}
} catch (err) {
console.error('Assign failed:', err);
alert('Failed to assign device: ' + err.message);
}
}
// Unassign Device
async function unassignDevice(deviceId) {
await assignDevices([deviceId], null);
}
// Update Inspector Card
function updateInspector() {
const panel = document.getElementById('device-inspector');
if (!state.selectedDeviceId) {
panel.style.display = 'none';
return;
}
const d = state.devices.find(x => x.id === state.selectedDeviceId);
if (!d) {
panel.style.display = 'none';
return;
}
panel.style.display = 'block';
document.getElementById('insp-dev-id').textContent = d.id;
document.getElementById('insp-dev-type').textContent = d.type.toUpperCase();
document.getElementById('insp-dev-room').textContent = d.room || 'General';
// Fill Breaker Dropdown
const select = document.getElementById('insp-breaker-select');
select.innerHTML = '<option value="">-- Unassigned --</option>';
state.breakers.forEach(b => {
const opt = document.createElement('option');
opt.value = b.id;
opt.textContent = `[${b.label}] ${b.description || ''} (${b.amps}A)`;
if (d.breaker_id === b.id) opt.selected = true;
select.appendChild(opt);
});
document.getElementById('insp-dev-notes').value = d.notes || '';
document.getElementById('insp-room-btn').textContent = `Assign all in ${d.room || 'this room'}`;
}
// Save Device Notes
async function saveDeviceNotes() {
if (!state.selectedDeviceId) return;
const notes = document.getElementById('insp-dev-notes').value;
try {
await fetch(`/api/devices/${state.selectedDeviceId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes })
});
const d = state.devices.find(x => x.id === state.selectedDeviceId);
if (d) d.notes = notes;
} catch (err) {
console.error('Failed to save notes:', err);
}
}
// Change Breaker via Inspector
async function onInspectorBreakerChange() {
if (!state.selectedDeviceId) return;
const breakerId = document.getElementById('insp-breaker-select').value || null;
await assignDevices([state.selectedDeviceId], breakerId);
}
// Assign All in Room
async function assignAllInCurrentRoom() {
if (!state.selectedDeviceId) return;
const d = state.devices.find(x => x.id === state.selectedDeviceId);
if (!d || !d.room) return;
const breakerId = document.getElementById('insp-breaker-select').value || null;
const roomDevices = state.devices.filter(x => x.room === d.room).map(x => x.id);
if (confirm(`Assign all ${roomDevices.length} devices in "${d.room}" to this circuit?`)) {
await assignDevices(roomDevices, breakerId);
}
}
// Setup Pan and Zoom
function setupPanZoom() {
const container = document.getElementById('canvas-container');
const viewportG = document.getElementById('viewport-g');
function updateTransform() {
viewportG.setAttribute('transform', `translate(${state.pan.x}, ${state.pan.y}) scale(${state.zoom})`);
}
updateTransform();
container.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85;
const newZoom = Math.min(Math.max(state.zoom * zoomFactor, 0.05), 3.0);
// Zoom towards mouse position
const rect = container.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
state.pan.x = mouseX - (mouseX - state.pan.x) * (newZoom / state.zoom);
state.pan.y = mouseY - (mouseY - state.pan.y) * (newZoom / state.zoom);
state.zoom = newZoom;
updateTransform();
});
container.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Only left click drag
state.isDragging = true;
state.dragStart = { x: e.clientX - state.pan.x, y: e.clientY - state.pan.y };
});
window.addEventListener('mousemove', (e) => {
if (!state.isDragging) return;
state.pan.x = e.clientX - state.dragStart.x;
state.pan.y = e.clientY - state.dragStart.y;
updateTransform();
});
window.addEventListener('mouseup', () => {
state.isDragging = false;
});
// Zoom control buttons
document.getElementById('btn-zoom-in').addEventListener('click', () => {
state.zoom = Math.min(state.zoom * 1.25, 3.0);
updateTransform();
});
document.getElementById('btn-zoom-out').addEventListener('click', () => {
state.zoom = Math.max(state.zoom * 0.8, 0.05);
updateTransform();
});
document.getElementById('btn-zoom-fit').addEventListener('click', () => {
state.zoom = 0.22;
state.pan = { x: 40, y: 40 };
updateTransform();
});
}
// Setup Toolbar Controls
function setupToolbar() {
// Orientation toggle
const btnOrient = document.getElementById('btn-orientation');
btnOrient.addEventListener('click', () => {
state.orientation = state.orientation === 'landscape' ? 'native' : 'landscape';
btnOrient.innerHTML = state.orientation === 'landscape'
? '<i class="fa-solid fa-file-lines"></i> View: Field Doc (Landscape)'
: '<i class="fa-solid fa-compass-drafting"></i> View: Draw.io (Native)';
renderMap();
});
// Layer Toggles
setupLayerToggle('toggle-lights', 'lights');
setupLayerToggle('toggle-outlets', 'outlets');
setupLayerToggle('toggle-switches', 'switches');
setupLayerToggle('toggle-lines', 'lines');
setupLayerToggle('toggle-halos', 'unmappedHalos');
// Exit Painter Button
document.getElementById('btn-exit-painter').addEventListener('click', () => {
state.activeBreakerId = null;
updatePainterBadgeUI();
renderBreakersList();
renderMap();
});
// Export buttons
document.getElementById('btn-export-drawio').addEventListener('click', () => {
window.location.href = '/api/export/drawio';
});
document.getElementById('btn-export-csv').addEventListener('click', () => {
window.location.href = '/api/export/csv';
});
document.getElementById('btn-export-json').addEventListener('click', () => {
window.location.href = '/api/export/json';
});
// Reset button
document.getElementById('btn-reset').addEventListener('click', async () => {
if (confirm('Are you sure you want to reset all mappings back to the initial field seed state?')) {
const res = await fetch('/api/reset', { method: 'POST' });
const json = await res.json();
if (json.success) {
await loadState();
}
}
});
}
function setupLayerToggle(elementId, layerKey) {
const el = document.getElementById(elementId);
if (!el) return;
el.addEventListener('click', () => {
state.layers[layerKey] = !state.layers[layerKey];
el.classList.toggle('active', state.layers[layerKey]);
renderMap();
});
}
// Setup Sidebar Tabs (Subpanel vs Main Panel)
function setupSidebarTabs() {
const tabs = document.querySelectorAll('.sidebar-tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
state.activePanel = tab.dataset.panel;
document.getElementById('panel-card-title').textContent = state.activePanel === 'subpanel'
? 'GE PowerMark Gold Load Center (Subpanel)'
: 'Main Service Panel (Square D QO)';
document.getElementById('panel-card-subtitle').textContent = state.activePanel === 'subpanel'
? '125A 120/240V • Half-inch THQP Slim Tandem Compatible'
: '100A/200A 120/240V • Service Entrance & Disconnect';
renderBreakersList();
});
});
}
// Prompt to edit breaker metadata
async function editBreakerPrompt(breakerId) {
const b = state.breakers.find(x => x.id === breakerId);
if (!b) return;
const newDesc = prompt(`Edit Circuit Description for Slot [${b.label}]:`, b.description || '');
if (newDesc === null) return;
const newAmps = prompt(`Breaker Amperage (15, 20, 30, 40, 50, etc.):`, b.amps);
if (newAmps === null) return;
try {
const res = await fetch(`/api/breakers/${b.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: newDesc,
amps: parseInt(newAmps, 10) || b.amps
})
});
const json = await res.json();
if (json.success) {
b.description = newDesc;
b.amps = parseInt(newAmps, 10) || b.amps;
renderBreakersList();
renderMap();
}
} catch (err) {
alert('Failed to update breaker: ' + err.message);
}
}