feat: initial release of interactive electrical circuit & panel mapper
This commit is contained in:
commit
3ed8193565
17 files changed
+7222
No files matched your search
+289
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user