feat: bulk operations

This commit is contained in:
2025-12-04 22:15:57 +00:00
parent f98e92da06
commit 2163be8f79
5 changed files with 581 additions and 18 deletions
+244 -15
View File
@@ -1004,7 +1004,27 @@ def register_routes(app):
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('''SELECT id, ip FROM IPAddress WHERE subnet_id = %s AND id NOT IN (SELECT ip_id FROM DeviceIPAddress) AND (hostname IS NULL OR hostname != 'DHCP')''', (subnet_id,))
available_ips = [{'id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
available_ips = cursor.fetchall()
# Filter out DHCP pool IPs
cursor.execute('SELECT start_ip, end_ip, excluded_ips FROM DHCPPool WHERE subnet_id = %s', (subnet_id,))
dhcp_row = cursor.fetchone()
if dhcp_row:
start_ip, end_ip, excluded_ips = dhcp_row
excluded_list = [x for x in (excluded_ips or '').replace(' ', '').split(',') if x]
in_range = False
filtered_ips = []
for ip_obj in available_ips:
ip = ip_obj[1]
if ip == start_ip:
in_range = True
if ip in excluded_list or not (in_range and ip not in excluded_list):
filtered_ips.append(ip_obj)
if ip == end_ip:
in_range = False
available_ips = filtered_ips
available_ips = [{'id': row[0], 'ip': row[1]} for row in available_ips]
return {'available_ips': available_ips}
@app.route('/rename_device', methods=['POST'])
@@ -2667,6 +2687,228 @@ def register_routes(app):
kwargs['current_user_name'] = get_current_user_name()
return render_template(*args, **kwargs)
# Bulk Operations
@app.route('/bulk', methods=['GET'])
@permission_required('view_devices')
def bulk_operations():
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('SELECT id, name FROM Device ORDER BY name')
devices = cursor.fetchall()
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
subnets = cursor.fetchall()
cursor.execute('SELECT id, name FROM Tag ORDER BY name')
tags = cursor.fetchall()
cursor.execute('SELECT id, name FROM DeviceType ORDER BY name')
device_types = cursor.fetchall()
return render_with_user('bulk_operations.html',
devices=devices,
subnets=subnets,
tags=tags,
device_types=device_types,
can_add_device_ip=has_permission('add_device_ip'),
can_add_device=has_permission('add_device'),
can_assign_device_tag=has_permission('assign_device_tag'),
can_export_subnet_csv=has_permission('export_subnet_csv'))
@app.route('/bulk/assign_ips', methods=['POST'])
@permission_required('add_device_ip')
def bulk_assign_ips():
device_id = request.form['device_id']
ip_ids = request.form.getlist('ip_ids[]')
user_name = get_current_user_name()
results = {'success': [], 'failed': []}
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
device = cursor.fetchone()
if not device:
return jsonify({'success': [], 'failed': [{'ip_id': 'all', 'reason': 'Device not found'}]})
device_name = device[0]
for ip_id in ip_ids:
try:
cursor.execute('SELECT ip, subnet_id FROM IPAddress WHERE id = %s', (ip_id,))
ip_row = cursor.fetchone()
if not ip_row:
results['failed'].append({'ip_id': ip_id, 'reason': 'IP not found'})
continue
ip, subnet_id = ip_row[0], ip_row[1]
# Check if IP is already assigned
cursor.execute('SELECT id FROM DeviceIPAddress WHERE ip_id = %s', (ip_id,))
if cursor.fetchone():
results['failed'].append({'ip_id': ip_id, 'ip': ip, 'reason': 'IP already assigned'})
continue
# Check if IP is in DHCP pool (using exact same logic as device_add_ip)
cursor.execute('SELECT start_ip, end_ip, excluded_ips FROM DHCPPool WHERE subnet_id = %s', (subnet_id,))
dhcp_row = cursor.fetchone()
if dhcp_row:
start_ip, end_ip, excluded_ips = dhcp_row
excluded_list = [x for x in (excluded_ips or '').replace(' ', '').split(',') if x]
if ip not in excluded_list:
cursor.execute('SELECT ip FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
all_ips = [row[0] for row in cursor.fetchall()]
in_range = False
reserved_for_dhcp = False
for candidate_ip in all_ips:
if candidate_ip == start_ip:
in_range = True
if in_range and candidate_ip == ip:
reserved_for_dhcp = True
break
if candidate_ip == end_ip:
in_range = False
if reserved_for_dhcp:
results['failed'].append({'ip_id': ip_id, 'ip': ip, 'reason': 'IP is reserved for DHCP'})
continue
cursor.execute('INSERT INTO DeviceIPAddress (device_id, ip_id) VALUES (%s, %s)', (device_id, ip_id))
cursor.execute('UPDATE IPAddress SET hostname = %s WHERE id = %s', (device_name, ip_id))
cursor.execute('SELECT name, cidr FROM Subnet WHERE id = %s', (subnet_id,))
subnet = cursor.fetchone()
subnet_name, subnet_cidr = subnet[0], subnet[1]
add_audit_log(session['user_id'], 'add_device_ip',
f"Assigned IP {ip} ({subnet_name} {subnet_cidr}) to device {device_name}",
subnet_id, conn=conn)
results['success'].append({'ip_id': ip_id, 'ip': ip})
except Exception as e:
results['failed'].append({'ip_id': ip_id, 'reason': str(e)})
conn.commit()
return jsonify(results)
@app.route('/bulk/create_devices', methods=['POST'])
@permission_required('add_device')
def bulk_create_devices():
device_names = request.form.get('device_names', '').strip().split('\n')
device_type_id = int(request.form.get('device_type', 1))
user_name = get_current_user_name()
results = {'success': [], 'failed': []}
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
for name in device_names:
name = name.strip()
if not name:
continue
try:
cursor.execute('INSERT INTO Device (name, device_type_id) VALUES (%s, %s)', (name, device_type_id))
device_id = cursor.lastrowid
add_audit_log(session['user_id'], 'add_device', f"Added device {name}", conn=conn)
results['success'].append({'name': name, 'id': device_id})
except Exception as e:
results['failed'].append({'name': name, 'reason': str(e)})
conn.commit()
logging.info(f"User {user_name} bulk created {len(results['success'])} devices.")
return jsonify(results)
@app.route('/bulk/assign_tags', methods=['POST'])
@permission_required('assign_device_tag')
def bulk_assign_tags():
device_ids = request.form.getlist('device_ids[]')
tag_ids = request.form.getlist('tag_ids[]')
user_name = get_current_user_name()
results = {'success': [], 'failed': []}
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
for device_id in device_ids:
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
device = cursor.fetchone()
if not device:
results['failed'].append({'device_id': device_id, 'reason': 'Device not found'})
continue
device_name = device[0]
for tag_id in tag_ids:
try:
cursor.execute('SELECT name FROM Tag WHERE id = %s', (tag_id,))
tag = cursor.fetchone()
if not tag:
continue
tag_name = tag[0]
cursor.execute('SELECT id FROM DeviceTag WHERE device_id = %s AND tag_id = %s', (device_id, tag_id))
if cursor.fetchone():
continue # Already assigned
cursor.execute('INSERT INTO DeviceTag (device_id, tag_id) VALUES (%s, %s)', (device_id, tag_id))
add_audit_log(session['user_id'], 'assign_device_tag',
f"Assigned tag '{tag_name}' to device '{device_name}'", conn=conn)
results['success'].append({'device_id': device_id, 'device_name': device_name, 'tag_id': tag_id, 'tag_name': tag_name})
except Exception as e:
results['failed'].append({'device_id': device_id, 'tag_id': tag_id, 'reason': str(e)})
conn.commit()
logging.info(f"User {user_name} bulk assigned tags to {len(device_ids)} devices.")
return jsonify(results)
@app.route('/bulk/export_subnets', methods=['POST'])
@permission_required('export_subnet_csv')
def bulk_export_subnets():
subnet_ids = request.form.getlist('subnet_ids[]')
from flask import current_app
output = StringIO()
writer = csv.writer(output)
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
for subnet_id in subnet_ids:
cursor.execute('SELECT id, name, cidr FROM Subnet WHERE id = %s', (subnet_id,))
subnet = cursor.fetchone()
if not subnet:
continue
writer.writerow([f"Subnet: {subnet[1]} ({subnet[2]})"])
writer.writerow(['IP Address', 'Hostname', 'Description'])
cursor.execute('SELECT * FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
ip_addresses = cursor.fetchall()
cursor.execute('SELECT id, name, description FROM Device')
devices = cursor.fetchall()
device_name_map = {name.lower(): (id, description) for id, name, description in devices}
for ip in ip_addresses:
hostname = ip[2]
device_description = None
if hostname:
match = device_name_map.get(hostname.lower())
if match:
device_description = match[1]
writer.writerow([ip[1] or '', hostname or '', device_description or ''])
writer.writerow([]) # Empty row between subnets
output.seek(0)
return send_file(BytesIO(output.getvalue().encode('utf-8')),
mimetype='text/csv',
as_attachment=True,
download_name='bulk_subnet_export.csv')
# API key regeneration route
@app.route('/regenerate_api_key', methods=['POST'])
@permission_required('manage_users')
def regenerate_api_key():
user_id = request.form['user_id']
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
new_api_key = generate_api_key()
cursor.execute('UPDATE User SET api_key = %s WHERE id = %s', (new_api_key, user_id))
conn.commit()
return redirect(url_for('users'))
app.add_url_rule('/login', 'login', login, methods=['GET', 'POST'])
app.add_url_rule('/logout', 'logout', logout)
app.add_url_rule('/', 'index', index)
@@ -2702,17 +2944,4 @@ def register_routes(app):
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)
app.add_url_rule('/help', 'help', help)
# API key regeneration route
@app.route('/regenerate_api_key', methods=['POST'])
@permission_required('manage_users')
def regenerate_api_key():
user_id = request.form['user_id']
from flask import current_app
with get_db_connection(current_app) as conn:
cursor = conn.cursor()
new_api_key = generate_api_key()
cursor.execute('UPDATE User SET api_key = %s WHERE id = %s', (new_api_key, user_id))
conn.commit()
return redirect(url_for('users'))
app.add_url_rule('/help', 'help', help)
+1 -1
View File
@@ -13,7 +13,7 @@
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-md pt-20">
<div class="flex items-center mb-6 relative">
<a href="javascript:window.history.back()" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 items-center justify-center rounded-full w-11 h-11"><i class="fas fa-arrow-left"></i></a>
<a href="/devices" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 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">Add Device</h1>
</div>
<form action="/add_device" method="POST" class="flex flex-col space-y-4">
+333
View File
@@ -0,0 +1,333 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bulk Operations</title>
<link rel="icon" type="image/png" href="{{ 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-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
{% include 'header.html' %}
<div class="flex-1 mx-4 py-8 pt-20">
<div class="container max-w-6xl mx-auto">
<div class="flex items-center mb-6 relative">
<a href="/devices" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer 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">Bulk Operations</h1>
</div>
<!-- Tabs -->
<div class="flex flex-wrap gap-2 mb-6 justify-center border-b border-gray-600">
<button onclick="showTab('assign-ips')" id="tab-assign-ips" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer active">Bulk IP Assignment</button>
<button onclick="showTab('create-devices')" id="tab-create-devices" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer">Bulk Device Creation</button>
<button onclick="showTab('assign-tags')" id="tab-assign-tags" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer">Bulk Tag Assignment</button>
<button onclick="showTab('export')" id="tab-export" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer">Bulk Export</button>
</div>
<!-- Bulk IP Assignment -->
<div id="panel-assign-ips" class="tab-panel bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
{% if can_add_device_ip %}
<h2 class="text-2xl font-bold mb-4">Bulk IP Assignment</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">Select a device and assign multiple IPs from a subnet. Hold Ctrl/Cmd to select multiple IPs.</p>
<form id="bulk-assign-ips-form" class="space-y-4">
<div>
<label class="block mb-2 font-medium">Select Device:</label>
<select id="bulk-device-select" name="device_id" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
<option value="">Select a device...</option>
{% for device in devices %}
<option value="{{ device[0] }}">{{ device[1] }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block mb-2 font-medium">Select Subnet:</label>
<select id="bulk-subnet-select" name="subnet_id" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
<option value="">Select a subnet...</option>
{% for subnet in subnets %}
<option value="{{ subnet[0] }}">{{ subnet[1] }} ({{ subnet[2] }}) - {{ subnet[3] or 'Unassigned' }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block mb-2 font-medium">Select IPs (hold Ctrl/Cmd to select multiple):</label>
<select id="bulk-ip-select" name="ip_ids[]" multiple size="15" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
<option value="" disabled>Select a subnet first...</option>
</select>
<p class="text-sm text-gray-500 mt-1">Selected: <span id="selected-ip-count">0</span> IPs</p>
</div>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Assign IPs</button>
</form>
<div id="assign-ips-result" class="mt-4 hidden"></div>
{% else %}
<p class="text-gray-500">You don't have permission to assign IPs to devices.</p>
{% endif %}
</div>
<!-- Bulk Device Creation -->
<div id="panel-create-devices" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
{% if can_add_device %}
<h2 class="text-2xl font-bold mb-4">Bulk Device Creation</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">Create multiple devices at once. Enter one device name per line.</p>
<form id="bulk-create-devices-form" class="space-y-4">
<div>
<label class="block mb-2 font-medium">Device Names (one per line):</label>
<textarea id="device-names" name="device_names" rows="10" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" placeholder="Device 1&#10;Device 2&#10;Device 3" required></textarea>
<p class="text-sm text-gray-500 mt-1">Enter device names, one per line</p>
</div>
<div>
<label class="block mb-2 font-medium">Device Type:</label>
<select name="device_type" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
{% for dtype in device_types %}
<option value="{{ dtype[0] }}">{{ dtype[1] }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Create Devices</button>
</form>
<div id="create-devices-result" class="mt-4 hidden"></div>
{% else %}
<p class="text-gray-500">You don't have permission to create devices.</p>
{% endif %}
</div>
<!-- Bulk Tag Assignment -->
<div id="panel-assign-tags" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
{% if can_assign_device_tag %}
<h2 class="text-2xl font-bold mb-4">Bulk Tag Assignment</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">Select multiple devices and assign one or more tags to them.</p>
<form id="bulk-assign-tags-form" class="space-y-4">
<div>
<label class="block mb-2 font-medium">Select Devices (hold Ctrl/Cmd to select multiple):</label>
<select id="bulk-tag-device-select" name="device_ids[]" multiple size="10" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full">
{% for device in devices %}
<option value="{{ device[0] }}">{{ device[1] }}</option>
{% endfor %}
</select>
<p class="text-sm text-gray-500 mt-1">Selected: <span id="selected-tag-device-count">0</span> devices</p>
</div>
<div>
<label class="block mb-2 font-medium">Select Tags (hold Ctrl/Cmd to select multiple):</label>
<select name="tag_ids[]" multiple size="5" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
{% for tag in tags %}
<option value="{{ tag[0] }}">{{ tag[1] }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Assign Tags</button>
</form>
<div id="assign-tags-result" class="mt-4 hidden"></div>
{% else %}
<p class="text-gray-500">You don't have permission to assign tags to devices.</p>
{% endif %}
</div>
<!-- Bulk Export -->
<div id="panel-export" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
{% if can_export_subnet_csv %}
<h2 class="text-2xl font-bold mb-4">Bulk Subnet Export</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">Select multiple subnets and export them to a single CSV file.</p>
<form id="bulk-export-form" method="POST" action="/bulk/export_subnets" class="space-y-4">
<div>
<label class="block mb-2 font-medium">Select Subnets (hold Ctrl/Cmd to select multiple):</label>
<select name="subnet_ids[]" multiple size="10" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
{% for subnet in subnets %}
<option value="{{ subnet[0] }}">{{ subnet[1] }} ({{ subnet[2] }}) - {{ subnet[3] or 'Unassigned' }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Export to CSV</button>
</form>
{% else %}
<p class="text-gray-500">You don't have permission to export subnets.</p>
{% endif %}
</div>
</div>
</div>
<script>
function showTab(tabName) {
// Hide all panels
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.add('hidden'));
// Remove active class from all tabs
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
// Show selected panel
document.getElementById('panel-' + tabName).classList.remove('hidden');
// Add active class to selected tab
document.getElementById('tab-' + tabName).classList.add('active');
}
// Update selected IP count
document.getElementById('bulk-ip-select')?.addEventListener('change', function() {
document.getElementById('selected-ip-count').textContent = this.selectedOptions.length;
});
document.getElementById('bulk-tag-device-select')?.addEventListener('change', function() {
document.getElementById('selected-tag-device-count').textContent = this.selectedOptions.length;
});
// Load available IPs when subnet changes
document.getElementById('bulk-subnet-select')?.addEventListener('change', function() {
const subnetId = this.value;
const ipSelect = document.getElementById('bulk-ip-select');
if (!subnetId) {
ipSelect.innerHTML = '<option value="" disabled>Select a subnet first...</option>';
document.getElementById('selected-ip-count').textContent = '0';
return;
}
ipSelect.innerHTML = '<option value="" disabled>Loading...</option>';
fetch(`/get_available_ips?subnet_id=${subnetId}`)
.then(response => response.json())
.then(data => {
ipSelect.innerHTML = '';
if (data.available_ips.length === 0) {
ipSelect.innerHTML = '<option value="" disabled>No available IPs in this subnet</option>';
} else {
data.available_ips.forEach(ip => {
const option = document.createElement('option');
option.value = ip.id;
option.textContent = ip.ip;
ipSelect.appendChild(option);
});
}
document.getElementById('selected-ip-count').textContent = '0';
})
.catch(() => {
ipSelect.innerHTML = '<option value="" disabled>Error loading IPs</option>';
});
});
// Bulk IP Assignment
document.getElementById('bulk-assign-ips-form')?.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const resultDiv = document.getElementById('assign-ips-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = '<p class="text-blue-500">Processing...</p>';
fetch('/bulk/assign_ips', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
let html = '<div class="space-y-2">';
if (data.success.length > 0) {
html += `<div class="text-green-600 dark:text-green-400"><strong>Successfully assigned ${data.success.length} IP(s):</strong><ul class="list-disc list-inside mt-2">`;
data.success.forEach(item => {
html += `<li>${item.ip}</li>`;
});
html += '</ul></div>';
}
if (data.failed.length > 0) {
html += `<div class="text-red-600 dark:text-red-400"><strong>Failed ${data.failed.length} assignment(s):</strong><ul class="list-disc list-inside mt-2">`;
data.failed.forEach(item => {
const ipDisplay = item.ip ? ` (${item.ip})` : '';
html += `<li>IP ID ${item.ip_id}${ipDisplay}: ${item.reason}</li>`;
});
html += '</ul></div>';
}
html += '</div>';
resultDiv.innerHTML = html;
// Reload IP list if successful
if (data.success.length > 0) {
const subnetSelect = document.getElementById('bulk-subnet-select');
if (subnetSelect.value) {
subnetSelect.dispatchEvent(new Event('change'));
}
}
})
.catch(error => {
resultDiv.innerHTML = `<p class="text-red-600">Error: ${error.message}</p>`;
});
});
// Bulk Device Creation
document.getElementById('bulk-create-devices-form')?.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const resultDiv = document.getElementById('create-devices-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = '<p class="text-blue-500">Processing...</p>';
fetch('/bulk/create_devices', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
let html = '<div class="space-y-2">';
if (data.success.length > 0) {
html += `<div class="text-green-600 dark:text-green-400"><strong>Successfully created ${data.success.length} device(s):</strong><ul class="list-disc list-inside mt-2">`;
data.success.forEach(item => {
html += `<li>${item.name}</li>`;
});
html += '</ul></div>';
}
if (data.failed.length > 0) {
html += `<div class="text-red-600 dark:text-red-400"><strong>Failed ${data.failed.length} creation(s):</strong><ul class="list-disc list-inside mt-2">`;
data.failed.forEach(item => {
html += `<li>${item.name}: ${item.reason}</li>`;
});
html += '</ul></div>';
}
html += '</div>';
resultDiv.innerHTML = html;
if (data.success.length > 0) {
setTimeout(() => window.location.reload(), 2000);
}
})
.catch(error => {
resultDiv.innerHTML = `<p class="text-red-600">Error: ${error.message}</p>`;
});
});
// Bulk Tag Assignment
document.getElementById('bulk-assign-tags-form')?.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const resultDiv = document.getElementById('assign-tags-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = '<p class="text-blue-500">Processing...</p>';
fetch('/bulk/assign_tags', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
let html = '<div class="space-y-2">';
if (data.success.length > 0) {
html += `<div class="text-green-600 dark:text-green-400"><strong>Successfully assigned ${data.success.length} tag(s):</strong><ul class="list-disc list-inside mt-2">`;
data.success.forEach(item => {
html += `<li>${item.device_name}: ${item.tag_name}</li>`;
});
html += '</ul></div>';
}
if (data.failed.length > 0) {
html += `<div class="text-red-600 dark:text-red-400"><strong>Failed ${data.failed.length} assignment(s):</strong><ul class="list-disc list-inside mt-2">`;
data.failed.forEach(item => {
html += `<li>Device ID ${item.device_id}, Tag ID ${item.tag_id}: ${item.reason}</li>`;
});
html += '</ul></div>';
}
html += '</div>';
resultDiv.innerHTML = html;
})
.catch(error => {
resultDiv.innerHTML = `<p class="text-red-600">Error: ${error.message}</p>`;
});
});
</script>
<style>
.tab-btn.active {
background-color: rgb(156 163 175);
color: white;
}
.dark .tab-btn.active {
background-color: rgb(63 63 70);
}
</style>
</body>
</html>
+1 -1
View File
@@ -11,7 +11,7 @@
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark: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 w-auto min-w-[20rem] max-w-2xl pt-20">
<div class="container py-8 w-auto min-w-2xl pt-20">
<div class="flex items-center mb-8 relative justify-between gap-4">
<a href="/devices" class="hidden sm:flex bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 flex items-center justify-center rounded-full w-11 h-11 shrink-0"><i class="fas fa-arrow-left"></i></a>
<h1 class="text-3xl font-bold text-center flex-1 min-w-0 truncate">{{ device.name }}</h1>
+2 -1
View File
@@ -14,8 +14,9 @@
<div class="flex-1 flex items-center justify-center mx-4">
<div class="container py-8 max-w-4xl pt-20">
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
<div class="flex flex-row justify-center gap-4 mb-6">
<div class="flex flex-row justify-center gap-4 mb-6 flex-wrap">
<a href="/add_device" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add New Device</a>
<a href="/bulk" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Bulk Operations</a>
<a href="/device_type_stats" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">View Device Stats</a>
</div>