From 3ed819356564d739eb549eeb1402a4d5ae095d39 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 20 Oct 2026 15:51:46 -0400 Subject: [PATCH] feat: initial release of interactive electrical circuit & panel mapper --- .gitignore | 5 + README.md | 85 + app.js | 41 + bin/www | 45 + conf/base.js | 11 + data/electrical_chart.sqlite | Bin 0 -> 94208 bytes diagram.drawio | 649 ++++++++ lib/seed.js | 151 ++ models/db.js | 100 ++ package-lock.json | 1370 ++++++++++++++++ package.json | 23 + public/css/style.css | 528 ++++++ public/js/app.js | 758 +++++++++ routes/api.js | 289 ++++ routes/render.js | 14 + seeds_raw.json | 2987 ++++++++++++++++++++++++++++++++++ views/index.ejs | 166 ++ 17 files changed, 7222 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100755 bin/www create mode 100644 conf/base.js create mode 100644 data/electrical_chart.sqlite create mode 100644 diagram.drawio create mode 100644 lib/seed.js create mode 100644 models/db.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/css/style.css create mode 100644 public/js/app.js create mode 100644 routes/api.js create mode 100644 routes/render.js create mode 100644 seeds_raw.json create mode 100644 views/index.ejs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6934fc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +*.log +.DS_Store +data/*.sqlite-wal +data/*.sqlite-shm diff --git a/README.md b/README.md new file mode 100644 index 0000000..76d6270 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Electrical Circuit & Panel Mapper + +Interactive, full-stack electrical circuit and panel mapping application for tracing branch circuits, documenting load centers, and mapping electrical devices (lighting fixtures, wall receptacles, switches, and dedicated appliances) across architectural floorplans. + +Repository: [https://git.theta42.com/wmantly/electrical-chart](https://git.theta42.com/wmantly/electrical-chart) + +--- + +## ⚡ Overview & Features + +- **Interactive Architectural Schematic:** + - Renders all 141 electrical devices (48 lights, 49 receptacles, 44 switches) and room structures derived from `diagram.drawio`. + - Supports dual orientation modes: **Field Doc Landscape** (matching physical handheld floorplan printout) and **Draw.io Native**. + - Smooth pan and zoom navigation (mouse wheel + click-and-drag). +- **Physical Panel Scheduling:** + - **GE PowerMark Gold Load Center (Subpanel):** 12 slots with 1/2-inch THQP slim tandem breakers (`6A`, `6B`, `7A`, `7B`, `8A`, `8B`, `9A`, `9B`, `11A`, `11B`, 2-pole 240V heaters). + - **Main Service Panel (Square D QO):** Main disconnect, subpanel feeder, 240V range/HVAC/dryer, utility branch circuits. +- **Painter Mode (Rapid Tagging):** + - Click any breaker card in the sidebar to activate Painter Mode. + - Click any device on the floor plan to instantly assign it to that breaker with zero dialogs. + - Right-click any device to unassign it. +- **Room Bulk Actions & Lasso Selection:** + - Assign entire rooms of receptacles or lighting in a single click via the Device Inspector. +- **Visual Circuit Tracing:** + - Color-coded badges per circuit (`6A`, `7B`, `7A`, etc.). + - Dashed circuit run lines dynamically link all devices sharing the same breaker. + - Unmapped warning halos highlight every unverified fixture that still needs testing. +- **Exports:** + - Export back to updated **Draw.io XML** with embedded circuit tags. + - Export **Panel Schedule CSV** for printing panel door labels. + - Export full **JSON state dump**. + +--- + +## 🚀 Quickstart + +### Prerequisites +- Node.js v18+ (tested on Node v22) +- npm + +### Installation & Run + +```bash +git clone ssh://gitea@git.theta42.com:2222/wmantly/electrical-chart.git +cd electrical-chart +npm install +npm start +``` + +Open [http://localhost:3456](http://localhost:3456) in your browser. + +To run on a different port: +```bash +PORT=3000 npm start +``` + +--- + +## 📁 Project Architecture + +``` +electrical-chart/ +├── app.js # Express application setup & middleware +├── bin/ +│ └── www # HTTP server launcher +├── conf/ +│ └── base.js # Configuration (port, database paths) +├── data/ +│ └── electrical_chart.sqlite # SQLite database (auto-created on startup) +├── diagram.drawio # Master Draw.io architectural source +├── lib/ +│ └── seed.js # Initial database seeder & circuit pre-mappings +├── models/ +│ └── db.js # better-sqlite3 connection & schema migrations +├── public/ +│ ├── css/ +│ │ └── style.css # Dark blueprint theme & responsive styling +│ └── js/ +│ └── app.js # Interactive SVG rendering, pan/zoom, painter mode +├── routes/ +│ ├── api.js # REST API (/api/state, /api/devices/assign, exports) +│ └── render.js # Main page render route +└── views/ + └── index.ejs # EJS single-page layout +``` diff --git a/app.js b/app.js new file mode 100644 index 0000000..59748c3 --- /dev/null +++ b/app.js @@ -0,0 +1,41 @@ +'use strict'; + +const path = require('path'); +const express = require('express'); +const conf = require('./conf/base'); +const { seedDatabase } = require('./lib/seed'); + +// Initialize database with initial seed if empty +seedDatabase(); + +const app = express(); + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Static files +app.use('/static', express.static(path.join(__dirname, 'public'))); + +// View engine +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// Routes +app.use('/', require('./routes/render')); +app.use('/api', require('./routes/api')); + +// 404 handler +app.use((req, res, next) => { + res.status(404).json({ error: 'Endpoint Not Found' }); +}); + +// Error handler +app.use((err, req, res, next) => { + console.error('Server error:', err); + res.status(err.status || 500).json({ + error: err.message || 'Internal Server Error' + }); +}); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..a5fa99d --- /dev/null +++ b/bin/www @@ -0,0 +1,45 @@ +#!/usr/bin/env node +'use strict'; + +const http = require('http'); +const app = require('../app'); +const conf = require('../conf/base'); + +const port = normalizePort(process.env.PORT || conf.port || '3456'); +app.set('port', port); + +const server = http.createServer(app); + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +function normalizePort(val) { + const port = parseInt(val, 10); + if (isNaN(port)) return val; + if (port >= 0) return port; + return false; +} + +function onError(error) { + if (error.syscall !== 'listen') throw error; + const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +function onListening() { + const addr = server.address(); + const bind = typeof addr === 'string' ? 'pipe ' + addr : 'http://localhost:' + addr.port; + console.log(`⚡ Electrical Chart & Mapping Server listening at ${bind}`); +} diff --git a/conf/base.js b/conf/base.js new file mode 100644 index 0000000..9f829b5 --- /dev/null +++ b/conf/base.js @@ -0,0 +1,11 @@ +'use strict'; + +const path = require('path'); + +module.exports = { + port: process.env.PORT || 3456, + environment: process.env.NODE_ENV || 'development', + appName: 'Electrical Circuit & Panel Mapper', + dbPath: process.env.DB_PATH || path.join(__dirname, '../data/electrical_chart.sqlite'), + drawioSourcePath: path.join(__dirname, '../diagram.drawio') +}; diff --git a/data/electrical_chart.sqlite b/data/electrical_chart.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..26658b5dc31ea80b37ff0b950a33f35e0f934aa2 GIT binary patch literal 94208 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/seed.js b/lib/seed.js new file mode 100644 index 0000000..a091236 --- /dev/null +++ b/lib/seed.js @@ -0,0 +1,151 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const db = require('../models/db'); +const conf = require('../conf/base'); + +function seedDatabase(force = false) { + const deviceCount = db.prepare('SELECT COUNT(*) as count FROM devices').get().count; + if (deviceCount > 0 && !force) { + console.log(`Database already contains ${deviceCount} devices. Skipping seed.`); + return; + } + + console.log('Seeding electrical chart database...'); + + const rawSeedPath = path.join(__dirname, '../seeds_raw.json'); + if (!fs.existsSync(rawSeedPath)) { + throw new Error('seeds_raw.json not found!'); + } + const rawData = JSON.parse(fs.readFileSync(rawSeedPath, 'utf8')); + + const tx = db.transaction(() => { + // Clear existing + db.prepare('DELETE FROM devices').run(); + db.prepare('DELETE FROM breakers').run(); + db.prepare('DELETE FROM panels').run(); + db.prepare('DELETE FROM structures').run(); + db.prepare('DELETE FROM edges').run(); + db.prepare('DELETE FROM rooms').run(); + + // 1. Panels + const insertPanel = db.prepare(` + INSERT INTO panels (id, name, location, model, bus_rating, notes) + VALUES (?, ?, ?, ?, ?, ?) + `); + insertPanel.run('subpanel', 'GE PowerMark Gold Load Center', 'Utility / Hallway', 'TM1210CCUG / TM1212CCU', '125A 120/240V', 'Feeds West Wing & Rec Room branch circuits. Uses THQP 1/2-inch slim tandem breakers.'); + insertPanel.run('main', 'Main Service Panel (Square D QO)', 'Service Entrance / Utility', 'QO 2-Tier Split Enclosure', '100A / 200A 120/240V', 'Main disconnect in lower compartment, branch distribution in upper compartment with transformer adjacent.'); + + // 2. Breakers + const breakersList = [ + // Subpanel Odd (Left side) + { id: 'sub-1', panel_id: 'subpanel', slot: 1, sub_slot: null, label: '1', description: 'Bed-Bath HEAT (240V Pole A)', amps: 50, poles: 2, voltage: 240, color: '#e03131', notes: 'Pairs with slot 12' }, + { id: 'sub-3', panel_id: 'subpanel', slot: 3, sub_slot: null, label: '3', description: 'Hall & Vermont', amps: 20, poles: 1, voltage: 120, color: '#f59f00', notes: 'Hallway lighting & convenience outlets' }, + { id: 'sub-5', panel_id: 'subpanel', slot: 5, sub_slot: null, label: '5', description: 'Loft Outlet', amps: 20, poles: 1, voltage: 120, color: '#37b24d', notes: 'Upper loft receptacles' }, + { id: 'sub-7A', panel_id: 'subpanel', slot: 7, sub_slot: 'A', label: '7A', description: 'Rec Room Light', amps: 15, poles: 1, voltage: 120, color: '#1098ad', notes: 'Central luminaire & key switch runs' }, + { id: 'sub-7B', panel_id: 'subpanel', slot: 7, sub_slot: 'B', label: '7B', description: 'Rec Room Outlets', amps: 20, poles: 1, voltage: 120, color: '#1c7ed6', notes: 'North & west perimeter receptacles' }, + { id: 'sub-9A', panel_id: 'subpanel', slot: 9, sub_slot: 'A', label: '9A', description: 'Rec Room Main Circuit', amps: 15, poles: 1, voltage: 120, color: '#4263eb', notes: 'THQP slim tandem pole' }, + { id: 'sub-9B', panel_id: 'subpanel', slot: 9, sub_slot: 'B', label: '9B', description: 'Rec Room Aux', amps: 15, poles: 1, voltage: 120, color: '#7048e8', notes: 'THQP slim tandem pole' }, + { id: 'sub-11A', panel_id: 'subpanel', slot: 11, sub_slot: 'A', label: '11A', description: 'Rec Room Light Primary', amps: 15, poles: 1, voltage: 120, color: '#ae3ec9', notes: 'Lighting circuit' }, + { id: 'sub-11B', panel_id: 'subpanel', slot: 11, sub_slot: 'B', label: '11B', description: 'Rec Room Light Aux', amps: 15, poles: 1, voltage: 120, color: '#d6336c', notes: 'Lighting circuit' }, + + // Subpanel Even (Right side) + { id: 'sub-2', panel_id: 'subpanel', slot: 2, sub_slot: null, label: '2', description: 'Water Heater (240V Pole A)', amps: 30, poles: 2, voltage: 240, color: '#f76707', notes: 'Tied 2-pole breaker with slot 4' }, + { id: 'sub-4', panel_id: 'subpanel', slot: 4, sub_slot: null, label: '4', description: 'Water Heater (240V Pole B)', amps: 30, poles: 2, voltage: 240, color: '#f76707', notes: 'Tied 2-pole breaker with slot 2' }, + { id: 'sub-6A', panel_id: 'subpanel', slot: 6, sub_slot: 'A', label: '6A', description: 'Rec Room Lite & Short Wall', amps: 20, poles: 1, voltage: 120, color: '#0ca678', notes: 'Traced across central rooms, pool, and east wing' }, + { id: 'sub-6B', panel_id: 'subpanel', slot: 6, sub_slot: 'B', label: '6B', description: 'Rec Room Tandem Aux', amps: 20, poles: 1, voltage: 120, color: '#66a80f', notes: 'THQP slim tandem' }, + { id: 'sub-8A', panel_id: 'subpanel', slot: 8, sub_slot: 'A', label: '8A', description: 'Open / Spare A', amps: 20, poles: 1, voltage: 120, color: '#868e96', notes: 'Physical empty slot' }, + { id: 'sub-8B', panel_id: 'subpanel', slot: 8, sub_slot: 'B', label: '8B', description: 'Open / Spare B', amps: 20, poles: 1, voltage: 120, color: '#adb5bd', notes: 'Physical empty slot' }, + { id: 'sub-10', panel_id: 'subpanel', slot: 10, sub_slot: null, label: '10', description: 'Spare / Available', amps: 20, poles: 1, voltage: 120, color: '#495057', notes: '' }, + { id: 'sub-12', panel_id: 'subpanel', slot: 12, sub_slot: null, label: '12', description: 'Bed-Bath HEAT (240V Pole B)', amps: 50, poles: 2, voltage: 240, color: '#e03131', notes: 'Pairs with slot 1' }, + + // Main Panel Breakers + { id: 'main-disc', panel_id: 'main', slot: 1, sub_slot: null, label: 'MAIN', description: 'Main Service Disconnect', amps: 100, poles: 2, voltage: 240, color: '#c92a2a', notes: 'Service entrance main breaker' }, + { id: 'main-sub', panel_id: 'main', slot: 2, sub_slot: null, label: 'SUB-FEED', description: 'Feeder to GE Subpanel', amps: 60, poles: 2, voltage: 240, color: '#1864ab', notes: 'Heavy gauge feeder to load center' }, + { id: 'main-range', panel_id: 'main', slot: 3, sub_slot: null, label: 'RANGE', description: 'Electric Range / Kitchen Cooktop', amps: 50, poles: 2, voltage: 240, color: '#5f3dc4', notes: '50A 240V branch' }, + { id: 'main-dryer', panel_id: 'main', slot: 4, sub_slot: null, label: 'DRYER', description: 'Electric Clothes Dryer', amps: 30, poles: 2, voltage: 240, color: '#a61e4d', notes: '30A 240V branch' }, + { id: 'main-hvac', panel_id: 'main', slot: 5, sub_slot: null, label: 'HVAC', description: 'Air Handler & Heat Pump', amps: 40, poles: 2, voltage: 240, color: '#0b7285', notes: '40A 240V branch' }, + { id: 'main-util', panel_id: 'main', slot: 6, sub_slot: null, label: 'UTIL-1', description: 'Utility Room & Chime Transformer', amps: 15, poles: 1, voltage: 120, color: '#2b8a3e', notes: 'Feeds transformer and basement utility lights' } + ]; + + const insertBreaker = db.prepare(` + INSERT INTO breakers (id, panel_id, slot, sub_slot, label, description, amps, poles, voltage, color, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const b of breakersList) { + insertBreaker.run(b.id, b.panel_id, b.slot, b.sub_slot, b.label, b.description, b.amps, b.poles, b.voltage, b.color, b.notes); + } + + // 3. Structures + const insertStruct = db.prepare(` + INSERT INTO structures (id, drawio_id, type, x, y, w, h, label, style) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const s of rawData.structures) { + insertStruct.run(s.id, s.drawio_id, s.type, s.x, s.y, s.w, s.h, s.label, s.style); + } + + // 4. Edges + const insertEdge = db.prepare(` + INSERT INTO edges (id, drawio_id, source, target, style) + VALUES (?, ?, ?, ?, ?) + `); + for (const e of rawData.edges) { + insertEdge.run(e.id, e.drawio_id, e.source, e.target, e.style); + } + + // 5. Pre-map identified devices from field notes + // 7B: Outlets on north wall of bedroom 1 & 2, and west wall + const map7B = ['dev-89', 'dev-90', 'dev-87']; + // 7A: Central luminaire + const map7A = ['dev-63']; + // 6A: Central devices, pool area, and east wing runs annotated with 6A + const map6A = ['dev-34', 'dev-35', 'dev-58', 'dev-80', 'dev-92', 'dev-184', 'dev-61', 'dev-62']; + + // 6. Devices + const insertDevice = db.prepare(` + INSERT INTO devices (id, drawio_id, type, orig_x, orig_y, orig_w, orig_h, cx, cy, room, zone, breaker_id, label, notes, color) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + for (const d of rawData.devices) { + let assignedBreaker = null; + let notes = ''; + + if (map7B.includes(d.id)) { + assignedBreaker = 'sub-7B'; + notes = 'Verified on circuit 7B from field tracing'; + } else if (map7A.includes(d.id)) { + assignedBreaker = 'sub-7A'; + notes = 'Verified central luminaire on circuit 7A'; + } else if (map6A.includes(d.id)) { + assignedBreaker = 'sub-6A'; + notes = 'Verified on circuit 6A from field tracing'; + } + + insertDevice.run( + d.id, + d.drawio_id, + d.type, + d.x, + d.y, + d.w, + d.h, + d.cx, + d.cy, + d.room, + d.zone, + assignedBreaker, + d.label, + notes, + d.color + ); + } + }); + + tx(); + console.log('Database seeded successfully.'); +} + +module.exports = { seedDatabase }; diff --git a/models/db.js b/models/db.js new file mode 100644 index 0000000..84a2d87 --- /dev/null +++ b/models/db.js @@ -0,0 +1,100 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Database = require('better-sqlite3'); +const conf = require('../conf/base'); + +// Ensure data directory exists +const dbDir = path.dirname(conf.dbPath); +if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); +} + +const db = new Database(conf.dbPath); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// Initialize schema +db.exec(` + CREATE TABLE IF NOT EXISTS panels ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + location TEXT, + model TEXT, + bus_rating TEXT, + notes TEXT + ); + + CREATE TABLE IF NOT EXISTS breakers ( + id TEXT PRIMARY KEY, + panel_id TEXT NOT NULL, + slot INTEGER NOT NULL, + sub_slot TEXT, + label TEXT NOT NULL, + description TEXT, + amps INTEGER DEFAULT 20, + poles INTEGER DEFAULT 1, + voltage INTEGER DEFAULT 120, + color TEXT NOT NULL, + notes TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(panel_id) REFERENCES panels(id) + ); + + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + zone TEXT NOT NULL, + x REAL, + y REAL, + w REAL, + h REAL + ); + + CREATE TABLE IF NOT EXISTS devices ( + id TEXT PRIMARY KEY, + drawio_id TEXT NOT NULL, + type TEXT NOT NULL, + orig_x REAL NOT NULL, + orig_y REAL NOT NULL, + orig_w REAL NOT NULL, + orig_h REAL NOT NULL, + cx REAL NOT NULL, + cy REAL NOT NULL, + room TEXT, + zone TEXT, + breaker_id TEXT, + label TEXT, + notes TEXT, + color TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(breaker_id) REFERENCES breakers(id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS structures ( + id TEXT PRIMARY KEY, + drawio_id TEXT NOT NULL, + type TEXT NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL, + w REAL NOT NULL, + h REAL NOT NULL, + label TEXT, + style TEXT + ); + + CREATE TABLE IF NOT EXISTS edges ( + id TEXT PRIMARY KEY, + drawio_id TEXT NOT NULL, + source TEXT, + target TEXT, + style TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_devices_breaker ON devices(breaker_id); + CREATE INDEX IF NOT EXISTS idx_devices_room ON devices(room); + CREATE INDEX IF NOT EXISTS idx_breakers_panel ON breakers(panel_id); +`); + +module.exports = db; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..286d653 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1370 @@ +{ + "name": "electrical-chart", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "electrical-chart", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^11.8.1", + "ejs": "^3.1.10", + "express": "^4.21.2", + "xml2js": "^0.6.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.99.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.99.0.tgz", + "integrity": "sha512-lLdwkeg2XUVPTHqWjPltTxH81zbQNbUWC+AEb9ni50KjxkUzAAx1HGtMT21sbntzfUGfIphhcklvs3imJeqDXw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f25c4f --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "electrical-chart", + "version": "1.0.0", + "description": "Interactive Electrical Circuit & Panel Mapping Tool", + "main": "app.js", + "scripts": { + "start": "node ./bin/www", + "dev": "node --watch ./bin/www", + "seed": "node -e \"require('./lib/seed').seedDatabase(true)\"" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@git.theta42.com:2222/wmantly/electrical-chart.git" + }, + "author": "William Mantly", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^11.8.1", + "ejs": "^3.1.10", + "express": "^4.21.2", + "xml2js": "^0.6.2" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..18757e3 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,528 @@ +:root { + --bg-dark: #12161f; + --bg-panel: #1a202c; + --bg-card: #222b3c; + --bg-card-hover: #2b364c; + --border-color: #2e3a50; + --text-main: #e2e8f0; + --text-muted: #94a3b8; + --accent: #3b82f6; + --accent-hover: #2563eb; + --unmapped-pulse: #f59f00; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: var(--bg-dark); + color: var(--text-main); + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* Header */ +.app-header { + height: 56px; + background-color: var(--bg-panel); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + z-index: 100; + flex-shrink: 0; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + font-size: 1.1rem; + letter-spacing: -0.5px; +} + +.brand-icon { + color: #fab005; + font-size: 1.3rem; +} + +.progress-container { + display: flex; + align-items: center; + gap: 12px; + background: var(--bg-card); + padding: 4px 12px; + border-radius: 20px; + border: 1px solid var(--border-color); +} + +.progress-bar-wrap { + width: 140px; + height: 8px; + background: #334155; + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #10b981, #3b82f6); + width: 0%; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 0.8rem; + color: var(--text-muted); + font-weight: 600; +} + +.header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +/* App Main Body */ +.app-body { + flex: 1; + display: flex; + overflow: hidden; + position: relative; +} + +/* Left Canvas Area */ +.canvas-panel { + flex: 1; + position: relative; + background: #0f131a; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.canvas-toolbar { + position: absolute; + top: 14px; + left: 14px; + z-index: 20; + display: flex; + flex-wrap: wrap; + gap: 8px; + background: rgba(26, 32, 44, 0.92); + backdrop-filter: blur(8px); + padding: 6px 10px; + border-radius: 8px; + border: 1px solid var(--border-color); + box-shadow: 0 4px 16px rgba(0,0,0,0.3); +} + +.active-painter-badge { + display: none; + align-items: center; + gap: 8px; + padding: 4px 10px; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; + color: #fff; + background: #1c7ed6; + border: 1px solid rgba(255,255,255,0.2); + box-shadow: 0 0 12px rgba(28, 126, 214, 0.5); + animation: pulse-border 2s infinite; +} + +@keyframes pulse-border { + 0% { box-shadow: 0 0 0 0 rgba(28, 126, 214, 0.6); } + 70% { box-shadow: 0 0 0 8px rgba(28, 126, 214, 0); } + 100% { box-shadow: 0 0 0 0 rgba(28, 126, 214, 0); } +} + +.active-painter-badge.active { + display: flex; +} + +.svg-viewport { + width: 100%; + height: 100%; + cursor: grab; +} + +.svg-viewport:active { + cursor: grabbing; +} + +.svg-viewport.painter-cursor { + cursor: crosshair; +} + +/* Right Sidebar */ +.sidebar { + width: 440px; + background-color: var(--bg-panel); + border-left: 1px solid var(--border-color); + display: flex; + flex-direction: column; + overflow: hidden; + flex-shrink: 0; + z-index: 30; +} + +.sidebar-tabs { + display: flex; + background: var(--bg-dark); + border-bottom: 1px solid var(--border-color); +} + +.sidebar-tab { + flex: 1; + padding: 10px 14px; + background: none; + border: none; + color: var(--text-muted); + font-weight: 600; + font-size: 0.88rem; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.sidebar-tab.active { + color: #fff; + border-bottom-color: var(--accent); + background: var(--bg-panel); +} + +.sidebar-content { + flex: 1; + overflow-y: auto; + padding: 14px; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Panel Header Card */ +.panel-info-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px 14px; +} + +.panel-title { + font-weight: 700; + font-size: 0.95rem; + display: flex; + align-items: center; + justify-content: space-between; +} + +.panel-subtitle { + font-size: 0.78rem; + color: var(--text-muted); + margin-top: 2px; +} + +/* Breakers Grid */ +.breakers-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.breaker-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 8px 10px; + cursor: pointer; + transition: all 0.15s ease; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 74px; + user-select: none; +} + +.breaker-card:hover { + background: var(--bg-card-hover); + border-color: #475569; + transform: translateY(-1px); +} + +.breaker-card.selected-painter { + border-color: #fff; + box-shadow: 0 0 12px rgba(255, 255, 255, 0.4); +} + +.breaker-card.highlighted-filter { + outline: 2px solid #38bdf8; +} + +.breaker-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.breaker-tag { + font-weight: 800; + font-size: 0.95rem; + padding: 1px 6px; + border-radius: 4px; + color: #fff; + letter-spacing: -0.3px; +} + +.breaker-meta { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.72rem; + font-weight: 600; + color: var(--text-muted); +} + +.breaker-desc { + font-size: 0.8rem; + font-weight: 500; + color: var(--text-main); + line-height: 1.25; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.breaker-footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 6px; + font-size: 0.72rem; +} + +.device-badge { + background: rgba(255, 255, 255, 0.1); + padding: 2px 6px; + border-radius: 10px; + font-weight: 600; +} + +/* Device Inspector Footer Card */ +.device-inspector-panel { + background: var(--bg-card); + border-top: 1px solid var(--border-color); + padding: 14px; + flex-shrink: 0; +} + +.inspector-title { + font-size: 0.88rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.inspector-field { + margin-bottom: 8px; +} + +.inspector-field label { + font-size: 0.75rem; + color: var(--text-muted); + display: block; + margin-bottom: 3px; + font-weight: 600; +} + +/* Buttons and Controls */ +.btn-tool { + background: var(--bg-card); + border: 1px solid var(--border-color); + color: var(--text-main); + padding: 5px 10px; + border-radius: 6px; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.15s; +} + +.btn-tool:hover { + background: var(--bg-card-hover); + color: #fff; + border-color: #475569; +} + +.btn-tool.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.btn-tool.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.btn-tool.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-tool.btn-danger { + background: #e03131; + border-color: #e03131; + color: #fff; +} + +.btn-tool.btn-danger:hover { + background: #c92a2a; +} + +.form-select, .form-input { + background: #0f131a; + border: 1px solid var(--border-color); + color: #fff; + padding: 6px 10px; + border-radius: 6px; + font-size: 0.82rem; + width: 100%; +} + +.form-select:focus, .form-input:focus { + outline: none; + border-color: var(--accent); +} + +/* SVG Stylings */ +.svg-room { + fill: #141a24; + stroke: #334155; + stroke-width: 2px; + transition: fill 0.2s, stroke 0.2s; +} + +.svg-room:hover { + fill: #1a2332; + stroke: #475569; +} + +.svg-room-label { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 32px; + font-weight: 700; + fill: #64748b; + text-anchor: middle; + pointer-events: none; + user-select: none; +} + +.svg-subpanel-boundary { + fill: rgba(59, 130, 246, 0.05); + stroke: #3b82f6; + stroke-width: 3px; + stroke-dasharray: 8 6; + pointer-events: none; +} + +.svg-subpanel-tag { + font-size: 44px; + font-weight: 800; + fill: #60a5fa; + pointer-events: none; + text-anchor: middle; +} + +/* Device node hover & selection */ +.svg-device { + cursor: pointer; + transition: transform 0.15s ease, filter 0.15s ease; +} + +.svg-device:hover { + filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.8)); +} + +.svg-device.selected { + filter: drop-shadow(0 0 14px #38bdf8); +} + +.svg-device.dimmed { + opacity: 0.15; +} + +.svg-unmapped-halo { + animation: pulse-ring 2s infinite; + pointer-events: none; +} + +@keyframes pulse-ring { + 0% { opacity: 0.8; transform: scale(1); } + 50% { opacity: 0.3; transform: scale(1.3); } + 100% { opacity: 0.8; transform: scale(1); } +} + +.svg-breaker-badge { + font-size: 20px; + font-weight: 800; + font-family: sans-serif; + fill: #ffffff; + text-anchor: middle; + dominant-baseline: central; + pointer-events: none; + user-select: none; +} + +.svg-circuit-line { + fill: none; + stroke-width: 4px; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 8 6; + pointer-events: none; +} + +/* Selection Box */ +.selection-box { + position: absolute; + border: 1px dashed #38bdf8; + background-color: rgba(56, 189, 248, 0.15); + pointer-events: none; + z-index: 1000; +} + +/* Tooltip */ +.device-tooltip { + position: absolute; + background: rgba(15, 23, 42, 0.95); + border: 1px solid #334155; + border-radius: 6px; + padding: 8px 12px; + font-size: 0.8rem; + color: #fff; + pointer-events: none; + z-index: 2000; + display: none; + box-shadow: 0 8px 24px rgba(0,0,0,0.5); +} diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..e007cf9 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,758 @@ +'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 = ` +
+ ${b.label} +
+ ${b.amps}A + • + ${b.poles}P ${b.voltage}V +
+
+
${b.description || 'Spare'}
+ + `; + + 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(' ', ' '); + 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 + ? `[${breaker.label}] ${breaker.description || ''}` + : `⚠️ UNMAPPED`; + + tooltip.innerHTML = ` +
${d.label || d.id} (${d.type.toUpperCase()})
+
Room: ${d.room || 'Unknown'}
+
Circuit: ${statusHtml}
+ ${d.notes ? `
${d.notes}
` : ''} +
Click to inspect / Right-click to unassign
+ `; + 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 = ''; + + 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' + ? ' View: Field Doc (Landscape)' + : ' 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); + } +} diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..be39e7a --- /dev/null +++ b/routes/api.js @@ -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 ]*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; diff --git a/routes/render.js b/routes/render.js new file mode 100644 index 0000000..f1288ac --- /dev/null +++ b/routes/render.js @@ -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; diff --git a/seeds_raw.json b/seeds_raw.json new file mode 100644 index 0000000..c5f73d0 --- /dev/null +++ b/seeds_raw.json @@ -0,0 +1,2987 @@ +{ + "devices": [ + { + "id": "dev-26", + "drawio_id": "26", + "type": "switch", + "x": 4028.859686628797, + "y": 2921.275545685916, + "w": 31.0, + "h": 64.0, + "cx": 4044.359686628797, + "cy": 2953.275545685916, + "color": "#e03131", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Switch #26", + "notes": "" + }, + { + "id": "dev-27", + "drawio_id": "27", + "type": "switch", + "x": 3885.990152870583, + "y": 2913.643029947668, + "w": 31.0, + "h": 64.0, + "cx": 3901.490152870583, + "cy": 2945.643029947668, + "color": "#e03131", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Switch #27", + "notes": "" + }, + { + "id": "dev-28", + "drawio_id": "28", + "type": "switch", + "x": 3884.5458989071894, + "y": 2808.524828959414, + "w": 36.0, + "h": 72.0, + "cx": 3902.5458989071894, + "cy": 2844.524828959414, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #28", + "notes": "" + }, + { + "id": "dev-29", + "drawio_id": "29", + "type": "switch", + "x": 2675.338723798538, + "y": 2546.777694690565, + "w": 31.0, + "h": 64.0, + "cx": 2690.838723798538, + "cy": 2578.777694690565, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #29", + "notes": "" + }, + { + "id": "dev-30", + "drawio_id": "30", + "type": "switch", + "x": 2718.080811932724, + "y": 2549.8307009858645, + "w": 31.0, + "h": 64.0, + "cx": 2733.580811932724, + "cy": 2581.8307009858645, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #30", + "notes": "" + }, + { + "id": "dev-31", + "drawio_id": "31", + "type": "switch", + "x": 2516.582396442992, + "y": 2839.8662990392672, + "w": 31.0, + "h": 64.0, + "cx": 2532.082396442992, + "cy": 2871.8662990392672, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #31", + "notes": "" + }, + { + "id": "dev-32", + "drawio_id": "32", + "type": "switch", + "x": 3482.9933218911174, + "y": 2919.749042538266, + "w": 31.0, + "h": 64.0, + "cx": 3498.4933218911174, + "cy": 2951.749042538266, + "color": "#e03131", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Switch #32", + "notes": "" + }, + { + "id": "dev-33", + "drawio_id": "33", + "type": "switch", + "x": 3260.1238623342924, + "y": 3258.6327413164527, + "w": 31.0, + "h": 64.0, + "cx": 3275.6238623342924, + "cy": 3290.6327413164527, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #33", + "notes": "" + }, + { + "id": "dev-34", + "drawio_id": "34", + "type": "switch", + "x": 2485.043173847551, + "y": 2079.7953683495543, + "w": 31.0, + "h": 64.0, + "cx": 2500.543173847551, + "cy": 2111.7953683495543, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #34", + "notes": "" + }, + { + "id": "dev-35", + "drawio_id": "35", + "type": "switch", + "x": 2235.705817275486, + "y": 2082.7207378051203, + "w": 31.0, + "h": 64.0, + "cx": 2251.205817275486, + "cy": 2114.7207378051203, + "color": "#e03131", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Switch #35", + "notes": "" + }, + { + "id": "dev-36", + "drawio_id": "36", + "type": "switch", + "x": 2094.4370596249073, + "y": 1844.1837764223606, + "w": 31.0, + "h": 64.0, + "cx": 2109.9370596249073, + "cy": 1876.1837764223606, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #36", + "notes": "" + }, + { + "id": "dev-37", + "drawio_id": "37", + "type": "switch", + "x": 2506.414217914644, + "y": 1966.834135423492, + "w": 31.0, + "h": 64.0, + "cx": 2521.914217914644, + "cy": 1998.834135423492, + "color": "#e03131", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Switch #37", + "notes": "" + }, + { + "id": "dev-38", + "drawio_id": "38", + "type": "switch", + "x": 3724.257401323578, + "y": 2103.2357864374417, + "w": 31.0, + "h": 64.0, + "cx": 3739.757401323578, + "cy": 2135.2357864374417, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #38", + "notes": "" + }, + { + "id": "dev-39", + "drawio_id": "39", + "type": "switch", + "x": 3779.1349325351193, + "y": 2327.4658212502636, + "w": 31.0, + "h": 64.0, + "cx": 3794.6349325351193, + "cy": 2359.4658212502636, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #39", + "notes": "" + }, + { + "id": "dev-40", + "drawio_id": "40", + "type": "switch", + "x": 3782.187938830418, + "y": 2245.034651277191, + "w": 31.0, + "h": 64.0, + "cx": 3797.687938830418, + "cy": 2277.034651277191, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #40", + "notes": "" + }, + { + "id": "dev-41", + "drawio_id": "41", + "type": "switch", + "x": 2037.2604080810513, + "y": 2180.4169392546873, + "w": 31.0, + "h": 64.0, + "cx": 2052.7604080810515, + "cy": 2212.4169392546873, + "color": "#e03131", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Switch #41", + "notes": "" + }, + { + "id": "dev-42", + "drawio_id": "42", + "type": "switch", + "x": 1767.9696049714892, + "y": 2077.0427229319075, + "w": 31.0, + "h": 64.0, + "cx": 1783.4696049714892, + "cy": 2109.0427229319075, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #42", + "notes": "" + }, + { + "id": "dev-43", + "drawio_id": "43", + "type": "switch", + "x": 1720.6352437103817, + "y": 2170.8299226514046, + "w": 31.0, + "h": 64.0, + "cx": 1736.1352437103817, + "cy": 2202.8299226514046, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #43", + "notes": "" + }, + { + "id": "dev-44", + "drawio_id": "44", + "type": "switch", + "x": 1571.3638234863313, + "y": 1857.9350684351793, + "w": 31.0, + "h": 64.0, + "cx": 1586.8638234863313, + "cy": 1889.9350684351793, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #44", + "notes": "" + }, + { + "id": "dev-45", + "drawio_id": "45", + "type": "switch", + "x": 1894.9824907880231, + "y": 1674.7546907172405, + "w": 31.0, + "h": 64.0, + "cx": 1910.4824907880231, + "cy": 1706.7546907172405, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #45", + "notes": "" + }, + { + "id": "dev-46", + "drawio_id": "46", + "type": "switch", + "x": 2412.7801824028256, + "y": 2565.095732462359, + "w": 31.0, + "h": 64.0, + "cx": 2428.2801824028256, + "cy": 2597.095732462359, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #46", + "notes": "" + }, + { + "id": "dev-47", + "drawio_id": "47", + "type": "switch", + "x": 2455.5222705370115, + "y": 2568.148738757658, + "w": 31.0, + "h": 64.0, + "cx": 2471.0222705370115, + "cy": 2600.148738757658, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #47", + "notes": "" + }, + { + "id": "dev-48", + "drawio_id": "48", + "type": "light", + "x": 3745.5518632868298, + "y": 2747.2541868538738, + "w": 61.0, + "h": 70.0, + "cx": 3776.0518632868298, + "cy": 2782.2541868538738, + "color": "#1971c2", + "room": "Office Hall", + "zone": "central", + "breaker_id": null, + "label": "Light #48", + "notes": "" + }, + { + "id": "dev-49", + "drawio_id": "49", + "type": "light", + "x": 2907.367202241261, + "y": 2900.926424945247, + "w": 61.0, + "h": 70.0, + "cx": 2937.867202241261, + "cy": 2935.926424945247, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #49", + "notes": "" + }, + { + "id": "dev-50", + "drawio_id": "50", + "type": "light", + "x": 4153.843798620308, + "y": 2902.9447442301484, + "w": 61.0, + "h": 70.0, + "cx": 4184.343798620308, + "cy": 2937.9447442301484, + "color": "#1971c2", + "room": "Office", + "zone": "central", + "breaker_id": null, + "label": "Light #50", + "notes": "" + }, + { + "id": "dev-51", + "drawio_id": "51", + "type": "light", + "x": 3769.975913649221, + "y": 3096.823407665607, + "w": 61.0, + "h": 70.0, + "cx": 3800.475913649221, + "cy": 3131.823407665607, + "color": "#1971c2", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Light #51", + "notes": "" + }, + { + "id": "dev-52", + "drawio_id": "52", + "type": "light", + "x": 2238.7588235707835, + "y": 2656.6859213213284, + "w": 61.0, + "h": 70.0, + "cx": 2269.2588235707835, + "cy": 2691.6859213213284, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #52", + "notes": "" + }, + { + "id": "dev-53", + "drawio_id": "53", + "type": "light", + "x": 1323.8720621612463, + "y": 2652.521645911793, + "w": 61.0, + "h": 70.0, + "cx": 1354.3720621612463, + "cy": 2687.521645911793, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #53", + "notes": "" + }, + { + "id": "dev-54", + "drawio_id": "54", + "type": "light", + "x": 1528.8259542957173, + "y": 2283.1078841806157, + "w": 61.0, + "h": 70.0, + "cx": 1559.3259542957173, + "cy": 2318.1078841806157, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #54", + "notes": "" + }, + { + "id": "dev-55", + "drawio_id": "55", + "type": "light", + "x": 2040.31341437635, + "y": 2394.1273799256164, + "w": 61.0, + "h": 70.0, + "cx": 2070.81341437635, + "cy": 2429.1273799256164, + "color": "#1971c2", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Light #55", + "notes": "" + }, + { + "id": "dev-56", + "drawio_id": "56", + "type": "light", + "x": 1458.402590560269, + "y": 1729.708804032622, + "w": 61.0, + "h": 70.0, + "cx": 1488.902590560269, + "cy": 1764.708804032622, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #56", + "notes": "" + }, + { + "id": "dev-57", + "drawio_id": "57", + "type": "light", + "x": 1913.3005285598167, + "y": 1766.3448795762101, + "w": 61.0, + "h": 70.0, + "cx": 1943.8005285598167, + "cy": 1801.3448795762101, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #57", + "notes": "" + }, + { + "id": "dev-58", + "drawio_id": "58", + "type": "light", + "x": 2182.974242188578, + "y": 1712.9045057245048, + "w": 61.0, + "h": 70.0, + "cx": 2213.474242188578, + "cy": 1747.9045057245048, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #58", + "notes": "" + }, + { + "id": "dev-59", + "drawio_id": "59", + "type": "light", + "x": 2842.2449103975314, + "y": 1728.6996443901717, + "w": 61.0, + "h": 70.0, + "cx": 2872.7449103975314, + "cy": 1763.6996443901717, + "color": "#1971c2", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Light #59", + "notes": "" + }, + { + "id": "dev-60", + "drawio_id": "60", + "type": "light", + "x": 2834.095051154085, + "y": 2247.583077751265, + "w": 61.0, + "h": 70.0, + "cx": 2864.595051154085, + "cy": 2282.583077751265, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #60", + "notes": "" + }, + { + "id": "dev-61", + "drawio_id": "61", + "type": "light", + "x": 3852.4070836222945, + "y": 2333.571833840862, + "w": 61.0, + "h": 70.0, + "cx": 3882.9070836222945, + "cy": 2368.571833840862, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #61", + "notes": "" + }, + { + "id": "dev-62", + "drawio_id": "62", + "type": "light", + "x": 4026.4284424543366, + "y": 2248.0876575724906, + "w": 61.0, + "h": 70.0, + "cx": 4056.9284424543366, + "cy": 2283.0876575724906, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #62", + "notes": "" + }, + { + "id": "dev-63", + "drawio_id": "63", + "type": "light", + "x": 2360.8790753827425, + "y": 2531.51266321407, + "w": 61.0, + "h": 70.0, + "cx": 2391.3790753827425, + "cy": 2566.51266321407, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #63", + "notes": "" + }, + { + "id": "dev-64", + "drawio_id": "64", + "type": "light", + "x": 3360.8730700791584, + "y": 3115.1414454374008, + "w": 61.0, + "h": 70.0, + "cx": 3391.3730700791584, + "cy": 3150.1414454374008, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #64", + "notes": "" + }, + { + "id": "dev-65", + "drawio_id": "65", + "type": "light", + "x": 3171.586679770622, + "y": 3270.8447664976493, + "w": 61.0, + "h": 70.0, + "cx": 3202.086679770622, + "cy": 3305.8447664976493, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #65", + "notes": "" + }, + { + "id": "dev-66", + "drawio_id": "66", + "type": "light", + "x": 1963.4380845901978, + "y": 2054.4049741477543, + "w": 61.0, + "h": 78.0, + "cx": 1993.9380845901978, + "cy": 2093.4049741477543, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #66", + "notes": "" + }, + { + "id": "dev-67", + "drawio_id": "67", + "type": "switch", + "x": 2449.735484166603, + "y": 2168.669904466954, + "w": 31.0, + "h": 64.0, + "cx": 2465.235484166603, + "cy": 2200.669904466954, + "color": "#e03131", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Switch #67", + "notes": "" + }, + { + "id": "dev-68", + "drawio_id": "68", + "type": "outlet", + "x": 4291.406635209138, + "y": 3252.7397745961734, + "w": 31.0, + "h": 64.0, + "cx": 4306.906635209138, + "cy": 3284.7397745961734, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #68", + "notes": "" + }, + { + "id": "dev-69", + "drawio_id": "69", + "type": "outlet", + "x": 4069.246257537897, + "y": 3265.8222024518727, + "w": 31.0, + "h": 64.0, + "cx": 4084.746257537897, + "cy": 3297.8222024518727, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #69", + "notes": "" + }, + { + "id": "dev-70", + "drawio_id": "70", + "type": "outlet", + "x": 4349.431526436321, + "y": 3218.161821659873, + "w": 31.0, + "h": 64.0, + "cx": 4364.931526436321, + "cy": 3250.161821659873, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #70", + "notes": "" + }, + { + "id": "dev-71", + "drawio_id": "71", + "type": "outlet", + "x": 4092.3543209521995, + "y": 2520.5871573406002, + "w": 31.0, + "h": 64.0, + "cx": 4107.8543209521995, + "cy": 2552.5871573406002, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #71", + "notes": "" + }, + { + "id": "dev-72", + "drawio_id": "72", + "type": "outlet", + "x": 3933.8929192138485, + "y": 2529.676690726215, + "w": 31.0, + "h": 64.0, + "cx": 3949.3929192138485, + "cy": 2561.676690726215, + "color": "#2f9e44", + "room": "Server Room", + "zone": "central", + "breaker_id": null, + "label": "Outlet #72", + "notes": "" + }, + { + "id": "dev-73", + "drawio_id": "73", + "type": "outlet", + "x": 3920.894633543303, + "y": 2916.736752915791, + "w": 31.0, + "h": 64.0, + "cx": 3936.394633543303, + "cy": 2948.736752915791, + "color": "#2f9e44", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Outlet #73", + "notes": "" + }, + { + "id": "dev-74", + "drawio_id": "74", + "type": "outlet", + "x": 3220.431461297242, + "y": 2535.453706579791, + "w": 31.0, + "h": 64.0, + "cx": 3235.931461297242, + "cy": 2567.453706579791, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #74", + "notes": "" + }, + { + "id": "dev-75", + "drawio_id": "75", + "type": "outlet", + "x": 3480.3971747081514, + "y": 2978.8396733417303, + "w": 31.0, + "h": 64.0, + "cx": 3495.8971747081514, + "cy": 3010.8396733417303, + "color": "#2f9e44", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Outlet #75", + "notes": "" + }, + { + "id": "dev-76", + "drawio_id": "76", + "type": "outlet", + "x": 2559.3653114552335, + "y": 2708.4241007343494, + "w": 31.0, + "h": 64.0, + "cx": 2574.8653114552335, + "cy": 2740.4241007343494, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #76", + "notes": "" + }, + { + "id": "dev-77", + "drawio_id": "77", + "type": "outlet", + "x": 2636.494548719841, + "y": 3276.015908348167, + "w": 31.0, + "h": 64.0, + "cx": 2651.994548719841, + "cy": 3308.015908348167, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #77", + "notes": "" + }, + { + "id": "dev-78", + "drawio_id": "78", + "type": "outlet", + "x": 3322.113015939547, + "y": 3276.3559898008816, + "w": 31.0, + "h": 64.0, + "cx": 3337.613015939547, + "cy": 3308.3559898008816, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #78", + "notes": "" + }, + { + "id": "dev-79", + "drawio_id": "79", + "type": "outlet", + "x": 2035.6848999479625, + "y": 2567.179090593998, + "w": 31.0, + "h": 64.0, + "cx": 2051.1848999479625, + "cy": 2599.179090593998, + "color": "#2f9e44", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Outlet #79", + "notes": "" + }, + { + "id": "dev-80", + "drawio_id": "80", + "type": "outlet", + "x": 2249.4344865302655, + "y": 2354.8737579750887, + "w": 31.0, + "h": 64.0, + "cx": 2264.9344865302655, + "cy": 2386.8737579750887, + "color": "#2f9e44", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Outlet #80", + "notes": "" + }, + { + "id": "dev-81", + "drawio_id": "81", + "type": "outlet", + "x": 1862.37442434069, + "y": 2481.968106753756, + "w": 31.0, + "h": 64.0, + "cx": 1877.87442434069, + "cy": 2513.968106753756, + "color": "#2f9e44", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Outlet #81", + "notes": "" + }, + { + "id": "dev-82", + "drawio_id": "82", + "type": "outlet", + "x": 2097.787820373902, + "y": 2167.120742733877, + "w": 31.0, + "h": 64.0, + "cx": 2113.287820373902, + "cy": 2199.120742733877, + "color": "#2f9e44", + "room": "Rec Room / Central North", + "zone": "central", + "breaker_id": null, + "label": "Outlet #82", + "notes": "" + }, + { + "id": "dev-83", + "drawio_id": "83", + "type": "outlet", + "x": 3203.704871204577, + "y": 1573.5639042125094, + "w": 31.0, + "h": 64.0, + "cx": 3219.204871204577, + "cy": 1605.5639042125094, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #83", + "notes": "" + }, + { + "id": "dev-84", + "drawio_id": "84", + "type": "outlet", + "x": 3427.422845602492, + "y": 1570.6753962857213, + "w": 31.0, + "h": 64.0, + "cx": 3442.922845602492, + "cy": 1602.6753962857213, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #84", + "notes": "" + }, + { + "id": "dev-85", + "drawio_id": "85", + "type": "outlet", + "x": 3722.8060075076864, + "y": 1942.6171247784964, + "w": 31.0, + "h": 64.0, + "cx": 3738.3060075076864, + "cy": 1974.6171247784964, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #85", + "notes": "" + }, + { + "id": "dev-86", + "drawio_id": "86", + "type": "outlet", + "x": 3360.2230722284057, + "y": 2444.5417099767255, + "w": 31.0, + "h": 64.0, + "cx": 3375.7230722284057, + "cy": 2476.5417099767255, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #86", + "notes": "" + }, + { + "id": "dev-87", + "drawio_id": "87", + "type": "outlet", + "x": 2502.738383364822, + "y": 2428.3148349266785, + "w": 31.0, + "h": 64.0, + "cx": 2518.238383364822, + "cy": 2460.3148349266785, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #87", + "notes": "" + }, + { + "id": "dev-88", + "drawio_id": "88", + "type": "outlet", + "x": 3152.988741114698, + "y": 1767.0939353072974, + "w": 31.0, + "h": 64.0, + "cx": 3168.488741114698, + "cy": 1799.0939353072974, + "color": "#2f9e44", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Outlet #88", + "notes": "" + }, + { + "id": "dev-89", + "drawio_id": "89", + "type": "outlet", + "x": 2833.375867914252, + "y": 1572.1196502491157, + "w": 31.0, + "h": 64.0, + "cx": 2848.875867914252, + "cy": 1604.1196502491157, + "color": "#2f9e44", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Outlet #89", + "notes": "" + }, + { + "id": "dev-90", + "drawio_id": "90", + "type": "outlet", + "x": 2447.1507184074762, + "y": 1576.1123306865832, + "w": 31.0, + "h": 64.0, + "cx": 2462.6507184074762, + "cy": 1608.1123306865832, + "color": "#2f9e44", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Outlet #90", + "notes": "" + }, + { + "id": "dev-91", + "drawio_id": "91", + "type": "outlet", + "x": 3493.068585417702, + "y": 3215.2062781477543, + "w": 31.0, + "h": 64.0, + "cx": 3508.568585417702, + "cy": 3247.2062781477543, + "color": "#2f9e44", + "room": "Guest Bath", + "zone": "central", + "breaker_id": null, + "label": "Outlet #91", + "notes": "" + }, + { + "id": "dev-92", + "drawio_id": "92", + "type": "outlet", + "x": 2062.922406205779, + "y": 1791.1792193429862, + "w": 31.0, + "h": 64.0, + "cx": 2078.422406205779, + "cy": 1823.1792193429862, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #92", + "notes": "" + }, + { + "id": "dev-98", + "drawio_id": "98", + "type": "outlet", + "x": 4244.650533493862, + "y": 3348.0873168620196, + "w": 31.0, + "h": 64.0, + "cx": 4260.150533493862, + "cy": 3380.0873168620196, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #98", + "notes": "" + }, + { + "id": "dev-99", + "drawio_id": "99", + "type": "outlet", + "x": 2193.167221864628, + "y": 2647.270036491784, + "w": 31.0, + "h": 64.0, + "cx": 2208.667221864628, + "cy": 2679.270036491784, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #99", + "notes": "" + }, + { + "id": "dev-100", + "drawio_id": "100", + "type": "outlet", + "x": 1612.1259857758516, + "y": 3465.3149346694045, + "w": 31.0, + "h": 64.0, + "cx": 1627.6259857758516, + "cy": 3497.3149346694045, + "color": "#2f9e44", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Outlet #100", + "notes": "" + }, + { + "id": "dev-108", + "drawio_id": "108", + "type": "light", + "x": 2595.8441321362006, + "y": 3556.5993435587925, + "w": 61.0, + "h": 70.0, + "cx": 2626.3441321362006, + "cy": 3591.5993435587925, + "color": "#1971c2", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #108", + "notes": "" + }, + { + "id": "dev-109", + "drawio_id": "109", + "type": "light", + "x": 2588.19885271398, + "y": 3734.9891967439435, + "w": 61.0, + "h": 70.0, + "cx": 2618.69885271398, + "cy": 3769.9891967439435, + "color": "#1971c2", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #109", + "notes": "" + }, + { + "id": "dev-110", + "drawio_id": "110", + "type": "light", + "x": 2585.6504262399067, + "y": 3898.088491084653, + "w": 61.0, + "h": 70.0, + "cx": 2616.1504262399067, + "cy": 3933.088491084653, + "color": "#1971c2", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #110", + "notes": "" + }, + { + "id": "dev-111", + "drawio_id": "111", + "type": "light", + "x": 2585.6504262399067, + "y": 4073.92991779573, + "w": 61.0, + "h": 70.0, + "cx": 2616.1504262399067, + "cy": 4108.92991779573, + "color": "#1971c2", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #111", + "notes": "" + }, + { + "id": "dev-112", + "drawio_id": "112", + "type": "light", + "x": 2585.6504262399067, + "y": 4224.287079766072, + "w": 61.0, + "h": 70.0, + "cx": 2616.1504262399067, + "cy": 4259.287079766072, + "color": "#1971c2", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #112", + "notes": "" + }, + { + "id": "dev-113", + "drawio_id": "113", + "type": "light", + "x": 2710.5233234695124, + "y": 4229.383932714219, + "w": 61.0, + "h": 70.0, + "cx": 2741.0233234695124, + "cy": 4264.383932714219, + "color": "#1971c2", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #113", + "notes": "" + }, + { + "id": "dev-114", + "drawio_id": "114", + "type": "light", + "x": 2799.7182500620875, + "y": 4231.932359188293, + "w": 61.0, + "h": 70.0, + "cx": 2830.2182500620875, + "cy": 4266.932359188293, + "color": "#1971c2", + "room": "Great Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #114", + "notes": "" + }, + { + "id": "dev-115", + "drawio_id": "115", + "type": "light", + "x": 3355.2752214101292, + "y": 4048.4456530549946, + "w": 61.0, + "h": 70.0, + "cx": 3385.7752214101292, + "cy": 4083.4456530549946, + "color": "#1971c2", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #115", + "notes": "" + }, + { + "id": "dev-116", + "drawio_id": "116", + "type": "light", + "x": 3184.5306476471987, + "y": 3561.69619650694, + "w": 61.0, + "h": 70.0, + "cx": 3215.0306476471987, + "cy": 3596.69619650694, + "color": "#1971c2", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #116", + "notes": "" + }, + { + "id": "dev-117", + "drawio_id": "117", + "type": "light", + "x": 3760.4750307878285, + "y": 4071.3814913216565, + "w": 61.0, + "h": 70.0, + "cx": 3790.9750307878285, + "cy": 4106.3814913216565, + "color": "#1971c2", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #117", + "notes": "" + }, + { + "id": "dev-118", + "drawio_id": "118", + "type": "light", + "x": 3709.5065013063568, + "y": 3732.44077026987, + "w": 61.0, + "h": 70.0, + "cx": 3740.0065013063568, + "cy": 3767.44077026987, + "color": "#1971c2", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #118", + "notes": "" + }, + { + "id": "dev-119", + "drawio_id": "119", + "type": "light", + "x": 4188.610678432191, + "y": 3712.053358477281, + "w": 61.0, + "h": 70.0, + "cx": 4219.110678432191, + "cy": 3747.053358477281, + "color": "#1971c2", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #119", + "notes": "" + }, + { + "id": "dev-120", + "drawio_id": "120", + "type": "light", + "x": 4191.1591049062645, + "y": 3913.3790499290944, + "w": 61.0, + "h": 70.0, + "cx": 4221.6591049062645, + "cy": 3948.3790499290944, + "color": "#1971c2", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #120", + "notes": "" + }, + { + "id": "dev-121", + "drawio_id": "121", + "type": "light", + "x": 3097.884147528698, + "y": 4336.41784462531, + "w": 61.0, + "h": 70.0, + "cx": 3128.384147528698, + "cy": 4371.41784462531, + "color": "#1971c2", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #121", + "notes": "" + }, + { + "id": "dev-122", + "drawio_id": "122", + "type": "light", + "x": 3429.1795891582633, + "y": 4226.835506240145, + "w": 61.0, + "h": 70.0, + "cx": 3459.6795891582633, + "cy": 4261.835506240145, + "color": "#1971c2", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Light #122", + "notes": "" + }, + { + "id": "dev-124", + "drawio_id": "124", + "type": "switch", + "x": 3067.2775024718676, + "y": 4240.0822184317385, + "w": 31.0, + "h": 64.0, + "cx": 3082.7775024718676, + "cy": 4272.0822184317385, + "color": "#e03131", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #124", + "notes": "" + }, + { + "id": "dev-125", + "drawio_id": "125", + "type": "switch", + "x": 3026.50267888669, + "y": 4234.9853654835915, + "w": 31.0, + "h": 64.0, + "cx": 3042.00267888669, + "cy": 4266.9853654835915, + "color": "#e03131", + "room": "Great Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #125", + "notes": "" + }, + { + "id": "dev-126", + "drawio_id": "126", + "type": "switch", + "x": 2988.2762817755865, + "y": 4240.0822184317385, + "w": 31.0, + "h": 64.0, + "cx": 3003.7762817755865, + "cy": 4272.0822184317385, + "color": "#e03131", + "room": "Great Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #126", + "notes": "" + }, + { + "id": "dev-127", + "drawio_id": "127", + "type": "switch", + "x": 2950.049884664483, + "y": 4237.533791957665, + "w": 31.0, + "h": 64.0, + "cx": 2965.549884664483, + "cy": 4269.533791957665, + "color": "#e03131", + "room": "Great Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #127", + "notes": "" + }, + { + "id": "dev-128", + "drawio_id": "128", + "type": "switch", + "x": 3197.2472526496203, + "y": 4234.985365483591, + "w": 31.0, + "h": 64.0, + "cx": 3212.7472526496203, + "cy": 4266.985365483591, + "color": "#e03131", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #128", + "notes": "" + }, + { + "id": "dev-129", + "drawio_id": "129", + "type": "switch", + "x": 3243.118929182945, + "y": 4234.9853654835915, + "w": 31.0, + "h": 64.0, + "cx": 3258.618929182945, + "cy": 4266.9853654835915, + "color": "#e03131", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #129", + "notes": "" + }, + { + "id": "dev-130", + "drawio_id": "130", + "type": "switch", + "x": 3286.4421792421954, + "y": 4232.436939009518, + "w": 31.0, + "h": 64.0, + "cx": 3301.9421792421954, + "cy": 4264.436939009518, + "color": "#e03131", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #130", + "notes": "" + }, + { + "id": "dev-131", + "drawio_id": "131", + "type": "switch", + "x": 2994.6473479607703, + "y": 3718.9290044836907, + "w": 31.0, + "h": 64.0, + "cx": 3010.1473479607703, + "cy": 3750.9290044836907, + "color": "#e03131", + "room": "Great Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #131", + "notes": "" + }, + { + "id": "dev-132", + "drawio_id": "132", + "type": "switch", + "x": 3040.519024494094, + "y": 3718.9290044836907, + "w": 31.0, + "h": 64.0, + "cx": 3056.019024494094, + "cy": 3750.9290044836907, + "color": "#e03131", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #132", + "notes": "" + }, + { + "id": "dev-133", + "drawio_id": "133", + "type": "switch", + "x": 3083.8422745533453, + "y": 3716.3805780096172, + "w": 31.0, + "h": 64.0, + "cx": 3099.3422745533453, + "cy": 3748.3805780096172, + "color": "#e03131", + "room": "Kitchen", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #133", + "notes": "" + }, + { + "id": "dev-134", + "drawio_id": "134", + "type": "switch", + "x": 3954.1299154494745, + "y": 3702.3642324022126, + "w": 31.0, + "h": 64.0, + "cx": 3969.6299154494745, + "cy": 3734.3642324022126, + "color": "#e03131", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #134", + "notes": "" + }, + { + "id": "dev-135", + "drawio_id": "135", + "type": "switch", + "x": 4043.3248420420505, + "y": 3880.754085587363, + "w": 31.0, + "h": 64.0, + "cx": 4058.8248420420505, + "cy": 3912.754085587363, + "color": "#e03131", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #135", + "notes": "" + }, + { + "id": "dev-136", + "drawio_id": "136", + "type": "switch", + "x": 3836.9022976420893, + "y": 3868.0119532169956, + "w": 31.0, + "h": 64.0, + "cx": 3852.4022976420893, + "cy": 3900.0119532169956, + "color": "#e03131", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #136", + "notes": "" + }, + { + "id": "dev-137", + "drawio_id": "137", + "type": "switch", + "x": 3576.9627972865846, + "y": 3898.5930709058784, + "w": 31.0, + "h": 64.0, + "cx": 3592.4627972865846, + "cy": 3930.5930709058784, + "color": "#e03131", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Switch #137", + "notes": "" + }, + { + "id": "dev-138", + "drawio_id": "138", + "type": "outlet", + "x": 2848.112825701539, + "y": 3513.780673320767, + "w": 31.0, + "h": 64.0, + "cx": 2863.612825701539, + "cy": 3545.780673320767, + "color": "#2f9e44", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #138", + "notes": "" + }, + { + "id": "dev-139", + "drawio_id": "139", + "type": "outlet", + "x": 2723.239928471933, + "y": 3511.2322468466937, + "w": 31.0, + "h": 64.0, + "cx": 2738.739928471933, + "cy": 3543.2322468466937, + "color": "#2f9e44", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #139", + "notes": "" + }, + { + "id": "dev-140", + "drawio_id": "140", + "type": "outlet", + "x": 2577.9796194497394, + "y": 3827.237129631818, + "w": 31.0, + "h": 64.0, + "cx": 2593.4796194497394, + "cy": 3859.237129631818, + "color": "#2f9e44", + "room": "Living Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #140", + "notes": "" + }, + { + "id": "dev-141", + "drawio_id": "141", + "type": "outlet", + "x": 2995.921561197807, + "y": 4173.823130105825, + "w": 31.0, + "h": 64.0, + "cx": 3011.421561197807, + "cy": 4205.823130105825, + "color": "#2f9e44", + "room": "Great Room", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #141", + "notes": "" + }, + { + "id": "dev-142", + "drawio_id": "142", + "type": "outlet", + "x": 3487.7678706940087, + "y": 4245.1790713798855, + "w": 31.0, + "h": 64.0, + "cx": 3503.2678706940087, + "cy": 4277.1790713798855, + "color": "#2f9e44", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #142", + "notes": "" + }, + { + "id": "dev-143", + "drawio_id": "143", + "type": "outlet", + "x": 3508.1552824865976, + "y": 4176.371556579899, + "w": 31.0, + "h": 64.0, + "cx": 3523.6552824865976, + "cy": 4208.371556579899, + "color": "#2f9e44", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #143", + "notes": "" + }, + { + "id": "dev-144", + "drawio_id": "144", + "type": "outlet", + "x": 3500.510003064376, + "y": 3906.238350328099, + "w": 31.0, + "h": 64.0, + "cx": 3516.010003064376, + "cy": 3938.238350328099, + "color": "#2f9e44", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #144", + "notes": "" + }, + { + "id": "dev-145", + "drawio_id": "145", + "type": "outlet", + "x": 3798.675900530985, + "y": 3868.011953216995, + "w": 31.0, + "h": 64.0, + "cx": 3814.175900530985, + "cy": 3900.011953216995, + "color": "#2f9e44", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #145", + "notes": "" + }, + { + "id": "dev-146", + "drawio_id": "146", + "type": "outlet", + "x": 4354.232871879027, + "y": 3597.878746965195, + "w": 31.0, + "h": 64.0, + "cx": 4369.732871879027, + "cy": 3629.878746965195, + "color": "#2f9e44", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #146", + "notes": "" + }, + { + "id": "dev-147", + "drawio_id": "147", + "type": "outlet", + "x": 4038.2279890939035, + "y": 4130.499880046575, + "w": 31.0, + "h": 64.0, + "cx": 4053.7279890939035, + "cy": 4162.499880046575, + "color": "#2f9e44", + "room": "Pink Bathroom", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #147", + "notes": "" + }, + { + "id": "dev-148", + "drawio_id": "148", + "type": "outlet", + "x": 4168.197739271655, + "y": 4245.1790713798855, + "w": 31.0, + "h": 64.0, + "cx": 4183.697739271655, + "cy": 4277.1790713798855, + "color": "#2f9e44", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #148", + "notes": "" + }, + { + "id": "dev-149", + "drawio_id": "149", + "type": "outlet", + "x": 4305.812768871629, + "y": 4245.1790713798855, + "w": 31.0, + "h": 64.0, + "cx": 4321.312768871629, + "cy": 4277.1790713798855, + "color": "#2f9e44", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #149", + "notes": "" + }, + { + "id": "dev-150", + "drawio_id": "150", + "type": "outlet", + "x": 2939.8561787681883, + "y": 3592.781894017048, + "w": 31.0, + "h": 64.0, + "cx": 2955.3561787681883, + "cy": 3624.781894017048, + "color": "#2f9e44", + "room": "West Wing", + "zone": "subpanel_west", + "breaker_id": null, + "label": "Outlet #150", + "notes": "" + }, + { + "id": "dev-151", + "drawio_id": "151", + "type": "light", + "x": 2952.0571070615315, + "y": 1489.260184197319, + "w": 61.0, + "h": 70.0, + "cx": 2982.5571070615315, + "cy": 1524.260184197319, + "color": "#1971c2", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Light #151", + "notes": "" + }, + { + "id": "dev-152", + "drawio_id": "152", + "type": "switch", + "x": 2999.8568685568434, + "y": 1583.3744249635415, + "w": 31.0, + "h": 64.0, + "cx": 3015.3568685568434, + "cy": 1615.3744249635415, + "color": "#e03131", + "room": "Central South", + "zone": "central", + "breaker_id": null, + "label": "Switch #152", + "notes": "" + }, + { + "id": "dev-153", + "drawio_id": "153", + "type": "light", + "x": 1221.5514174120751, + "y": 778.0067284404868, + "w": 61.0, + "h": 70.0, + "cx": 1252.0514174120751, + "cy": 813.0067284404868, + "color": "#1971c2", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Light #153", + "notes": "" + }, + { + "id": "dev-156", + "drawio_id": "156", + "type": "switch", + "x": 1317.6660195498632, + "y": 828.7382670573347, + "w": 31.0, + "h": 64.0, + "cx": 1333.1660195498632, + "cy": 860.7382670573347, + "color": "#e03131", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Switch #156", + "notes": "" + }, + { + "id": "dev-157", + "drawio_id": "157", + "type": "switch", + "x": 1317.666019549863, + "y": 740.2517923345727, + "w": 31.0, + "h": 64.0, + "cx": 1333.166019549863, + "cy": 772.2517923345727, + "color": "#e03131", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Switch #157", + "notes": "" + }, + { + "id": "dev-158", + "drawio_id": "158", + "type": "switch", + "x": 2226.642580947386, + "y": 797.4422443960052, + "w": 31.0, + "h": 64.0, + "cx": 2242.142580947386, + "cy": 829.4422443960052, + "color": "#e03131", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Switch #158", + "notes": "" + }, + { + "id": "dev-159", + "drawio_id": "159", + "type": "switch", + "x": 2305.562950294715, + "y": 804.6168234275806, + "w": 31.0, + "h": 64.0, + "cx": 2321.062950294715, + "cy": 836.6168234275806, + "color": "#e03131", + "room": "East Wing Lower", + "zone": "east", + "breaker_id": null, + "label": "Switch #159", + "notes": "" + }, + { + "id": "dev-160", + "drawio_id": "160", + "type": "switch", + "x": 2986.951407563055, + "y": 1269.5595341489648, + "w": 31.0, + "h": 64.0, + "cx": 3002.451407563055, + "cy": 1301.5595341489648, + "color": "#e03131", + "room": "East Wing Lower", + "zone": "east", + "breaker_id": null, + "label": "Switch #160", + "notes": "" + }, + { + "id": "dev-161", + "drawio_id": "161", + "type": "light", + "x": 1914.631732101129, + "y": 750.9513437206115, + "w": 61.0, + "h": 70.0, + "cx": 1945.131732101129, + "cy": 785.9513437206115, + "color": "#1971c2", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Light #161", + "notes": "" + }, + { + "id": "dev-162", + "drawio_id": "162", + "type": "light", + "x": 2139.4352084238217, + "y": 1133.595558737962, + "w": 61.0, + "h": 70.0, + "cx": 2169.9352084238217, + "cy": 1168.595558737962, + "color": "#1971c2", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Light #162", + "notes": "" + }, + { + "id": "dev-163", + "drawio_id": "163", + "type": "light", + "x": 2926.247375553249, + "y": 770.0835544714796, + "w": 61.0, + "h": 70.0, + "cx": 2956.747375553249, + "cy": 805.0835544714796, + "color": "#1971c2", + "room": "East Wing Lower", + "zone": "east", + "breaker_id": null, + "label": "Light #163", + "notes": "" + }, + { + "id": "dev-164", + "drawio_id": "164", + "type": "light", + "x": 3383.028907230211, + "y": 774.8666071591965, + "w": 61.0, + "h": 70.0, + "cx": 3413.528907230211, + "cy": 809.8666071591965, + "color": "#1971c2", + "room": "East Wing Lower", + "zone": "east", + "breaker_id": null, + "label": "Light #164", + "notes": "" + }, + { + "id": "dev-165", + "drawio_id": "165", + "type": "light", + "x": 2041.3826283256265, + "y": 1695.9944380736217, + "w": 61.0, + "h": 70.0, + "cx": 2071.8826283256267, + "cy": 1730.9944380736217, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #165", + "notes": "" + }, + { + "id": "dev-166", + "drawio_id": "166", + "type": "light", + "x": 1974.41989069759, + "y": 1691.2113853859048, + "w": 61.0, + "h": 70.0, + "cx": 2004.91989069759, + "cy": 1726.2113853859048, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #166", + "notes": "" + }, + { + "id": "dev-167", + "drawio_id": "167", + "type": "outlet", + "x": 1353.5389147077399, + "y": 785.1973958466842, + "w": 31.0, + "h": 64.0, + "cx": 1369.0389147077399, + "cy": 817.1973958466842, + "color": "#2f9e44", + "room": "East Wing Upper", + "zone": "east", + "breaker_id": null, + "label": "Outlet #167", + "notes": "" + }, + { + "id": "dev-172", + "drawio_id": "172", + "type": "light", + "x": 1319.8608103823005, + "y": 3914.8038470456395, + "w": 61.0, + "h": 70.0, + "cx": 1350.3608103823005, + "cy": 3949.8038470456395, + "color": "#1971c2", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Light #172", + "notes": "" + }, + { + "id": "dev-173", + "drawio_id": "173", + "type": "light", + "x": 1291.5716448183873, + "y": 3547.0446947147693, + "w": 61.0, + "h": 70.0, + "cx": 1322.0716448183873, + "cy": 3582.0446947147693, + "color": "#1971c2", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Light #173", + "notes": "" + }, + { + "id": "dev-174", + "drawio_id": "174", + "type": "outlet", + "x": 1451.4257447326606, + "y": 3647.5381601746576, + "w": 31.0, + "h": 64.0, + "cx": 1466.9257447326606, + "cy": 3679.5381601746576, + "color": "#2f9e44", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Outlet #174", + "notes": "" + }, + { + "id": "dev-175", + "drawio_id": "175", + "type": "switch", + "x": 1388.560932368409, + "y": 3641.2516789382325, + "w": 31.0, + "h": 64.0, + "cx": 1404.060932368409, + "cy": 3673.2516789382325, + "color": "#e03131", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Switch #175", + "notes": "" + }, + { + "id": "dev-176", + "drawio_id": "176", + "type": "outlet", + "x": 1193.6800140392304, + "y": 3647.538160174658, + "w": 31.0, + "h": 64.0, + "cx": 1209.1800140392304, + "cy": 3679.538160174658, + "color": "#2f9e44", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Outlet #176", + "notes": "" + }, + { + "id": "dev-177", + "drawio_id": "177", + "type": "outlet", + "x": 1096.2395548746406, + "y": 4203.891749598282, + "w": 31.0, + "h": 64.0, + "cx": 1111.7395548746406, + "cy": 4235.891749598282, + "color": "#2f9e44", + "room": "General", + "zone": "other", + "breaker_id": null, + "label": "Outlet #177", + "notes": "" + }, + { + "id": "dev-178", + "drawio_id": "178", + "type": "light", + "x": 1172.1285013263098, + "y": 3171.467146667434, + "w": 61.0, + "h": 70.0, + "cx": 1202.6285013263098, + "cy": 3206.467146667434, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #178", + "notes": "" + }, + { + "id": "dev-179", + "drawio_id": "179", + "type": "light", + "x": 794.939627140802, + "y": 2945.153822156129, + "w": 61.0, + "h": 70.0, + "cx": 825.439627140802, + "cy": 2980.153822156129, + "color": "#1971c2", + "room": "Pool", + "zone": "exterior", + "breaker_id": null, + "label": "Light #179", + "notes": "" + }, + { + "id": "dev-180", + "drawio_id": "180", + "type": "outlet", + "x": 1234.5421420759933, + "y": 3234.2417247087715, + "w": 31.0, + "h": 64.0, + "cx": 1250.0421420759933, + "cy": 3266.2417247087715, + "color": "#2f9e44", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Outlet #180", + "notes": "" + }, + { + "id": "dev-181", + "drawio_id": "181", + "type": "outlet", + "x": 816.4911398537222, + "y": 3139.9445061623946, + "w": 31.0, + "h": 64.0, + "cx": 831.9911398537222, + "cy": 3171.9445061623946, + "color": "#2f9e44", + "room": "Pool", + "zone": "exterior", + "breaker_id": null, + "label": "Outlet #181", + "notes": "" + }, + { + "id": "dev-182", + "drawio_id": "182", + "type": "outlet", + "x": 869.9262303633357, + "y": 2948.206828451428, + "w": 31.0, + "h": 64.0, + "cx": 885.4262303633357, + "cy": 2980.206828451428, + "color": "#2f9e44", + "room": "Pool", + "zone": "exterior", + "breaker_id": null, + "label": "Outlet #182", + "notes": "" + }, + { + "id": "dev-183", + "drawio_id": "183", + "type": "light", + "x": 451.424056526497, + "y": 2845.610264877667, + "w": 336.0, + "h": 355.0, + "cx": 619.424056526497, + "cy": 3023.110264877667, + "color": "#1971c2", + "room": "Pool", + "zone": "exterior", + "breaker_id": null, + "label": "Pool", + "notes": "" + }, + { + "id": "dev-184", + "drawio_id": "184", + "type": "light", + "x": 1783.7204087878783, + "y": 2645.619507570388, + "w": 61.0, + "h": 70.0, + "cx": 1814.2204087878783, + "cy": 2680.619507570388, + "color": "#1971c2", + "room": "Central Area", + "zone": "central", + "breaker_id": null, + "label": "Light #184", + "notes": "" + }, + { + "id": "dev-185", + "drawio_id": "185", + "type": "outlet", + "x": 2714.8794411128065, + "y": 325.3842130411467, + "w": 31.0, + "h": 64.0, + "cx": 2730.3794411128065, + "cy": 357.3842130411467, + "color": "#2f9e44", + "room": "East Wing Lower", + "zone": "east", + "breaker_id": null, + "label": "Outlet #185", + "notes": "" + } + ], + "structures": [ + { + "id": "struct-2", + "drawio_id": "2", + "type": "boundary", + "x": 1135.3104982069542, + "y": 3167.931375789274, + "w": 173.0, + "h": 145.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=#eaddd7;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-3", + "drawio_id": "3", + "type": "room", + "x": 2554.443570512737, + "y": 2526.5728498344406, + "w": 1826.0, + "h": 820.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-4", + "drawio_id": "4", + "type": "boundary", + "x": 3494.3298519290975, + "y": 2526.9941881846908, + "w": 514.0, + "h": 141.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-5", + "drawio_id": "5", + "type": "room", + "x": 3526.9972649966567, + "y": 2886.335731927842, + "w": 484.0, + "h": 454.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-6", + "drawio_id": "6", + "type": "boundary", + "x": 2560.383100161384, + "y": 2535.4821443074115, + "w": 71.0, + "h": 86.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-7", + "drawio_id": "7", + "type": "boundary", + "x": 2759.357343391063, + "y": 2529.5426146587647, + "w": 267.0, + "h": 89.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-11", + "drawio_id": "11", + "type": "boundary", + "x": 2297.036868658687, + "y": 2527.0448680240142, + "w": 249.0, + "h": 101.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-12", + "drawio_id": "12", + "type": "boundary", + "x": 2297.120110129663, + "y": 2155.8242649835693, + "w": 143.0, + "h": 353.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-13", + "drawio_id": "13", + "type": "room", + "x": 2305.1034864311587, + "y": 1567.049738557299, + "w": 884.0, + "h": 490.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-14", + "drawio_id": "14", + "type": "room", + "x": 2043.906956498701, + "y": 1572.9892682059458, + "w": 258.0, + "h": 360.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-15", + "drawio_id": "15", + "type": "boundary", + "x": 2171.464069336598, + "y": 1932.330811949097, + "w": 131.0, + "h": 116.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-16", + "drawio_id": "16", + "type": "boundary", + "x": 1889.6219602418917, + "y": 1933.5948269998466, + "w": 198.0, + "h": 101.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-17", + "drawio_id": "17", + "type": "room", + "x": 1878.4428030370814, + "y": 1576.8017097307697, + "w": 160.0, + "h": 350.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-18", + "drawio_id": "18", + "type": "room", + "x": 1854.5688379652686, + "y": 2158.794029807893, + "w": 436.0, + "h": 479.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-19", + "drawio_id": "19", + "type": "boundary", + "x": 1759.6196050578897, + "y": 2161.5973116902655, + "w": 92.0, + "h": 163.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-20", + "drawio_id": "20", + "type": "boundary", + "x": 1762.6726113531884, + "y": 2506.0374482041425, + "w": 89.0, + "h": 132.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-21", + "drawio_id": "21", + "type": "boundary", + "x": 1759.7028465288654, + "y": 2342.8362074449783, + "w": 86.0, + "h": 142.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-22", + "drawio_id": "22", + "type": "room", + "x": 1303.5204875704248, + "y": 1581.6379987106218, + "w": 571.0, + "h": 345.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-23", + "drawio_id": "23", + "type": "boundary", + "x": 1304.5296472128753, + "y": 1928.141449543083, + "w": 348.0, + "h": 110.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-25", + "drawio_id": "25", + "type": "room", + "x": 4006.9229073539236, + "y": 1527.0019466799745, + "w": 235.0, + "h": 519.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-103", + "drawio_id": "103", + "type": "boundary", + "x": 2562.6635332373517, + "y": 2615.615940740584, + "w": 451.0, + "h": 3.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=4" + }, + { + "id": "struct-104", + "drawio_id": "104", + "type": "room", + "x": 2567.1289387089437, + "y": 3496.534248499198, + "w": 1826.0, + "h": 820.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-105", + "drawio_id": "105", + "type": "room", + "x": 3539.6826331928632, + "y": 3856.297130592599, + "w": 484.0, + "h": 454.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-106", + "drawio_id": "106", + "type": "room", + "x": 3299.158784244617, + "y": 3498.6135584040762, + "w": 711.0, + "h": 186.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=4" + }, + { + "id": "struct-107", + "drawio_id": "107", + "type": "room", + "x": 2983.1539014594928, + "y": 3493.516705455929, + "w": 153.0, + "h": 199.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=4" + }, + { + "id": "struct-154", + "drawio_id": "154", + "type": "room", + "x": 1302.3673438500857, + "y": 317.1908435337409, + "w": 982.0, + "h": 1052.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-155", + "drawio_id": "155", + "type": "room", + "x": 2292.5033294793348, + "y": 311.80613292935413, + "w": 1460.0, + "h": 1052.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-168", + "drawio_id": "168", + "type": "room", + "x": 0.0, + "y": 3.4161028494681887, + "w": 392.0, + "h": 4718.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=#1e1e1e;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-170", + "drawio_id": "170", + "type": "room", + "x": 1095.788383260073, + "y": 3579.011026492495, + "w": 547.0, + "h": 481.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=#ffffff;strokeColor=#ffffff;strokeWidth=4;dashed=1" + }, + { + "id": "struct-171", + "drawio_id": "171", + "type": "room", + "x": 1054.9262552233097, + "y": 3626.1596357656836, + "w": 559.0, + "h": 651.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=none;strokeColor=#1e1e1e;strokeWidth=2" + }, + { + "id": "struct-186", + "drawio_id": "186", + "type": "boundary", + "x": 3714.627288402001, + "y": 3826.0729290096747, + "w": 91.0, + "h": 25.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=#e03131;strokeColor=#e03131;strokeWidth=2" + }, + { + "id": "struct-187", + "drawio_id": "187", + "type": "boundary", + "x": 4291.411941844007, + "y": 3320.011189477452, + "w": 91.0, + "h": 25.0, + "label": "", + "style": "shape=rect;rounded=1;absoluteArcSize=1;arcSize=15;fillColor=#e03131;strokeColor=#e03131;strokeWidth=2" + }, + { + "id": "struct-189", + "drawio_id": "189", + "type": "room", + "x": 4126.922376519738, + "y": 2792.968153669947, + "w": 115.0, + "h": 45.0, + "label": "office", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-190", + "drawio_id": "190", + "type": "room", + "x": 3569.2919904964165, + "y": 2811.8447792442494, + "w": 179.0, + "h": 45.0, + "label": "Office Hall", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-191", + "drawio_id": "191", + "type": "room", + "x": 3676.325902407507, + "y": 3041.203161910872, + "w": 211.0, + "h": 45.0, + "label": "Guest Bath", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-192", + "drawio_id": "192", + "type": "room", + "x": 3808.844079059333, + "y": 2549.3568524146704, + "w": 112.0, + "h": 90.0, + "label": "Server Room", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-193", + "drawio_id": "193", + "type": "room", + "x": 4372.046329829595, + "y": 1629.3748952741068, + "w": 25.0, + "h": 45.0, + "label": "N", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-194", + "drawio_id": "194", + "type": "room", + "x": 4802.730403948031, + "y": 1802.6678955111108, + "w": 30.0, + "h": 45.0, + "label": "W", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-195", + "drawio_id": "195", + "type": "room", + "x": 4802.730403948031, + "y": 1479.0177333037657, + "w": 25.0, + "h": 45.0, + "label": "E", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-196", + "drawio_id": "196", + "type": "room", + "x": 4983.668683607256, + "y": 1614.0843364296657, + "w": 24.0, + "h": 45.0, + "label": "S", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-197", + "drawio_id": "197", + "type": "room", + "x": 3710.7672156901435, + "y": 4021.231378864096, + "w": 258.0, + "h": 45.0, + "label": "Pink Bathroom", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-198", + "drawio_id": "198", + "type": "room", + "x": 2704.1387584310783, + "y": 3791.872996197473, + "w": 96.0, + "h": 90.0, + "label": "Living Room", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-199", + "drawio_id": "199", + "type": "room", + "x": 3167.9523767124706, + "y": 3965.165996434477, + "w": 142.0, + "h": 45.0, + "label": "Kitchen", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Comic Sans MS;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + }, + { + "id": "struct-200", + "drawio_id": "200", + "type": "room", + "x": 2910.5613028310386, + "y": 3914.1974669530055, + "w": 99.0, + "h": 90.0, + "label": "Great Room", + "style": "text;html=1;nl2Br=0;align=left;verticalAlign=top;fontSize=32;fontFamily=Courier New;spacingLeft=0;spacingRight=0;spacingTop=0;spacingBottom=0;whiteSpace=wrap;strokeColor=none;fillColor=none" + } + ], + "edges": [ + { + "id": "edge-8", + "drawio_id": "8", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-9", + "drawio_id": "9", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-10", + "drawio_id": "10", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-24", + "drawio_id": "24", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-93", + "drawio_id": "93", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-94", + "drawio_id": "94", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-95", + "drawio_id": "95", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-96", + "drawio_id": "96", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-97", + "drawio_id": "97", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-101", + "drawio_id": "101", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=4;strokeColor=#e03131;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=block;endFill=1;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-102", + "drawio_id": "102", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=4;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + }, + { + "id": "edge-123", + "drawio_id": "123", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=4;strokeColor=#1e1e1e;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;dashed=1;dashPattern=2 2;fixDash=1" + }, + { + "id": "edge-169", + "drawio_id": "169", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=4;strokeColor=#ffffff;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;dashed=1;fixDash=1" + }, + { + "id": "edge-188", + "drawio_id": "188", + "source": null, + "target": null, + "style": "shape=filledEdge;strokeWidth=2;strokeColor=#e03131;fillColor=none;startArrow=none;startFill=0;startSize=6;endArrow=none;endFill=0;endSize=6;rounded=1;fixDash=1" + } + ] +} \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..e4015d2 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,166 @@ + + + + + + <%= appName %> + + + + + + + +
+
+ + <%= appName %> + Interactive Load Center +
+ + +
+
+
+
+ Loading... +
+ + +
+ + + + + +
+
+ + +
+ + +
+ + +
+
+ + + +
+ +
+ + + + + +
+ + +
+ + Painter: [6A] + +
+
+ + + + + + + + + + + + + +
+
+ + + + +
+ + + + +