Added rack stuff

This commit is contained in:
2025-06-08 09:26:46 +00:00
parent 64b5fb826d
commit f295f33809
6 changed files with 475 additions and 1 deletions
+22
View File
@@ -107,6 +107,25 @@ def init_db(app=None):
FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE CASCADE
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS Rack (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
site VARCHAR(255) NOT NULL,
height_u INTEGER NOT NULL
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS RackDevice (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
rack_id INTEGER NOT NULL,
device_id INTEGER NOT NULL,
position_u INTEGER NOT NULL,
side ENUM('front', 'back') NOT NULL,
FOREIGN KEY (rack_id) REFERENCES Rack(id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE
)
''')
cursor.execute('SELECT COUNT(*) FROM DeviceType')
if cursor.fetchone()[0] == 0:
cursor.executemany('INSERT INTO DeviceType (name, icon_class) VALUES (%s, %s)', [
@@ -133,5 +152,8 @@ 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()
+265 -1
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''')
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''')
devices = cursor.fetchall()
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
subnets = cursor.fetchall()
@@ -610,6 +610,261 @@ def register_routes(app):
site_devices[site].append({'id': device_id, 'name': name, 'description': description})
return render_with_user('devices_by_type.html', device_type=device_type, icon_class=icon_class, site_devices=site_devices)
@app.route('/racks')
@login_required
def racks():
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM Rack')
racks = cursor.fetchall()
return render_with_user('racks.html', racks=racks)
@app.route('/rack/add', methods=['GET', 'POST'])
@login_required
def add_rack():
from flask import current_app
if request.method == 'POST':
name = request.form['name']
site = request.form['site']
height_u = int(request.form['height_u'])
user_id = session.get('user_id')
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('INSERT INTO Rack (name, site, height_u) VALUES (%s, %s, %s)', (name, site, height_u))
rack_id = cursor.lastrowid
add_audit_log(user_id, 'add_rack', f"Added rack '{name}' at site '{site}' ({height_u}U)", conn=conn)
conn.commit()
return redirect(url_for('racks'))
return render_with_user('add_rack.html')
@app.route('/rack/<int:rack_id>')
@login_required
def rack(rack_id):
from flask import current_app, request
side = request.args.get('side', 'front')
with get_db_connection(current_app) as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
rack = cursor.fetchone()
if not rack:
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_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
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)
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'])
@login_required
def rack_add_device(rack_id):
device_id = int(request.form['device_id'])
position_u = int(request.form['position_u'])
side = request.form['side']
user_id = session.get('user_id')
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
rack = cursor.fetchone()
if not rack:
return 'Rack not found', 404
if position_u < 1 or position_u > rack['height_u']:
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_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
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')
all_devices = cursor.fetchall()
site_devices = [d for d in all_devices if d['device_type_id'] not in (2, 6)]
error = f"Invalid U position: {position_u}. Rack is {rack['height_u']}U tall."
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))
if cursor.fetchone()['COUNT(*)'] > 0:
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_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
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')
all_devices = cursor.fetchall()
site_devices = [d for d in all_devices if d['device_type_id'] not in (2, 6)]
error = f"U{position_u} on the {side} is already occupied."
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 RackDevice (rack_id, device_id, position_u, side) VALUES (%s, %s, %s, %s)', (rack_id, device_id, position_u, side))
cursor2 = conn.cursor()
cursor2.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
device_name = cursor2.fetchone()
cursor2.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
rack_name = cursor2.fetchone()
add_audit_log(user_id, 'rack_add_device', f"Assigned device '{device_name[0] if device_name else device_id}' to rack '{rack_name[0] if rack_name else rack_id}' U{position_u} ({side})", conn=conn)
conn.commit()
return redirect(url_for('rack', rack_id=rack_id))
@app.route('/rack/<int:rack_id>/add_nonnet_device', methods=['POST'])
@login_required
def rack_add_nonnet_device(rack_id):
device_name = request.form['device_name']
position_u = int(request.form['position_u'])
side = request.form['side']
user_id = session.get('user_id')
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
rack = cursor.fetchone()
if not rack:
return 'Rack not found', 404
if position_u < 1 or position_u > rack['height_u']:
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_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
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)')
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))
if cursor.fetchone()['COUNT(*)'] > 0:
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_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
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)')
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))
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))
@app.route('/rack/<int:rack_id>/remove_device', methods=['POST'])
@login_required
def rack_remove_device(rack_id):
rack_device_id = int(request.form['rack_device_id'])
user_id = session.get('user_id')
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,))
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']
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
add_audit_log(user_id, 'rack_remove_device', f"Removed device '{device_label}' from rack '{rack_label}' U{rd['position_u']} ({rd['side']})", conn=conn)
cursor.execute('DELETE FROM RackDevice WHERE id = %s', (rack_device_id,))
conn.commit()
return redirect(url_for('rack', rack_id=rack_id))
@app.route('/rack/<int:rack_id>/delete', methods=['POST'])
@login_required
def delete_rack(rack_id):
user_id = session.get('user_id')
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
rack_name = cursor.fetchone()
cursor.execute('DELETE FROM Rack WHERE id = %s', (rack_id,))
add_audit_log(user_id, 'delete_rack', f"Deleted rack '{rack_name[0] if rack_name else rack_id}'", conn=conn)
conn.commit()
return redirect(url_for('racks'))
@app.route('/rack/<int:rack_id>/export_csv')
@login_required
def export_rack_csv(rack_id):
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
rack = cursor.fetchone()
if not rack:
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_names = {}
if device_ids:
format_strings = ','.join(['%s'] * len(device_ids))
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
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')
output = StringIO()
writer = csv.writer(output)
writer.writerow([f"Rack: {rack['name']} ({rack['height_u']}U, {rack['site']})"])
writer.writerow([])
for side in ['front', 'back']:
writer.writerow([side.capitalize()])
writer.writerow(['U', 'Device'])
for u in range(rack['height_u'], 0, -1):
found = False
for rd in rack_devices:
if rd['position_u'] == u and rd['side'] == side:
writer.writerow([u, rd['device_name']])
found = True
break
if not found:
writer.writerow([u, ''])
writer.writerow([])
csv_bytes = output.getvalue().encode('utf-8')
output_bytes = BytesIO(csv_bytes)
output_bytes.seek(0)
filename = f"{rack['name']}_rack.csv".replace(' ', '_')
return send_file(
output_bytes,
mimetype='text/csv',
as_attachment=True,
download_name=filename
)
def get_current_user_name():
user_id = session.get('user_id')
if not user_id:
@@ -647,3 +902,12 @@ def register_routes(app):
app.add_url_rule('/subnet/<int:subnet_id>/export_csv', 'export_subnet_csv', export_subnet_csv)
app.add_url_rule('/subnet/<int:subnet_id>/dhcp', 'dhcp_pool', dhcp_pool, methods=['GET', 'POST'])
app.add_url_rule('/device_type_stats', 'device_type_stats', device_type_stats)
app.add_url_rule('/devices/type/<device_type>', 'devices_by_type', devices_by_type)
app.add_url_rule('/racks', 'racks', racks)
app.add_url_rule('/rack/add', 'add_rack', add_rack, methods=['GET', 'POST'])
app.add_url_rule('/rack/<int:rack_id>', 'rack', rack)
app.add_url_rule('/rack/<int:rack_id>/add_device', 'rack_add_device', rack_add_device, methods=['POST'])
app.add_url_rule('/rack/<int:rack_id>/add_nonnet_device', 'rack_add_nonnet_device', rack_add_nonnet_device, methods=['POST'])
app.add_url_rule('/rack/<int:rack_id>/remove_device', 'rack_remove_device', rack_remove_device, methods=['POST'])
app.add_url_rule('/rack/<int:rack_id>/delete', 'delete_rack', delete_rack, methods=['POST'])
app.add_url_rule('/rack/<int:rack_id>/export_csv', 'export_rack_csv', export_rack_csv)
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Rack</title>
<link rel="icon" type="image/png" href="/static/logo.png">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<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-md pt-20">
<h1 class="text-3xl font-bold mb-8 text-center">Add Rack</h1>
<form action="/rack/add" method="POST" class="space-y-6 bg-gray-800 rounded-lg shadow-md p-6">
<div>
<label for="name" class="block font-medium mb-1">Rack Name</label>
<input type="text" id="name" name="name" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600 w-full" required>
</div>
<div>
<label for="site" class="block font-medium mb-1">Site</label>
<input type="text" id="site" name="site" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600 w-full" required>
</div>
<div>
<label for="height_u" class="block font-medium mb-1">Height (U)</label>
<input type="number" id="height_u" name="height_u" min="1" max="60" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600 w-full" required>
</div>
<button type="submit" 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 w-full">Add Rack</button>
</form>
</div>
</div>
</body>
</html>
+2
View File
@@ -12,6 +12,7 @@
<a href="/admin" class="text-gray-200 hover:text-blue-400 font-medium">Admin</a>
<a href="/audit" class="text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
<a href="/device_type_stats" class="text-gray-200 hover:text-blue-400 font-medium">Stats</a>
<a href="/racks" class="text-gray-200 hover:text-blue-400 font-medium">Racks</a>
{% if current_user_name %}
<a href="/logout" class="text-gray-200 hover:text-blue-400 font-medium">Logout</a>
{% endif %}
@@ -27,6 +28,7 @@
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Admin</a>
<a href="/audit" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
<a href="/device_type_stats" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Stats</a>
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Racks</a>
{% if current_user_name %}
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Logout</a>
{% endif %}
+117
View File
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ rack.name }} - Rack View</title>
<link rel="icon" type="image/png" href="/static/logo.png">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<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-2xl pt-20">
<div class="flex 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>
<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>
</button>
</form>
<button type="button" id="export-csv" class="hidden sm:flex absolute right-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 export-csv-btn" title="Export as CSV" data-rack-id="{{ rack.id }}">
<i class="fas fa-file-csv fa-lg"></i>
</button>
<script>
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('export-csv');
if (btn) {
btn.addEventListener('click', function() {
window.location = '/rack/' + btn.getAttribute('data-rack-id') + '/export_csv';
});
}
});
</script>
</div>
<div class="flex justify-center gap-4 mb-6">
</div>
<div class="flex flex-col md:flex-row gap-4 mb-6 items-stretch">
<div class="flex gap-4">
<a href="?side=front" id="front-btn" class="rack-side-btn px-4 py-2 rounded-lg text-white font-semibold bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 {% if current_side == 'front' %}ring-2 ring-blue-400{% endif %}">Front</a>
<a href="?side=back" id="back-btn" class="rack-side-btn px-4 py-2 rounded-lg text-white font-semibold bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 {% if current_side == 'back' %}ring-2 ring-blue-400{% endif %}">Back</a>
</div>
<button id="show-nonnet-form" type="button" class="w-full md:w-auto 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 mb-2 md:mb-0 md:ml-auto flex-shrink-0"><i class="fas fa-plus"></i> Add Non-Networked Device</button>
</div>
<form action="/rack/{{ rack.id }}/add_device" method="POST" class="mb-6 bg-gray-800 rounded-lg p-4">
<div class="flex flex-col md:flex-row gap-2 mb-2">
<select name="device_id" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:flex-1" required>
<option value="" disabled selected>Select Device...</option>
{% for device in site_devices %}
{% if device.device_type_id != 2 and device.device_type_id != 6 %}
<option value="{{ device.id }}">{{ device.name }}</option>
{% endif %}
{% endfor %}
</select>
<input type="number" name="position_u" min="1" max="{{ rack.height_u }}" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-24" placeholder="U" required>
<select name="side" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-28" required>
<option value="front">Front</option>
<option value="back">Back</option>
</select>
<button type="submit" class="w-full md:w-auto 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">Add Device</button>
</div>
<div class="text-xs text-gray-400">To add a multi-U device, repeat for each U position.</div>
</form>
<form id="nonnet-form" action="/rack/{{ rack.id }}/add_nonnet_device" method="POST" class="hidden flex flex-col gap-2 mt-2 mb-6 bg-gray-800 rounded-lg p-4">
<div class="flex flex-col md:flex-row gap-2">
<input type="text" name="device_name" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:flex-1" placeholder="Device Name" required>
<input type="number" name="position_u" min="1" max="{{ rack.height_u }}" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-24" placeholder="U" required>
<select name="side" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-28" required>
<option value="front">Front</option>
<option value="back">Back</option>
</select>
<button type="submit" class="w-full md:w-auto 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">Add</button>
<button id="hide-nonnet-form" type="button" class="w-full md:w-auto 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 md:ml-0.5 mt-2 md:mt-0 flex-shrink-0"><i class="fas fa-times"></i></button>
</div>
<div class="text-xs text-gray-400 mt-2">Add a non-networked device.</div>
</form>
<script>
document.getElementById('show-nonnet-form').onclick = function() {
document.getElementById('nonnet-form').classList.remove('hidden');
this.classList.add('hidden');
};
document.getElementById('hide-nonnet-form').onclick = function() {
document.getElementById('nonnet-form').classList.add('hidden');
document.getElementById('show-nonnet-form').classList.remove('hidden');
};
</script>
{% if error %}
<div class="mb-4 p-3 bg-red-700 text-white rounded-lg text-center font-semibold">{{ error }}</div>
{% endif %}
<div id="rack-visual" class="bg-gray-800 rounded-lg shadow-md p-4">
<h2 class="text-xl font-bold mb-2 text-blue-200" id="rack-side-label">{{ current_side|capitalize }}</h2>
<div id="rack-units">
{% for u in range(rack.height_u, 0, -1) %}
<div class="flex items-center h-10 border-b border-gray-700 hover:bg-gray-700/30 transition group">
<span class="w-16 text-right text-gray-400 text-base font-mono pr-4">U{{ u }}</span>
<span class="flex-1 ml-4 flex items-center min-h-8">
{% 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>
<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>
</form>
{% set found = true %}
{% endif %}
{% endfor %}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Racks</title>
<link rel="icon" type="image/png" href="/static/logo.png">
<link href="/static/css/output.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<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">
<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">
<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>
{% else %}
<div class="text-center text-gray-400">No racks defined yet.</div>
{% endfor %}
</div>
</div>
</div>
</body>
</html>