feat: Rack stuff now complete

This commit is contained in:
2025-06-08 11:04:37 +00:00
parent f295f33809
commit 5d220d354d
4 changed files with 71 additions and 33 deletions
+2 -4
View File
@@ -119,9 +119,10 @@ def init_db(app=None):
CREATE TABLE IF NOT EXISTS RackDevice (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
rack_id INTEGER NOT NULL,
device_id INTEGER NOT NULL,
device_id INTEGER,
position_u INTEGER NOT NULL,
side ENUM('front', 'back') NOT NULL,
nonnet_device_name VARCHAR(255),
FOREIGN KEY (rack_id) REFERENCES Rack(id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE
)
@@ -152,8 +153,5 @@ def init_db(app=None):
if cursor.fetchone()[0] == 0:
cursor.execute('''INSERT INTO User (name, email, password) VALUES (%s, %s, %s)''',
('Jamie Banks', 'jamie@jdbnet.co.uk', hash_password('Drippy-Cavity-Jawline')))
cursor.execute("SHOW COLUMNS FROM Device LIKE 'rack_only'")
if not cursor.fetchone():
cursor.execute('ALTER TABLE Device ADD COLUMN rack_only BOOLEAN DEFAULT 0')
conn.commit()
conn.close()
+46 -22
View File
@@ -78,7 +78,7 @@ def register_routes(app):
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('''SELECT Device.id, Device.name, DeviceType.icon_class FROM Device LEFT JOIN DeviceType ON Device.device_type_id = DeviceType.id WHERE Device.rack_only IS NULL OR Device.rack_only=0''')
cursor.execute('''SELECT Device.id, Device.name, DeviceType.icon_class FROM Device LEFT JOIN DeviceType ON Device.device_type_id = DeviceType.id''')
devices = cursor.fetchall()
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
subnets = cursor.fetchall()
@@ -618,6 +618,16 @@ def register_routes(app):
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM Rack')
racks = cursor.fetchall()
rack_ids = [rack['id'] for rack in racks]
usage = {rack_id: 0 for rack_id in rack_ids}
if rack_ids:
format_strings = ','.join(['%s'] * len(rack_ids))
cursor.execute(f'SELECT rack_id, COUNT(*) as used FROM RackDevice WHERE rack_id IN ({format_strings}) AND side = %s GROUP BY rack_id', tuple(rack_ids) + ('front',))
for row in cursor.fetchall():
usage[row['rack_id']] = row['used']
for rack in racks:
rack['used_u'] = usage.get(rack['id'], 0)
rack['percent_full'] = int((rack['used_u'] / rack['height_u']) * 100) if rack['height_u'] else 0
return render_with_user('racks.html', racks=racks)
@app.route('/rack/add', methods=['GET', 'POST'])
@@ -651,7 +661,7 @@ def register_routes(app):
return 'Rack not found', 404
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
rack_devices = cursor.fetchall()
device_ids = [rd['device_id'] for rd in rack_devices]
device_ids = [rd['device_id'] for rd in rack_devices if rd['device_id']]
device_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
@@ -659,13 +669,20 @@ def register_routes(app):
for row in cursor.fetchall():
device_names[row['id']] = row['name']
for rd in rack_devices:
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6)')
all_devices = cursor.fetchall()
assigned = set((rd['device_id'], rd['position_u'], rd['side']) for rd in rack_devices)
site_devices = []
for d in all_devices:
site_devices.append(d)
if rd['device_id']:
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
else:
rd['device_name'] = rd['nonnet_device_name']
cursor.execute('''
SELECT DISTINCT Device.id, Device.name, Device.device_type_id, Device.description
FROM Device
JOIN DeviceIPAddress ON Device.id = DeviceIPAddress.device_id
JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id
JOIN Subnet ON IPAddress.subnet_id = Subnet.id
WHERE Device.device_type_id NOT IN (2, 6)
AND Subnet.site = %s
''', (rack['site'],))
site_devices = cursor.fetchall()
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side)
@app.route('/rack/<int:rack_id>/add_device', methods=['POST'])
@@ -745,7 +762,7 @@ def register_routes(app):
error = f"Invalid U position: {position_u}. Rack is {rack['height_u']}U tall."
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
rack_devices = cursor.fetchall()
device_ids = [rd['device_id'] for rd in rack_devices]
device_ids = [rd['device_id'] for rd in rack_devices if rd['device_id']]
device_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
@@ -753,8 +770,11 @@ def register_routes(app):
for row in cursor.fetchall():
device_names[row['id']] = row['name']
for rd in rack_devices:
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6) AND (rack_only IS NULL OR rack_only=0)')
if rd['device_id']:
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
else:
rd['device_name'] = rd['nonnet_device_name']
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6)')
site_devices = cursor.fetchall()
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side, error=error)
cursor.execute('SELECT COUNT(*) FROM RackDevice WHERE rack_id = %s AND position_u = %s AND side = %s', (rack_id, position_u, side))
@@ -762,7 +782,7 @@ def register_routes(app):
error = f"U{position_u} on the {side} is already occupied."
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
rack_devices = cursor.fetchall()
device_ids = [rd['device_id'] for rd in rack_devices]
device_ids = [rd['device_id'] for rd in rack_devices if rd['device_id']]
device_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
@@ -770,13 +790,14 @@ def register_routes(app):
for row in cursor.fetchall():
device_names[row['id']] = row['name']
for rd in rack_devices:
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6) AND (rack_only IS NULL OR rack_only=0)')
if rd['device_id']:
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
else:
rd['device_name'] = rd['nonnet_device_name']
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6)')
site_devices = cursor.fetchall()
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side, error=error)
cursor.execute('INSERT INTO Device (name, device_type_id, rack_only) VALUES (%s, %s, %s)', (device_name, 1, 1))
device_id = cursor.lastrowid
cursor.execute('INSERT INTO RackDevice (rack_id, device_id, position_u, side) VALUES (%s, %s, %s, %s)', (rack_id, device_id, position_u, side))
cursor.execute('INSERT INTO RackDevice (rack_id, device_id, position_u, side, nonnet_device_name) VALUES (%s, NULL, %s, %s, %s)', (rack_id, position_u, side, device_name))
add_audit_log(user_id, 'rack_add_nonnet_device', f"Added non-networked device '{device_name}' to rack '{rack_id}' U{position_u} ({side})", conn=conn)
conn.commit()
return redirect(url_for('rack', rack_id=rack_id))
@@ -789,11 +810,14 @@ def register_routes(app):
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT device_id, position_u, side FROM RackDevice WHERE id = %s', (rack_device_id,))
cursor.execute('SELECT device_id, nonnet_device_name, position_u, side FROM RackDevice WHERE id = %s', (rack_device_id,))
rd = cursor.fetchone()
cursor.execute('SELECT name FROM Device WHERE id = %s', (rd['device_id'],))
device_name_row = cursor.fetchone()
device_label = device_name_row['name'] if device_name_row and 'name' in device_name_row else rd['device_id']
if rd['device_id']:
cursor.execute('SELECT name FROM Device WHERE id = %s', (rd['device_id'],))
device_name_row = cursor.fetchone()
device_label = device_name_row['name'] if device_name_row and 'name' in device_name_row else rd['device_id']
else:
device_label = rd['nonnet_device_name']
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
rack_name_row = cursor.fetchone()
rack_label = rack_name_row['name'] if rack_name_row and 'name' in rack_name_row else rack_id
+8 -3
View File
@@ -12,9 +12,10 @@
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-2xl pt-20">
<div class="flex items-center mb-6 relative min-h-[3.5rem]">
<div class="flex flex-col items-center mb-6 relative min-h-[3.5rem]">
<a href="/racks" class="hidden sm:flex absolute left-0 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white items-center justify-center rounded-full w-11 h-11"><i class="fas fa-arrow-left"></i></a>
<h1 class="text-3xl font-bold text-center w-full flex items-center justify-center mb-0">{{ rack.name }} <span class="text-base text-gray-400 ml-2">({{ rack.height_u }}U, {{ rack.site }})</span></h1>
<h1 class="text-3xl font-bold text-center w-full mb-0">{{ rack.name }}</h1>
<span class="text-base text-gray-400 mt-1">({{ rack.height_u }}U, {{ rack.site }})</span>
<form action="/rack/{{ rack.id }}/delete" method="POST" onsubmit="return confirm('Delete this rack?');" class="hidden sm:flex absolute right-0 mr-14">
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white items-center justify-center rounded-full w-11 h-11 flex" title="Delete Rack">
<i class="fas fa-times fa-lg"></i>
@@ -98,7 +99,11 @@
{% set found = false %}
{% for rd in rack_devices %}
{% if rd.position_u == u and rd.side == current_side %}
<a href="/device/{{ rd.device_id }}" class="text-blue-400 hover:underline text-base">{{ rd.device_name }}</a>
{% if rd.device_id %}
<a href="/device/{{ rd.device_id }}" class="text-blue-400 hover:underline text-base">{{ rd.device_name }}</a>
{% else %}
<span class="text-blue-200 text-base">{{ rd.nonnet_device_name }}</span>
{% endif %}
<form action="/rack/{{ rack.id }}/remove_device" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to remove this device from the rack?');">
<input type="hidden" name="rack_device_id" value="{{ rd.id }}">
<button type="submit" class="ml-3 text-red-400 hover:text-red-600"><i class="fas fa-times"></i></button>
+15 -4
View File
@@ -11,20 +11,31 @@
<body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-3xl pt-20">
<div class="container py-8 max-w-md pt-20">
<h1 class="text-3xl font-bold mb-8 text-center">Racks</h1>
<div class="flex justify-center mb-6">
<a href="/rack/add" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg inline-block"><i class="fas fa-plus"></i> Add Rack</a>
</div>
<div class="space-y-6">
{% for rack in racks %}
<div class="bg-gray-800 rounded-lg shadow-md p-4 flex items-center justify-between">
<a href="/rack/{{ rack.id }}" class="block bg-gray-800 rounded-lg shadow-md p-4 flex items-start justify-between hover:ring-2 hover:ring-gray-400 transition group">
<div>
<h2 class="text-xl font-bold text-blue-200 mb-1">{{ rack.name }}</h2>
<div class="text-gray-400">Site: {{ rack.site }} | Height: {{ rack.height_u }}U</div>
</div>
<a href="/rack/{{ rack.id }}" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">View</a>
</div>
<div class="ml-6 flex-shrink-0">
<div class="relative flex items-center justify-center w-16 h-16 group">
<svg class="w-16 h-16 rotate-[-90deg]" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" stroke="#374151" stroke-width="12" fill="none" />
<circle cx="50" cy="50" r="42" stroke="#6b7280" stroke-width="12" fill="none" stroke-dasharray="264" stroke-dashoffset="{{ 264 - (264 * rack.percent_full / 100) }}" style="transition: stroke-dashoffset 0.5s;" />
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-sm font-bold text-white">{{ rack.percent_full }}%</span>
<span class="text-xs text-gray-300">Full</span>
</div>
</div>
</div>
</a>
{% else %}
<div class="text-center text-gray-400">No racks defined yet.</div>
{% endfor %}