Initial commit

This commit is contained in:
2026-05-09 01:16:30 +03:00
commit eed5f39a10
32 changed files with 10267 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"name": "networkview-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"morgan": "^1.10.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
+79
View File
@@ -0,0 +1,79 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const DB_PATH = path.join(__dirname, '../../../data/networkview.db');
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
const db = new Database(DB_PATH);
// Enable WAL mode for better performance
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS sites (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
location TEXT,
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS rooms (
id TEXT PRIMARY KEY,
site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
name TEXT NOT NULL,
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS racks (
id TEXT PRIMARY KEY,
room_id TEXT REFERENCES rooms(id) ON DELETE CASCADE,
name TEXT NOT NULL,
total_units INTEGER DEFAULT 42,
manufacturer TEXT,
model TEXT,
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS components (
id TEXT PRIMARY KEY,
rack_id TEXT REFERENCES racks(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'other',
position INTEGER,
height_units INTEGER DEFAULT 1,
manufacturer TEXT,
model TEXT,
serial_number TEXT,
asset_tag TEXT,
ip_address TEXT,
mac_address TEXT,
status TEXT DEFAULT 'active',
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS ports (
id TEXT PRIMARY KEY,
component_id TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
port_number INTEGER NOT NULL,
label TEXT,
port_type TEXT DEFAULT 'RJ45',
connected_to_port_id TEXT REFERENCES ports(id) ON DELETE SET NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// Migrate: add port_count column if it doesn't exist yet
try { db.exec('ALTER TABLE components ADD COLUMN port_count INTEGER DEFAULT NULL'); } catch (_) {}
try { db.exec('ALTER TABLE components ADD COLUMN sfp_count INTEGER DEFAULT NULL'); } catch (_) {}
module.exports = db;
+51
View File
@@ -0,0 +1,51 @@
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
// API routes
app.use('/api/sites', require('./routes/sites'));
app.use('/api/rooms', require('./routes/rooms'));
app.use('/api/racks', require('./routes/racks'));
app.use('/api/components', require('./routes/components'));
// Search across all entities
const db = require('./db');
app.get('/api/search', (req, res) => {
const { q } = req.query;
if (!q || q.trim().length < 2) return res.json({ sites: [], rooms: [], racks: [], components: [] });
const term = `%${q.trim()}%`;
const sites = db.prepare('SELECT id, name, location FROM sites WHERE name LIKE ? OR location LIKE ? LIMIT 10').all(term, term);
const rooms = db.prepare('SELECT id, name, site_id FROM rooms WHERE name LIKE ? LIMIT 10').all(term);
const racks = db.prepare('SELECT id, name, room_id FROM racks WHERE name LIKE ? OR model LIKE ? LIMIT 10').all(term, term);
const components = db.prepare(`
SELECT id, name, type, rack_id, model, ip_address FROM components
WHERE name LIKE ? OR model LIKE ? OR ip_address LIKE ? OR serial_number LIKE ? LIMIT 20
`).all(term, term, term, term);
res.json({ sites, rooms, racks, components });
});
// Health check
app.get('/api/health', (_req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));
// Serve built frontend in production
const FRONTEND_DIST = path.join(__dirname, '../../frontend/dist');
app.use(express.static(FRONTEND_DIST));
app.get('*', (_req, res) => {
res.sendFile(path.join(FRONTEND_DIST, 'index.html'), err => {
if (err) res.status(200).json({ message: 'NetworkView API running. Start frontend separately in dev mode.' });
});
});
app.listen(PORT, () => {
console.log(`NetworkView backend running on http://localhost:${PORT}`);
});
+233
View File
@@ -0,0 +1,233 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const router = express.Router();
function checkPositionOverlap(rackId, position, heightUnits, excludeId = null) {
const end = position + heightUnits - 1;
const existing = db.prepare(`
SELECT id, name, position, height_units FROM components
WHERE rack_id = ?
AND id != ?
AND position IS NOT NULL
AND position <= ? AND (position + height_units - 1) >= ?
`).all(rackId, excludeId || '', end, position);
return existing;
}
// GET /api/components?rackId=
router.get('/', (req, res) => {
const { rackId } = req.query;
if (!rackId) return res.status(400).json({ error: 'rackId query param required' });
const components = db.prepare(
'SELECT * FROM components WHERE rack_id = ? ORDER BY position ASC NULLS LAST'
).all(rackId);
res.json(components);
});
// GET /api/components/:id
router.get('/:id', (req, res) => {
const component = db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
const rawPorts = db.prepare('SELECT * FROM ports WHERE component_id = ? ORDER BY port_number').all(req.params.id);
const ports = rawPorts.map(p => {
if (!p.connected_to_port_id) return p;
const lp = db.prepare('SELECT * FROM ports WHERE id = ?').get(p.connected_to_port_id);
if (!lp) return p;
const lc = db.prepare('SELECT id, name, type FROM components WHERE id = ?').get(lp.component_id);
return { ...p, linked_port: lc ? { id: lp.id, port_number: lp.port_number, label: lp.label, component_id: lc.id, component_name: lc.name, component_type: lc.type } : null };
});
let rack = null, room = null, site = null;
if (component.rack_id) {
rack = db.prepare('SELECT id, name, total_units, room_id FROM racks WHERE id = ?').get(component.rack_id);
if (rack?.room_id) {
room = db.prepare('SELECT id, name, site_id FROM rooms WHERE id = ?').get(rack.room_id);
if (room?.site_id) site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(room.site_id);
}
}
res.json({ ...component, ports, rack, room, site });
});
// POST /api/components
router.post('/', (req, res) => {
const {
rack_id, name, type, position, height_units,
manufacturer, model, serial_number, asset_tag,
ip_address, mac_address, status, notes
} = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
if (rack_id) {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(rack_id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
if (position != null) {
const h = height_units || 1;
if (position < 1 || position + h - 1 > rack.total_units) {
return res.status(400).json({ error: `Position ${position} with ${h}U height exceeds rack size (${rack.total_units}U)` });
}
const overlapping = checkPositionOverlap(rack_id, position, h);
if (overlapping.length > 0) {
return res.status(400).json({
error: `Position overlaps with existing component: ${overlapping[0].name}`
});
}
}
}
const { port_count, sfp_count } = req.body;
const id = uuidv4();
db.prepare(`
INSERT INTO components
(id, rack_id, name, type, position, height_units, manufacturer, model,
serial_number, asset_tag, ip_address, mac_address, status, notes, port_count, sfp_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id, rack_id || null, name, type || 'other',
position ?? null, height_units || 1,
manufacturer || null, model || null,
serial_number || null, asset_tag || null,
ip_address || null, mac_address || null,
status || 'active', notes || '',
port_count ?? null, sfp_count ?? null
);
res.status(201).json(db.prepare('SELECT * FROM components WHERE id = ?').get(id));
});
// PUT /api/components/:id
router.put('/:id', (req, res) => {
const component = db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
const {
name, type, position, height_units, manufacturer, model,
serial_number, asset_tag, ip_address, mac_address, status, notes, port_count, sfp_count
} = req.body;
const newPos = position ?? component.position;
const newH = height_units ?? component.height_units;
if (component.rack_id && newPos != null) {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(component.rack_id);
if (newPos < 1 || newPos + newH - 1 > rack.total_units) {
return res.status(400).json({ error: `Position exceeds rack size (${rack.total_units}U)` });
}
const overlapping = checkPositionOverlap(component.rack_id, newPos, newH, req.params.id);
if (overlapping.length > 0) {
return res.status(400).json({ error: `Position overlaps with: ${overlapping[0].name}` });
}
}
db.prepare(`
UPDATE components SET
name = ?, type = ?, position = ?, height_units = ?,
manufacturer = ?, model = ?, serial_number = ?, asset_tag = ?,
ip_address = ?, mac_address = ?, status = ?, notes = ?,
port_count = ?, sfp_count = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
name ?? component.name,
type ?? component.type,
newPos,
newH,
manufacturer ?? component.manufacturer,
model ?? component.model,
serial_number ?? component.serial_number,
asset_tag ?? component.asset_tag,
ip_address ?? component.ip_address,
mac_address ?? component.mac_address,
status ?? component.status,
notes ?? component.notes,
port_count !== undefined ? port_count : component.port_count,
sfp_count !== undefined ? sfp_count : component.sfp_count,
req.params.id
);
res.json(db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id));
});
// DELETE /api/components/:id
router.delete('/:id', (req, res) => {
const component = db.prepare('SELECT id FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
db.prepare('DELETE FROM components WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
// --- Ports sub-resource ---
// GET /api/components/:id/ports
router.get('/:id/ports', (req, res) => {
const ports = db.prepare('SELECT * FROM ports WHERE component_id = ? ORDER BY port_number').all(req.params.id);
res.json(ports);
});
// POST /api/components/:id/ports
router.post('/:id/ports', (req, res) => {
const component = db.prepare('SELECT id FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
const { port_number, label, port_type, connected_to_port_id, notes } = req.body;
if (port_number == null) return res.status(400).json({ error: 'port_number is required' });
const id = uuidv4();
db.prepare(`
INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(id, req.params.id, port_number, label || null, port_type || 'RJ45', connected_to_port_id || null, notes || null);
// Mirror link on the other side
if (connected_to_port_id) {
db.prepare('UPDATE ports SET connected_to_port_id = ? WHERE id = ?').run(id, connected_to_port_id);
}
res.status(201).json(db.prepare('SELECT * FROM ports WHERE id = ?').get(id));
});
// PUT /api/components/:componentId/ports/:portId
router.put('/:componentId/ports/:portId', (req, res) => {
const port = db.prepare('SELECT * FROM ports WHERE id = ? AND component_id = ?').get(req.params.portId, req.params.componentId);
if (!port) return res.status(404).json({ error: 'Port not found' });
const { label, port_type, connected_to_port_id, notes } = req.body;
const newLinkedId = connected_to_port_id !== undefined ? (connected_to_port_id || null) : port.connected_to_port_id;
const oldLinkedId = port.connected_to_port_id;
// Clear old reverse link if it changed
if (oldLinkedId && oldLinkedId !== newLinkedId) {
db.prepare('UPDATE ports SET connected_to_port_id = NULL WHERE id = ? AND connected_to_port_id = ?').run(oldLinkedId, port.id);
}
db.prepare(`
UPDATE ports SET label = ?, port_type = ?, connected_to_port_id = ?, notes = ? WHERE id = ?
`).run(
label ?? port.label,
port_type ?? port.port_type,
newLinkedId,
notes ?? port.notes,
req.params.portId
);
// Set new reverse link
if (newLinkedId && newLinkedId !== oldLinkedId) {
db.prepare('UPDATE ports SET connected_to_port_id = ? WHERE id = ?').run(port.id, newLinkedId);
}
res.json(db.prepare('SELECT * FROM ports WHERE id = ?').get(req.params.portId));
});
// DELETE /api/components/:componentId/ports/:portId
router.delete('/:componentId/ports/:portId', (req, res) => {
db.prepare('DELETE FROM ports WHERE id = ? AND component_id = ?').run(req.params.portId, req.params.componentId);
res.json({ ok: true });
});
module.exports = router;
+95
View File
@@ -0,0 +1,95 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const router = express.Router();
// GET /api/racks?roomId=
router.get('/', (req, res) => {
const { roomId } = req.query;
if (!roomId) return res.status(400).json({ error: 'roomId query param required' });
const racks = db.prepare(`
SELECT ra.*, COUNT(c.id) as component_count
FROM racks ra
LEFT JOIN components c ON c.rack_id = ra.id
WHERE ra.room_id = ?
GROUP BY ra.id
ORDER BY ra.name
`).all(roomId);
res.json(racks);
});
// GET /api/racks/:id
router.get('/:id', (req, res) => {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
const components = db.prepare(`
SELECT * FROM components WHERE rack_id = ? ORDER BY position ASC NULLS LAST, name ASC
`).all(req.params.id);
// Attach ports to each component so the graphic view can show active ports
const componentsWithPorts = components.map(c => {
const ports = db.prepare('SELECT * FROM ports WHERE component_id = ? ORDER BY port_number').all(c.id);
return { ...c, ports };
});
let room = null, site = null;
if (rack.room_id) {
room = db.prepare('SELECT id, name, site_id FROM rooms WHERE id = ?').get(rack.room_id);
if (room) site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(room.site_id);
}
res.json({ ...rack, components: componentsWithPorts, room, site });
});
// POST /api/racks
router.post('/', (req, res) => {
const { room_id, name, total_units, manufacturer, model, notes } = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
if (room_id) {
const room = db.prepare('SELECT id FROM rooms WHERE id = ?').get(room_id);
if (!room) return res.status(404).json({ error: 'Room not found' });
}
const id = uuidv4();
db.prepare(`
INSERT INTO racks (id, room_id, name, total_units, manufacturer, model, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(id, room_id || null, name, total_units || 42, manufacturer || null, model || null, notes || '');
res.status(201).json(db.prepare('SELECT * FROM racks WHERE id = ?').get(id));
});
// PUT /api/racks/:id
router.put('/:id', (req, res) => {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
const { name, total_units, manufacturer, model, notes } = req.body;
db.prepare(`
UPDATE racks SET name = ?, total_units = ?, manufacturer = ?, model = ?, notes = ?,
updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(
name ?? rack.name,
total_units ?? rack.total_units,
manufacturer ?? rack.manufacturer,
model ?? rack.model,
notes ?? rack.notes,
req.params.id
);
res.json(db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id));
});
// DELETE /api/racks/:id
router.delete('/:id', (req, res) => {
const rack = db.prepare('SELECT id FROM racks WHERE id = ?').get(req.params.id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
db.prepare('DELETE FROM racks WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
module.exports = router;
+77
View File
@@ -0,0 +1,77 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const router = express.Router();
// GET /api/rooms?siteId=
router.get('/', (req, res) => {
const { siteId } = req.query;
if (!siteId) return res.status(400).json({ error: 'siteId query param required' });
const rooms = db.prepare(`
SELECT r.*, COUNT(ra.id) as rack_count
FROM rooms r
LEFT JOIN racks ra ON ra.room_id = r.id
WHERE r.site_id = ?
GROUP BY r.id
ORDER BY r.name
`).all(siteId);
res.json(rooms);
});
// GET /api/rooms/:id
router.get('/:id', (req, res) => {
const room = db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id);
if (!room) return res.status(404).json({ error: 'Room not found' });
const racks = db.prepare(`
SELECT ra.*, COUNT(c.id) as component_count
FROM racks ra
LEFT JOIN components c ON c.rack_id = ra.id
WHERE ra.room_id = ?
GROUP BY ra.id
ORDER BY ra.name
`).all(req.params.id);
const site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(room.site_id);
res.json({ ...room, racks, site });
});
// POST /api/rooms
router.post('/', (req, res) => {
const { site_id, name, notes } = req.body;
if (!site_id || !name) return res.status(400).json({ error: 'site_id and name are required' });
const site = db.prepare('SELECT id FROM sites WHERE id = ?').get(site_id);
if (!site) return res.status(404).json({ error: 'Site not found' });
const id = uuidv4();
db.prepare('INSERT INTO rooms (id, site_id, name, notes) VALUES (?, ?, ?, ?)').run(
id, site_id, name, notes || ''
);
res.status(201).json(db.prepare('SELECT * FROM rooms WHERE id = ?').get(id));
});
// PUT /api/rooms/:id
router.put('/:id', (req, res) => {
const room = db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id);
if (!room) return res.status(404).json({ error: 'Room not found' });
const { name, notes } = req.body;
db.prepare(`
UPDATE rooms SET name = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(name ?? room.name, notes ?? room.notes, req.params.id);
res.json(db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id));
});
// DELETE /api/rooms/:id
router.delete('/:id', (req, res) => {
const room = db.prepare('SELECT id FROM rooms WHERE id = ?').get(req.params.id);
if (!room) return res.status(404).json({ error: 'Room not found' });
db.prepare('DELETE FROM rooms WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
module.exports = router;
+70
View File
@@ -0,0 +1,70 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const router = express.Router();
// GET /api/sites
router.get('/', (req, res) => {
const sites = db.prepare(`
SELECT s.*, COUNT(r.id) as room_count
FROM sites s
LEFT JOIN rooms r ON r.site_id = s.id
GROUP BY s.id
ORDER BY s.name
`).all();
res.json(sites);
});
// GET /api/sites/:id
router.get('/:id', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Site not found' });
const rooms = db.prepare(`
SELECT r.*, COUNT(ra.id) as rack_count
FROM rooms r
LEFT JOIN racks ra ON ra.room_id = r.id
WHERE r.site_id = ?
GROUP BY r.id
ORDER BY r.name
`).all(req.params.id);
res.json({ ...site, rooms });
});
// POST /api/sites
router.post('/', (req, res) => {
const { name, location, notes } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const id = uuidv4();
db.prepare('INSERT INTO sites (id, name, location, notes) VALUES (?, ?, ?, ?)').run(
id, name, location || null, notes || ''
);
res.status(201).json(db.prepare('SELECT * FROM sites WHERE id = ?').get(id));
});
// PUT /api/sites/:id
router.put('/:id', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Site not found' });
const { name, location, notes } = req.body;
db.prepare(`
UPDATE sites SET name = ?, location = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(name ?? site.name, location ?? site.location, notes ?? site.notes, req.params.id);
res.json(db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id));
});
// DELETE /api/sites/:id
router.delete('/:id', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Site not found' });
db.prepare('DELETE FROM sites WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
module.exports = router;