feat: initial release of interactive electrical circuit & panel mapper

This commit is contained in:
wmantly committed 2026-10-20 15:51:46 -04:00
commit 3ed8193565
17 files changed
+7222

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
'use strict';
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const xml2js = require('xml2js');
const db = require('../models/db');
const { seedDatabase } = require('../lib/seed');
const conf = require('../conf/base');
// GET /api/state - Full application state
router.get('/state', (req, res) => {
try {
const panels = db.prepare('SELECT * FROM panels').all();
const breakers = db.prepare(`
SELECT b.*,
(SELECT COUNT(*) FROM devices d WHERE d.breaker_id = b.id) as device_count
FROM breakers b
ORDER BY b.panel_id DESC, b.slot ASC, b.sub_slot ASC
`).all();
const devices = db.prepare(`
SELECT d.*, b.label as breaker_label, b.color as breaker_color, b.description as breaker_desc
FROM devices d
LEFT JOIN breakers b ON d.breaker_id = b.id
`).all();
const structures = db.prepare('SELECT * FROM structures').all();
const edges = db.prepare('SELECT * FROM edges').all();
const totalDevices = devices.length;
const mappedDevices = devices.filter(d => d.breaker_id !== null).length;
const unmappedDevices = totalDevices - mappedDevices;
const progressPercent = totalDevices > 0 ? Math.round((mappedDevices / totalDevices) * 100) : 0;
res.json({
success: true,
data: {
panels,
breakers,
devices,
structures,
edges,
stats: {
totalDevices,
mappedDevices,
unmappedDevices,
progressPercent
}
}
});
} catch (err) {
console.error('API /state error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// POST /api/devices/assign - Assign one or more devices to a breaker
router.post('/devices/assign', (req, res) => {
try {
const { deviceIds, breakerId } = req.body;
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return res.status(400).json({ success: false, error: 'deviceIds array is required' });
}
if (breakerId !== null) {
const breaker = db.prepare('SELECT id FROM breakers WHERE id = ?').get(breakerId);
if (!breaker) {
return res.status(404).json({ success: false, error: `Breaker ${breakerId} not found` });
}
}
const updateStmt = db.prepare(`
UPDATE devices
SET breaker_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
const tx = db.transaction(() => {
for (const id of deviceIds) {
updateStmt.run(breakerId, id);
}
});
tx();
res.json({ success: true, updated: deviceIds.length, breakerId });
} catch (err) {
console.error('API /devices/assign error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// POST /api/devices/bulk-room - Assign all devices in a room
router.post('/devices/bulk-room', (req, res) => {
try {
const { room, breakerId } = req.body;
if (!room) {
return res.status(400).json({ success: false, error: 'room is required' });
}
const updateStmt = db.prepare(`
UPDATE devices
SET breaker_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE room = ?
`);
const result = updateStmt.run(breakerId, room);
res.json({ success: true, updated: result.changes, room, breakerId });
} catch (err) {
console.error('API /devices/bulk-room error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// POST /api/devices/:id - Update individual device attributes
router.post('/devices/:id', (req, res) => {
try {
const { id } = req.params;
const { label, notes, type, breaker_id } = req.body;
const current = db.prepare('SELECT * FROM devices WHERE id = ?').get(id);
if (!current) {
return res.status(404).json({ success: false, error: 'Device not found' });
}
const newLabel = label !== undefined ? label : current.label;
const newNotes = notes !== undefined ? notes : current.notes;
const newType = type !== undefined ? type : current.type;
const newBreakerId = breaker_id !== undefined ? breaker_id : current.breaker_id;
db.prepare(`
UPDATE devices
SET label = ?, notes = ?, type = ?, breaker_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(newLabel, newNotes, newType, newBreakerId, id);
res.json({ success: true, device: { id, label: newLabel, notes: newNotes, type: newType, breaker_id: newBreakerId } });
} catch (err) {
console.error('API /devices/:id error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// POST /api/breakers/:id - Update breaker metadata
router.post('/breakers/:id', (req, res) => {
try {
const { id } = req.params;
const { description, amps, voltage, color, notes } = req.body;
const current = db.prepare('SELECT * FROM breakers WHERE id = ?').get(id);
if (!current) {
return res.status(404).json({ success: false, error: 'Breaker not found' });
}
const newDesc = description !== undefined ? description : current.description;
const newAmps = amps !== undefined ? parseInt(amps, 10) : current.amps;
const newVolt = voltage !== undefined ? parseInt(voltage, 10) : current.voltage;
const newColor = color !== undefined ? color : current.color;
const newNotes = notes !== undefined ? notes : current.notes;
db.prepare(`
UPDATE breakers
SET description = ?, amps = ?, voltage = ?, color = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(newDesc, newAmps, newVolt, newColor, newNotes, id);
res.json({ success: true, breaker: { id, description: newDesc, amps: newAmps, voltage: newVolt, color: newColor, notes: newNotes } });
} catch (err) {
console.error('API /breakers/:id error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// POST /api/reset - Reset to seed data
router.post('/reset', (req, res) => {
try {
seedDatabase(true);
res.json({ success: true, message: 'Database reset to initial seed state.' });
} catch (err) {
console.error('API /reset error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// GET /api/export/json - Export entire database state
router.get('/export/json', (req, res) => {
try {
const panels = db.prepare('SELECT * FROM panels').all();
const breakers = db.prepare('SELECT * FROM breakers').all();
const devices = db.prepare('SELECT * FROM devices').all();
const structures = db.prepare('SELECT * FROM structures').all();
const edges = db.prepare('SELECT * FROM edges').all();
const exportPayload = {
timestamp: new Date().toISOString(),
panels,
breakers,
devices,
structures,
edges
};
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', 'attachment; filename="electrical_chart_export.json"');
res.send(JSON.stringify(exportPayload, null, 2));
} catch (err) {
console.error('API /export/json error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// GET /api/export/csv - Export schedule CSV
router.get('/export/csv', (req, res) => {
try {
const devices = db.prepare(`
SELECT
d.id as device_id,
d.type,
d.room,
d.label as device_label,
d.notes as device_notes,
b.panel_id,
b.label as breaker_label,
b.description as circuit_description,
b.amps,
b.voltage
FROM devices d
LEFT JOIN breakers b ON d.breaker_id = b.id
ORDER BY b.panel_id, b.slot, b.sub_slot, d.room
`).all();
const headers = ['Device ID', 'Type', 'Room', 'Device Label', 'Panel', 'Breaker Slot', 'Circuit Description', 'Amps', 'Voltage', 'Notes'];
const rows = devices.map(d => [
`"${d.device_id || ''}"`,
`"${d.type || ''}"`,
`"${d.room || ''}"`,
`"${(d.device_label || '').replace(/"/g, '""')}"`,
`"${d.panel_id || 'UNMAPPED'}"`,
`"${d.breaker_label || 'UNMAPPED'}"`,
`"${(d.circuit_description || '').replace(/"/g, '""')}"`,
`"${d.amps || ''}"`,
`"${d.voltage || ''}"`,
`"${(d.device_notes || '').replace(/"/g, '""')}"`
]);
const csvContent = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="electrical_panel_schedule.csv"');
res.send(csvContent);
} catch (err) {
console.error('API /export/csv error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// GET /api/export/drawio - Export updated draw.io XML with breaker badges
router.get('/export/drawio', (req, res) => {
try {
const xmlPath = conf.drawioSourcePath;
let rawXml = fs.readFileSync(xmlPath, 'utf8');
const devices = db.prepare(`
SELECT d.*, b.label as breaker_label, b.color as breaker_color
FROM devices d
LEFT JOIN breakers b ON d.breaker_id = b.id
WHERE d.breaker_id IS NOT NULL
`).all();
// In rawXml, update matching mxCell values or add labels
for (const d of devices) {
// Find <mxCell id="d.drawio_id"
const cellPattern = new RegExp(`(<mxCell id="${d.drawio_id}"[^>]*value=")([^"]*)(")`, 'g');
if (cellPattern.test(rawXml)) {
rawXml = rawXml.replace(cellPattern, `$1[${d.breaker_label}]$3`);
}
}
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Content-Disposition', 'attachment; filename="mapped_electrical_diagram.drawio"');
res.send(rawXml);
} catch (err) {
console.error('API /export/drawio error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
module.exports = router;
+14
View File
@@ -0,0 +1,14 @@
'use strict';
const express = require('express');
const router = express.Router();
const conf = require('../conf/base');
router.get('/', (req, res) => {
res.render('index', {
appName: conf.appName,
environment: conf.environment
});
});
module.exports = router;