refactor: 🎨 remove caching #48
@@ -146,8 +146,6 @@ Subnets must be `/24` or smaller (prefix length ≥ 24).
|
|||||||
|
|
||||||
## Racks
|
## Racks
|
||||||
|
|
||||||
Requires the racks feature to be enabled.
|
|
||||||
|
|
||||||
| Method | Endpoint | Permission | Description |
|
| Method | Endpoint | Permission | Description |
|
||||||
|--------|----------|------------|-------------|
|
|--------|----------|------------|-------------|
|
||||||
| GET | `/api/v1/racks` | `view_racks` | List all racks |
|
| GET | `/api/v1/racks` | `view_racks` | List all racks |
|
||||||
@@ -161,8 +159,6 @@ Requires the racks feature to be enabled.
|
|||||||
|
|
||||||
## Tags
|
## Tags
|
||||||
|
|
||||||
Requires the device tags feature to be enabled.
|
|
||||||
|
|
||||||
| Method | Endpoint | Permission | Description |
|
| Method | Endpoint | Permission | Description |
|
||||||
|--------|----------|------------|-------------|
|
|--------|----------|------------|-------------|
|
||||||
| GET | `/api/v1/tags` | `view_tags` | List all tags |
|
| GET | `/api/v1/tags` | `view_tags` | List all tags |
|
||||||
@@ -206,7 +202,6 @@ Requires the device tags feature to be enabled.
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"help_text": "Optional help text",
|
"help_text": "Optional help text",
|
||||||
"display_order": 0,
|
"display_order": 0,
|
||||||
"searchable": true,
|
|
||||||
"validation_rules": {
|
"validation_rules": {
|
||||||
"max_length": 32
|
"max_length": 32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from ipaddress import ip_network, ip_address, IPv4Address, IPv6Address
|
|||||||
|
|
||||||
import pyotp
|
import pyotp
|
||||||
import qrcode
|
import qrcode
|
||||||
import requests
|
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from flask import (
|
from flask import (
|
||||||
@@ -158,23 +157,6 @@ def permission_required(permission_name):
|
|||||||
return decorated_function
|
return decorated_function
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def is_feature_enabled(feature_key, conn=None):
|
|
||||||
"""Check if a feature flag is enabled"""
|
|
||||||
close_conn = False
|
|
||||||
if conn is None:
|
|
||||||
from flask import current_app
|
|
||||||
conn = get_db_connection(current_app)
|
|
||||||
close_conn = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute('SELECT enabled FROM FeatureFlags WHERE feature_key = %s', (feature_key,))
|
|
||||||
result = cursor.fetchone()
|
|
||||||
# Default to True if feature flag doesn't exist (backward compatibility)
|
|
||||||
return result[0] if result else True
|
|
||||||
finally:
|
|
||||||
if close_conn:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def get_user_from_api_key(api_key):
|
def get_user_from_api_key(api_key):
|
||||||
"""Get user from API key"""
|
"""Get user from API key"""
|
||||||
@@ -546,7 +528,7 @@ def get_custom_fields_for_entity(entity_type, entity_id, conn=None):
|
|||||||
# Get field definitions for this entity type
|
# Get field definitions for this entity type
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT id, entity_type, name, field_key, field_type, required,
|
SELECT id, entity_type, name, field_key, field_type, required,
|
||||||
default_value, help_text, display_order, validation_rules, searchable
|
default_value, help_text, display_order, validation_rules
|
||||||
FROM CustomFieldDefinition
|
FROM CustomFieldDefinition
|
||||||
WHERE entity_type = %s
|
WHERE entity_type = %s
|
||||||
ORDER BY display_order, name
|
ORDER BY display_order, name
|
||||||
@@ -1313,13 +1295,12 @@ def devices():
|
|||||||
tag_filter = request.args.get('tag')
|
tag_filter = request.args.get('tag')
|
||||||
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if device tags feature is enabled
|
tag_filter = request.args.get('tag')
|
||||||
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
|
||||||
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Base device query
|
# Base device query
|
||||||
if tag_filter and tags_enabled:
|
if tag_filter:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT DISTINCT d.id, d.name, dt.icon_class
|
SELECT DISTINCT d.id, d.name, dt.icon_class
|
||||||
FROM Device d
|
FROM Device d
|
||||||
@@ -1341,10 +1322,9 @@ def devices():
|
|||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
device_ips.setdefault(row[0], []).append((row[1], row[2]))
|
device_ips.setdefault(row[0], []).append((row[1], row[2]))
|
||||||
|
|
||||||
# Get tags for each device (only if tags feature is enabled)
|
# Get tags for each device
|
||||||
device_tags = {}
|
device_tags = {}
|
||||||
all_tag_names = []
|
all_tag_names = []
|
||||||
if tags_enabled:
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT t.id, t.name, t.color
|
SELECT t.id, t.name, t.color
|
||||||
@@ -1355,7 +1335,6 @@ def devices():
|
|||||||
''', (device[0],))
|
''', (device[0],))
|
||||||
device_tags[device[0]] = [{'id': row[0], 'name': row[1], 'color': row[2]} for row in cursor.fetchall()]
|
device_tags[device[0]] = [{'id': row[0], 'name': row[1], 'color': row[2]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
# Get all available tags for filtering
|
|
||||||
cursor.execute('SELECT DISTINCT name FROM Tag ORDER BY name')
|
cursor.execute('SELECT DISTINCT name FROM Tag ORDER BY name')
|
||||||
all_tag_names = [row[0] for row in cursor.fetchall()]
|
all_tag_names = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
@@ -1424,11 +1403,7 @@ def device(device_id):
|
|||||||
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
||||||
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
# Get device tags (only if tags feature is enabled)
|
# Get device tags
|
||||||
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
|
||||||
device_tags = []
|
|
||||||
all_tags = []
|
|
||||||
if tags_enabled:
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT t.id, t.name, t.color
|
SELECT t.id, t.name, t.color
|
||||||
FROM DeviceTag dt
|
FROM DeviceTag dt
|
||||||
@@ -1438,7 +1413,6 @@ def device(device_id):
|
|||||||
''', (device_id,))
|
''', (device_id,))
|
||||||
device_tags = [{'id': row[0], 'name': row[1], 'color': row[2]} for row in cursor.fetchall()]
|
device_tags = [{'id': row[0], 'name': row[1], 'color': row[2]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
# Get all available tags
|
|
||||||
cursor.execute('SELECT id, name, color FROM Tag ORDER BY name')
|
cursor.execute('SELECT id, name, color FROM Tag ORDER BY name')
|
||||||
all_tags = [{'id': row[0], 'name': row[1], 'color': row[2]} for row in cursor.fetchall()]
|
all_tags = [{'id': row[0], 'name': row[1], 'color': row[2]} for row in cursor.fetchall()]
|
||||||
available_ips_by_subnet = {}
|
available_ips_by_subnet = {}
|
||||||
@@ -1548,8 +1522,6 @@ def device_delete_ip(device_id):
|
|||||||
def device_assign_tag(device_id):
|
def device_assign_tag(device_id):
|
||||||
tag_id = request.form['tag_id']
|
tag_id = request.form['tag_id']
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
try:
|
try:
|
||||||
assign_tag_to_device(conn, device_id, tag_id, session['user_id'])
|
assign_tag_to_device(conn, device_id, tag_id, session['user_id'])
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -1562,8 +1534,6 @@ def device_assign_tag(device_id):
|
|||||||
def device_remove_tag(device_id):
|
def device_remove_tag(device_id):
|
||||||
tag_id = request.form['tag_id']
|
tag_id = request.form['tag_id']
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
try:
|
try:
|
||||||
remove_tag_from_device(conn, device_id, tag_id, session['user_id'])
|
remove_tag_from_device(conn, device_id, tag_id, session['user_id'])
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -1614,15 +1584,11 @@ def subnet(subnet_id):
|
|||||||
'vlan_description': subnet[4] if len(subnet) > 4 else None,
|
'vlan_description': subnet[4] if len(subnet) > 4 else None,
|
||||||
'vlan_notes': subnet[5] if len(subnet) > 5 else None
|
'vlan_notes': subnet[5] if len(subnet) > 5 else None
|
||||||
}
|
}
|
||||||
# Check if IP address notes feature is enabled
|
|
||||||
ip_notes_enabled = is_feature_enabled('ip_address_notes', conn=conn)
|
|
||||||
|
|
||||||
return render_with_user('subnet.html', subnet=subnet_dict,
|
return render_with_user('subnet.html', subnet=subnet_dict,
|
||||||
ip_addresses=ip_addresses_with_device,
|
ip_addresses=ip_addresses_with_device,
|
||||||
utilization=utilization_stats,
|
utilization=utilization_stats,
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
can_edit_subnet=has_permission('edit_subnet'),
|
can_edit_subnet=has_permission('edit_subnet'))
|
||||||
ip_notes_enabled=ip_notes_enabled)
|
|
||||||
|
|
||||||
@app.route('/add_subnet', methods=['POST'])
|
@app.route('/add_subnet', methods=['POST'])
|
||||||
@permission_required('add_subnet')
|
@permission_required('add_subnet')
|
||||||
@@ -1750,49 +1716,14 @@ def admin():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get feature flags (inside the connection context)
|
|
||||||
cursor.execute('SELECT feature_key, enabled, description FROM FeatureFlags ORDER BY feature_key')
|
|
||||||
feature_flags = []
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
feature_flags.append({
|
|
||||||
'key': row[0],
|
|
||||||
'enabled': bool(row[1]),
|
|
||||||
'description': row[2]
|
|
||||||
})
|
|
||||||
|
|
||||||
result_data = {
|
result_data = {
|
||||||
'subnets': subnets,
|
'subnets': subnets,
|
||||||
'can_add_subnet': has_permission('add_subnet'),
|
'can_add_subnet': has_permission('add_subnet'),
|
||||||
'can_edit_subnet': has_permission('edit_subnet'),
|
'can_edit_subnet': has_permission('edit_subnet'),
|
||||||
'can_delete_subnet': has_permission('delete_subnet'),
|
'can_delete_subnet': has_permission('delete_subnet'),
|
||||||
'feature_flags': feature_flags
|
|
||||||
}
|
}
|
||||||
return render_with_user('admin.html', **result_data)
|
return render_with_user('admin.html', **result_data)
|
||||||
|
|
||||||
@app.route('/admin/feature_flags', methods=['POST'])
|
|
||||||
@permission_required('manage_users')
|
|
||||||
def update_feature_flags():
|
|
||||||
"""Update feature flags"""
|
|
||||||
user_name = get_current_user_name()
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
# Get all feature flags
|
|
||||||
cursor.execute('SELECT feature_key FROM FeatureFlags')
|
|
||||||
all_features = [row[0] for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
# Update each feature flag based on form data
|
|
||||||
for feature_key in all_features:
|
|
||||||
enabled = request.form.get(f'feature_{feature_key}') == 'on'
|
|
||||||
cursor.execute('UPDATE FeatureFlags SET enabled = %s WHERE feature_key = %s',
|
|
||||||
(enabled, feature_key))
|
|
||||||
logging.info(f"User {user_name} {'enabled' if enabled else 'disabled'} feature '{feature_key}'")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# Clear admin cache to refresh feature flags
|
|
||||||
|
|
||||||
return redirect(url_for('admin'))
|
|
||||||
|
|
||||||
@app.route('/account', methods=['GET'])
|
@app.route('/account', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2066,9 +1997,6 @@ def users():
|
|||||||
def tags():
|
def tags():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
error = None
|
error = None
|
||||||
|
|
||||||
@@ -2158,15 +2086,8 @@ def custom_fields():
|
|||||||
error = 'You do not have permission to add custom fields.'
|
error = 'You do not have permission to add custom fields.'
|
||||||
else:
|
else:
|
||||||
entity_type = request.form.get('entity_type', '').strip()
|
entity_type = request.form.get('entity_type', '').strip()
|
||||||
# Debug logging
|
|
||||||
logging.info(f"Received entity_type: '{entity_type}' (type: {type(entity_type)})")
|
|
||||||
logging.info(f"Form data keys: {list(request.form.keys())}")
|
|
||||||
if not entity_type or entity_type not in ['device', 'subnet']:
|
if not entity_type or entity_type not in ['device', 'subnet']:
|
||||||
# Try to get from form data directly
|
error = 'Invalid entity type. Must be "device" or "subnet".'
|
||||||
entity_type_raw = request.form.get('entity_type')
|
|
||||||
logging.error(f"Invalid entity_type received: '{entity_type}' (raw: '{entity_type_raw}')")
|
|
||||||
# Show more helpful error
|
|
||||||
error = f'Invalid entity type: "{entity_type}". Must be "device" or "subnet". Please try again.'
|
|
||||||
else:
|
else:
|
||||||
name = request.form['name'].strip()
|
name = request.form['name'].strip()
|
||||||
field_key = request.form.get('field_key', '').strip()
|
field_key = request.form.get('field_key', '').strip()
|
||||||
@@ -2175,7 +2096,6 @@ def custom_fields():
|
|||||||
default_value = request.form.get('default_value', '').strip()
|
default_value = request.form.get('default_value', '').strip()
|
||||||
help_text = request.form.get('help_text', '').strip()
|
help_text = request.form.get('help_text', '').strip()
|
||||||
display_order = int(request.form.get('display_order', 0))
|
display_order = int(request.form.get('display_order', 0))
|
||||||
searchable = 'searchable' in request.form
|
|
||||||
|
|
||||||
# Generate field_key from name if not provided
|
# Generate field_key from name if not provided
|
||||||
if not field_key:
|
if not field_key:
|
||||||
@@ -2193,10 +2113,10 @@ def custom_fields():
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO CustomFieldDefinition
|
INSERT INTO CustomFieldDefinition
|
||||||
(entity_type, name, field_key, field_type, required, default_value,
|
(entity_type, name, field_key, field_type, required, default_value,
|
||||||
help_text, display_order, validation_rules, searchable)
|
help_text, display_order, validation_rules)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
''', (entity_type, name, field_key, field_type, required, default_value,
|
''', (entity_type, name, field_key, field_type, required, default_value,
|
||||||
help_text, display_order, validation_rules_json, searchable))
|
help_text, display_order, validation_rules_json))
|
||||||
add_audit_log(session['user_id'], 'add_custom_field',
|
add_audit_log(session['user_id'], 'add_custom_field',
|
||||||
f"Added custom field '{name}' for {entity_type}", conn=conn)
|
f"Added custom field '{name}' for {entity_type}", conn=conn)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -2216,7 +2136,6 @@ def custom_fields():
|
|||||||
default_value = request.form.get('default_value', '').strip()
|
default_value = request.form.get('default_value', '').strip()
|
||||||
help_text = request.form.get('help_text', '').strip()
|
help_text = request.form.get('help_text', '').strip()
|
||||||
display_order = int(request.form.get('display_order', 0))
|
display_order = int(request.form.get('display_order', 0))
|
||||||
searchable = 'searchable' in request.form
|
|
||||||
|
|
||||||
# Build validation_rules JSON
|
# Build validation_rules JSON
|
||||||
validation_rules_json = build_validation_rules_from_form(request.form, field_type)
|
validation_rules_json = build_validation_rules_from_form(request.form, field_type)
|
||||||
@@ -2232,10 +2151,10 @@ def custom_fields():
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE CustomFieldDefinition
|
UPDATE CustomFieldDefinition
|
||||||
SET name = %s, field_type = %s, required = %s, default_value = %s,
|
SET name = %s, field_type = %s, required = %s, default_value = %s,
|
||||||
help_text = %s, display_order = %s, validation_rules = %s, searchable = %s
|
help_text = %s, display_order = %s, validation_rules = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
''', (name, field_type, required, default_value, help_text,
|
''', (name, field_type, required, default_value, help_text,
|
||||||
display_order, validation_rules_json, searchable, field_id))
|
display_order, validation_rules_json, field_id))
|
||||||
add_audit_log(session['user_id'], 'edit_custom_field',
|
add_audit_log(session['user_id'], 'edit_custom_field',
|
||||||
f"Updated custom field '{name}'", conn=conn)
|
f"Updated custom field '{name}'", conn=conn)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -2274,7 +2193,7 @@ def custom_fields():
|
|||||||
# Get all custom fields grouped by entity type
|
# Get all custom fields grouped by entity type
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT id, entity_type, name, field_key, field_type, required,
|
SELECT id, entity_type, name, field_key, field_type, required,
|
||||||
default_value, help_text, display_order, validation_rules, searchable
|
default_value, help_text, display_order, validation_rules
|
||||||
FROM CustomFieldDefinition
|
FROM CustomFieldDefinition
|
||||||
ORDER BY entity_type, display_order, name
|
ORDER BY entity_type, display_order, name
|
||||||
''')
|
''')
|
||||||
@@ -2305,26 +2224,6 @@ def custom_fields():
|
|||||||
can_manage=has_permission('manage_custom_fields'),
|
can_manage=has_permission('manage_custom_fields'),
|
||||||
active_tab=active_tab)
|
active_tab=active_tab)
|
||||||
|
|
||||||
@app.route('/custom_fields/<entity_type>')
|
|
||||||
@permission_required('view_custom_fields')
|
|
||||||
def custom_fields_by_type(entity_type):
|
|
||||||
"""Get custom fields for a specific entity type"""
|
|
||||||
if entity_type not in ['device', 'subnet']:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
|
||||||
cursor.execute('''
|
|
||||||
SELECT id, entity_type, name, field_key, field_type, required,
|
|
||||||
default_value, help_text, display_order, validation_rules, searchable
|
|
||||||
FROM CustomFieldDefinition
|
|
||||||
WHERE entity_type = %s
|
|
||||||
ORDER BY display_order, name
|
|
||||||
''', (entity_type,))
|
|
||||||
fields = cursor.fetchall()
|
|
||||||
|
|
||||||
return jsonify({'fields': fields})
|
|
||||||
|
|
||||||
@app.route('/audit')
|
@app.route('/audit')
|
||||||
@permission_required('view_audit')
|
@permission_required('view_audit')
|
||||||
@@ -2370,60 +2269,6 @@ def export_audit_csv():
|
|||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
return csv_attachment(logs, ['User', 'Action', 'Details', 'Subnet', 'Timestamp'], f'audit_logs_{timestamp}.csv')
|
return csv_attachment(logs, ['User', 'Action', 'Details', 'Subnet', 'Timestamp'], f'audit_logs_{timestamp}.csv')
|
||||||
|
|
||||||
@app.route('/check_update')
|
|
||||||
@login_required
|
|
||||||
def check_update():
|
|
||||||
"""Check for available updates from Gitea"""
|
|
||||||
try:
|
|
||||||
# Get current version from environment
|
|
||||||
current_version = os.environ.get('VERSION', 'unknown').lstrip('v')
|
|
||||||
|
|
||||||
# Fetch latest release from Gitea
|
|
||||||
response = requests.get('https://git.jdbnet.co.uk/api/v1/repos/jamie/ipam/releases/latest', timeout=5)
|
|
||||||
if response.status_code != 200:
|
|
||||||
return jsonify({'error': 'Failed to fetch release information'}), 500
|
|
||||||
|
|
||||||
release_data = response.json()
|
|
||||||
latest_version = release_data.get('tag_name', '').lstrip('v')
|
|
||||||
|
|
||||||
# Compare versions using semantic versioning
|
|
||||||
result = {'update_available': False}
|
|
||||||
if latest_version and latest_version != current_version:
|
|
||||||
# Simple semantic version comparison
|
|
||||||
def version_tuple(v):
|
|
||||||
"""Convert version string to tuple for comparison"""
|
|
||||||
parts = v.split('.')
|
|
||||||
return tuple(int(x) if x.isdigit() else 0 for x in parts[:3])
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_tuple = version_tuple(current_version)
|
|
||||||
latest_tuple = version_tuple(latest_version)
|
|
||||||
# Only show update if latest is actually newer
|
|
||||||
if latest_tuple > current_tuple:
|
|
||||||
result = {
|
|
||||||
'update_available': True,
|
|
||||||
'current_version': current_version,
|
|
||||||
'latest_version': latest_version,
|
|
||||||
'release_url': release_data.get('html_url', '')
|
|
||||||
}
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
# Fallback to string comparison if parsing fails
|
|
||||||
if latest_version != current_version:
|
|
||||||
result = {
|
|
||||||
'update_available': True,
|
|
||||||
'current_version': current_version,
|
|
||||||
'latest_version': latest_version,
|
|
||||||
'release_url': release_data.get('html_url', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
logging.error(f"Error checking for updates: {e}")
|
|
||||||
return jsonify({'error': 'Failed to check for updates'}), 500
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Unexpected error checking for updates: {e}")
|
|
||||||
return jsonify({'error': 'Failed to check for updates'}), 500
|
|
||||||
|
|
||||||
@app.route('/get_available_ips')
|
@app.route('/get_available_ips')
|
||||||
@permission_required('view_device')
|
@permission_required('view_device')
|
||||||
@@ -2756,9 +2601,6 @@ def devices_by_tag(tag_id):
|
|||||||
def racks():
|
def racks():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT * FROM Rack')
|
cursor.execute('SELECT * FROM Rack')
|
||||||
racks = cursor.fetchall()
|
racks = cursor.fetchall()
|
||||||
@@ -2778,10 +2620,6 @@ def racks():
|
|||||||
@permission_required('add_rack')
|
@permission_required('add_rack')
|
||||||
def add_rack():
|
def add_rack():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
name = request.form['name']
|
name = request.form['name']
|
||||||
site = request.form['site']
|
site = request.form['site']
|
||||||
@@ -2802,8 +2640,6 @@ def add_rack():
|
|||||||
def rack(rack_id):
|
def rack(rack_id):
|
||||||
side = request.args.get('side', 'front')
|
side = request.args.get('side', 'front')
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
return render_rack_page(conn, rack_id, side)
|
return render_rack_page(conn, rack_id, side)
|
||||||
|
|
||||||
@app.route('/rack/<int:rack_id>/add_device', methods=['POST'])
|
@app.route('/rack/<int:rack_id>/add_device', methods=['POST'])
|
||||||
@@ -2814,8 +2650,6 @@ def rack_add_device(rack_id):
|
|||||||
side = request.form['side']
|
side = request.form['side']
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
_, placement_error = check_rack_placement(cursor, rack_id, position_u, side)
|
_, placement_error = check_rack_placement(cursor, rack_id, position_u, side)
|
||||||
if placement_error == 'Rack not found':
|
if placement_error == 'Rack not found':
|
||||||
@@ -2848,8 +2682,6 @@ def rack_add_nonnet_device(rack_id):
|
|||||||
side = request.form['side']
|
side = request.form['side']
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
_, placement_error = check_rack_placement(cursor, rack_id, position_u, side)
|
_, placement_error = check_rack_placement(cursor, rack_id, position_u, side)
|
||||||
if placement_error == 'Rack not found':
|
if placement_error == 'Rack not found':
|
||||||
@@ -2876,9 +2708,6 @@ def rack_remove_device(rack_id):
|
|||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT device_id, nonnet_device_name, 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()
|
rd = cursor.fetchone()
|
||||||
@@ -2903,9 +2732,6 @@ def delete_rack(rack_id):
|
|||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack_name = cursor.fetchone()
|
rack_name = cursor.fetchone()
|
||||||
@@ -2920,9 +2746,6 @@ def delete_rack(rack_id):
|
|||||||
def export_rack_csv(rack_id):
|
def export_rack_csv(rack_id):
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3546,9 +3369,6 @@ def api_racks():
|
|||||||
"""Get all racks"""
|
"""Get all racks"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
return jsonify({'error': 'Racks feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, site, height_u FROM Rack ORDER BY site, name')
|
cursor.execute('SELECT id, name, site, height_u FROM Rack ORDER BY site, name')
|
||||||
racks = cursor.fetchall()
|
racks = cursor.fetchall()
|
||||||
@@ -3574,9 +3394,6 @@ def api_rack(rack_id):
|
|||||||
"""Get a specific rack"""
|
"""Get a specific rack"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
return jsonify({'error': 'Racks feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, site, height_u FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT id, name, site, height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3613,9 +3430,6 @@ def api_add_rack():
|
|||||||
return jsonify({'error': 'height_u must be greater than zero'}), 400
|
return jsonify({'error': 'height_u must be greater than zero'}), 400
|
||||||
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
return jsonify({'error': 'Racks feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('INSERT INTO Rack (name, site, height_u) VALUES (%s, %s, %s)', (name, site, height_u))
|
cursor.execute('INSERT INTO Rack (name, site, height_u) VALUES (%s, %s, %s)', (name, site, height_u))
|
||||||
rack_id = cursor.lastrowid
|
rack_id = cursor.lastrowid
|
||||||
@@ -3629,9 +3443,6 @@ def api_delete_rack(rack_id):
|
|||||||
"""Delete a rack"""
|
"""Delete a rack"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
return jsonify({'error': 'Racks feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3674,9 +3485,6 @@ def api_add_device_to_rack(rack_id):
|
|||||||
return jsonify({'error': 'device_id must be an integer'}), 400
|
return jsonify({'error': 'device_id must be an integer'}), 400
|
||||||
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
return jsonify({'error': 'Racks feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT name, height_u FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT name, height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3736,9 +3544,6 @@ def api_remove_device_from_rack(rack_id, rack_device_id):
|
|||||||
"""Remove a device from a rack"""
|
"""Remove a device from a rack"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if racks feature is enabled
|
|
||||||
if not is_feature_enabled('racks', conn=conn):
|
|
||||||
return jsonify({'error': 'Racks feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT rd.device_id, rd.nonnet_device_name, rd.position_u, rd.side,
|
SELECT rd.device_id, rd.nonnet_device_name, rd.position_u, rd.side,
|
||||||
@@ -3778,7 +3583,7 @@ def api_custom_fields_by_type(entity_type):
|
|||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT id, entity_type, name, field_key, field_type, required,
|
SELECT id, entity_type, name, field_key, field_type, required,
|
||||||
default_value, help_text, display_order, validation_rules, searchable
|
default_value, help_text, display_order, validation_rules
|
||||||
FROM CustomFieldDefinition
|
FROM CustomFieldDefinition
|
||||||
WHERE entity_type = %s
|
WHERE entity_type = %s
|
||||||
ORDER BY display_order, name
|
ORDER BY display_order, name
|
||||||
@@ -3820,12 +3625,12 @@ def api_add_custom_field():
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO CustomFieldDefinition
|
INSERT INTO CustomFieldDefinition
|
||||||
(entity_type, name, field_key, field_type, required, default_value,
|
(entity_type, name, field_key, field_type, required, default_value,
|
||||||
help_text, display_order, validation_rules, searchable)
|
help_text, display_order, validation_rules)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
''', (entity_type, data['name'], data['field_key'], data['field_type'],
|
''', (entity_type, data['name'], data['field_key'], data['field_type'],
|
||||||
data.get('required', False), data.get('default_value'),
|
data.get('required', False), data.get('default_value'),
|
||||||
data.get('help_text'), data.get('display_order', 0),
|
data.get('help_text'), data.get('display_order', 0),
|
||||||
validation_rules_json, data.get('searchable', False)))
|
validation_rules_json))
|
||||||
field_id = cursor.lastrowid
|
field_id = cursor.lastrowid
|
||||||
add_audit_log(request.api_user['id'], 'add_custom_field',
|
add_audit_log(request.api_user['id'], 'add_custom_field',
|
||||||
f"Added custom field '{data['name']}' for {entity_type}", conn=conn)
|
f"Added custom field '{data['name']}' for {entity_type}", conn=conn)
|
||||||
@@ -3874,9 +3679,6 @@ def api_update_custom_field(field_id):
|
|||||||
validation_rules_json = json.dumps(data['validation_rules']) if data['validation_rules'] else None
|
validation_rules_json = json.dumps(data['validation_rules']) if data['validation_rules'] else None
|
||||||
updates.append('validation_rules = %s')
|
updates.append('validation_rules = %s')
|
||||||
values.append(validation_rules_json)
|
values.append(validation_rules_json)
|
||||||
if 'searchable' in data:
|
|
||||||
updates.append('searchable = %s')
|
|
||||||
values.append(data['searchable'])
|
|
||||||
|
|
||||||
if not updates:
|
if not updates:
|
||||||
return jsonify({'error': 'No changes to apply'}), 400
|
return jsonify({'error': 'No changes to apply'}), 400
|
||||||
@@ -3987,9 +3789,6 @@ def api_tags():
|
|||||||
"""Get all tags"""
|
"""Get all tags"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, color, description, created_at FROM Tag ORDER BY name')
|
cursor.execute('SELECT id, name, color, description, created_at FROM Tag ORDER BY name')
|
||||||
tags = cursor.fetchall()
|
tags = cursor.fetchall()
|
||||||
@@ -4002,11 +3801,6 @@ def api_tags():
|
|||||||
@api_permission_required('add_tag')
|
@api_permission_required('add_tag')
|
||||||
def api_add_tag():
|
def api_add_tag():
|
||||||
"""Create a new tag"""
|
"""Create a new tag"""
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'name' not in data:
|
if not data or 'name' not in data:
|
||||||
return jsonify({'error': 'Tag name is required'}), 400
|
return jsonify({'error': 'Tag name is required'}), 400
|
||||||
@@ -4018,7 +3812,6 @@ def api_add_tag():
|
|||||||
color = data.get('color', '#6B7280')
|
color = data.get('color', '#6B7280')
|
||||||
description = data.get('description', '')
|
description = data.get('description', '')
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
try:
|
||||||
@@ -4036,9 +3829,6 @@ def api_tag(tag_id):
|
|||||||
"""Get a specific tag"""
|
"""Get a specific tag"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, color, description, created_at FROM Tag WHERE id = %s', (tag_id,))
|
cursor.execute('SELECT id, name, color, description, created_at FROM Tag WHERE id = %s', (tag_id,))
|
||||||
tag = cursor.fetchone()
|
tag = cursor.fetchone()
|
||||||
@@ -4059,16 +3849,10 @@ def api_tag(tag_id):
|
|||||||
@api_permission_required('edit_tag')
|
@api_permission_required('edit_tag')
|
||||||
def api_update_tag(tag_id):
|
def api_update_tag(tag_id):
|
||||||
"""Update a tag"""
|
"""Update a tag"""
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'error': 'Request body is required'}), 400
|
return jsonify({'error': 'Request body is required'}), 400
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name, color, description FROM Tag WHERE id = %s', (tag_id,))
|
cursor.execute('SELECT name, color, description FROM Tag WHERE id = %s', (tag_id,))
|
||||||
@@ -4113,9 +3897,6 @@ def api_delete_tag(tag_id):
|
|||||||
"""Delete a tag"""
|
"""Delete a tag"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Tag WHERE id = %s', (tag_id,))
|
cursor.execute('SELECT name FROM Tag WHERE id = %s', (tag_id,))
|
||||||
tag = cursor.fetchone()
|
tag = cursor.fetchone()
|
||||||
@@ -4133,9 +3914,6 @@ def api_device_tags(device_id):
|
|||||||
"""Get tags for a specific device"""
|
"""Get tags for a specific device"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name FROM Device WHERE id = %s', (device_id,))
|
cursor.execute('SELECT id, name FROM Device WHERE id = %s', (device_id,))
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
@@ -4154,9 +3932,6 @@ def api_device_tags(device_id):
|
|||||||
@api_permission_required('assign_device_tag')
|
@api_permission_required('assign_device_tag')
|
||||||
def api_assign_device_tag(device_id):
|
def api_assign_device_tag(device_id):
|
||||||
"""Assign a tag to a device"""
|
"""Assign a tag to a device"""
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'tag_id' not in data:
|
if not data or 'tag_id' not in data:
|
||||||
return jsonify({'error': 'tag_id is required'}), 400
|
return jsonify({'error': 'tag_id is required'}), 400
|
||||||
@@ -4180,8 +3955,6 @@ def api_assign_device_tag(device_id):
|
|||||||
def api_remove_device_tag(device_id, tag_id):
|
def api_remove_device_tag(device_id, tag_id):
|
||||||
"""Remove a tag from a device"""
|
"""Remove a tag from a device"""
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
|
||||||
try:
|
try:
|
||||||
remove_tag_from_device(conn, device_id, tag_id, request.api_user['id'])
|
remove_tag_from_device(conn, device_id, tag_id, request.api_user['id'])
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -4373,19 +4146,12 @@ def api_roles():
|
|||||||
def bulk_operations():
|
def bulk_operations():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
# Check if bulk operations feature is enabled
|
|
||||||
if not is_feature_enabled('bulk_operations', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT id, name FROM Device ORDER BY name')
|
cursor.execute('SELECT id, name FROM Device ORDER BY name')
|
||||||
devices = cursor.fetchall()
|
devices = cursor.fetchall()
|
||||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
||||||
subnets = cursor.fetchall()
|
subnets = cursor.fetchall()
|
||||||
|
|
||||||
# Get tags only if device tags feature is enabled
|
|
||||||
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
|
||||||
tags = []
|
|
||||||
if tags_enabled:
|
|
||||||
cursor.execute('SELECT id, name FROM Tag ORDER BY name')
|
cursor.execute('SELECT id, name FROM Tag ORDER BY name')
|
||||||
tags = cursor.fetchall()
|
tags = cursor.fetchall()
|
||||||
|
|
||||||
@@ -4404,17 +4170,11 @@ def bulk_operations():
|
|||||||
@app.route('/bulk/assign_ips', methods=['POST'])
|
@app.route('/bulk/assign_ips', methods=['POST'])
|
||||||
@permission_required('add_device_ip')
|
@permission_required('add_device_ip')
|
||||||
def bulk_assign_ips():
|
def bulk_assign_ips():
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
# Check if bulk operations feature is enabled
|
|
||||||
if not is_feature_enabled('bulk_operations', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
device_id = request.form['device_id']
|
device_id = request.form['device_id']
|
||||||
ip_ids = request.form.getlist('ip_ids[]')
|
ip_ids = request.form.getlist('ip_ids[]')
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
results = {'success': [], 'failed': []}
|
results = {'success': [], 'failed': []}
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
if not get_device_name(cursor, device_id):
|
if not get_device_name(cursor, device_id):
|
||||||
@@ -4436,17 +4196,11 @@ def bulk_assign_ips():
|
|||||||
@app.route('/bulk/create_devices', methods=['POST'])
|
@app.route('/bulk/create_devices', methods=['POST'])
|
||||||
@permission_required('add_device')
|
@permission_required('add_device')
|
||||||
def bulk_create_devices():
|
def bulk_create_devices():
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
# Check if bulk operations feature is enabled
|
|
||||||
if not is_feature_enabled('bulk_operations', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
device_names = request.form.get('device_names', '').strip().split('\n')
|
device_names = request.form.get('device_names', '').strip().split('\n')
|
||||||
device_type_id = int(request.form.get('device_type', 1))
|
device_type_id = int(request.form.get('device_type', 1))
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
results = {'success': [], 'failed': []}
|
results = {'success': [], 'failed': []}
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
for name in device_names:
|
for name in device_names:
|
||||||
@@ -4467,20 +4221,11 @@ def bulk_create_devices():
|
|||||||
@app.route('/bulk/assign_tags', methods=['POST'])
|
@app.route('/bulk/assign_tags', methods=['POST'])
|
||||||
@permission_required('assign_device_tag')
|
@permission_required('assign_device_tag')
|
||||||
def bulk_assign_tags():
|
def bulk_assign_tags():
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
# Check if bulk operations feature is enabled
|
|
||||||
if not is_feature_enabled('bulk_operations', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
# Check if device tags feature is enabled
|
|
||||||
if not is_feature_enabled('device_tags', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
device_ids = request.form.getlist('device_ids[]')
|
device_ids = request.form.getlist('device_ids[]')
|
||||||
tag_ids = request.form.getlist('tag_ids[]')
|
tag_ids = request.form.getlist('tag_ids[]')
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
results = {'success': [], 'failed': []}
|
results = {'success': [], 'failed': []}
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
for device_id in device_ids:
|
for device_id in device_ids:
|
||||||
@@ -4517,9 +4262,6 @@ def bulk_assign_tags():
|
|||||||
@app.route('/bulk/export_subnets', methods=['POST'])
|
@app.route('/bulk/export_subnets', methods=['POST'])
|
||||||
@permission_required('export_subnet_csv')
|
@permission_required('export_subnet_csv')
|
||||||
def bulk_export_subnets():
|
def bulk_export_subnets():
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
if not is_feature_enabled('bulk_operations', conn=conn):
|
|
||||||
abort(404)
|
|
||||||
subnet_ids = request.form.getlist('subnet_ids[]')
|
subnet_ids = request.form.getlist('subnet_ids[]')
|
||||||
output = StringIO()
|
output = StringIO()
|
||||||
writer = csv.writer(output)
|
writer = csv.writer(output)
|
||||||
@@ -4566,7 +4308,6 @@ def inject_env_vars():
|
|||||||
'LOGO_PNG': os.environ.get('LOGO_PNG', 'https://assets.jdbnet.co.uk/logo/128x128.png'),
|
'LOGO_PNG': os.environ.get('LOGO_PNG', 'https://assets.jdbnet.co.uk/logo/128x128.png'),
|
||||||
'VERSION': version,
|
'VERSION': version,
|
||||||
'has_permission': has_permission,
|
'has_permission': has_permission,
|
||||||
'is_feature_enabled': is_feature_enabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init_db(app)
|
init_db(app)
|
||||||
|
|||||||
@@ -313,7 +313,6 @@ def init_db(app=None):
|
|||||||
help_text TEXT,
|
help_text TEXT,
|
||||||
display_order INTEGER DEFAULT 0,
|
display_order INTEGER DEFAULT 0,
|
||||||
validation_rules TEXT,
|
validation_rules TEXT,
|
||||||
searchable BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
@@ -351,32 +350,6 @@ def init_db(app=None):
|
|||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_notes TEXT DEFAULT NULL')
|
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_notes TEXT DEFAULT NULL')
|
||||||
|
|
||||||
# Create FeatureFlags table
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS FeatureFlags (
|
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
feature_key VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
enabled BOOLEAN DEFAULT TRUE,
|
|
||||||
description TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
# Initialize default feature flags
|
|
||||||
default_features = [
|
|
||||||
('racks', True, 'Enable rack management functionality'),
|
|
||||||
('ip_address_notes', True, 'Enable IP address notes/descriptions editing on subnet page'),
|
|
||||||
('device_tags', True, 'Enable device tagging functionality'),
|
|
||||||
('bulk_operations', True, 'Enable bulk operations for devices and IPs')
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature_key, enabled, description in default_features:
|
|
||||||
cursor.execute('SELECT id FROM FeatureFlags WHERE feature_key = %s', (feature_key,))
|
|
||||||
if not cursor.fetchone():
|
|
||||||
cursor.execute('INSERT INTO FeatureFlags (feature_key, enabled, description) VALUES (%s, %s, %s)',
|
|
||||||
(feature_key, enabled, description))
|
|
||||||
|
|
||||||
# Define all permissions with categories
|
# Define all permissions with categories
|
||||||
permissions = [
|
permissions = [
|
||||||
# View permissions
|
# View permissions
|
||||||
|
|||||||
@@ -2,6 +2,5 @@ Flask
|
|||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
dotenv
|
dotenv
|
||||||
gunicorn
|
gunicorn
|
||||||
requests
|
|
||||||
pyotp
|
pyotp
|
||||||
qrcode[pil]
|
qrcode[pil]
|
||||||
@@ -99,7 +99,6 @@ function showAddFieldModal(entityType) {
|
|||||||
document.getElementById('field-default-value').value = '';
|
document.getElementById('field-default-value').value = '';
|
||||||
document.getElementById('field-help-text').value = '';
|
document.getElementById('field-help-text').value = '';
|
||||||
document.getElementById('field-display-order').value = '0';
|
document.getElementById('field-display-order').value = '0';
|
||||||
document.getElementById('field-searchable').checked = false;
|
|
||||||
|
|
||||||
// Reset validation fields
|
// Reset validation fields
|
||||||
document.getElementById('field-min-length').value = '';
|
document.getElementById('field-min-length').value = '';
|
||||||
@@ -181,7 +180,6 @@ function populateEditForm(field, entityType) {
|
|||||||
document.getElementById('field-default-value').value = field.default_value || '';
|
document.getElementById('field-default-value').value = field.default_value || '';
|
||||||
document.getElementById('field-help-text').value = field.help_text || '';
|
document.getElementById('field-help-text').value = field.help_text || '';
|
||||||
document.getElementById('field-display-order').value = field.display_order || 0;
|
document.getElementById('field-display-order').value = field.display_order || 0;
|
||||||
document.getElementById('field-searchable').checked = field.searchable || false;
|
|
||||||
|
|
||||||
// Parse validation rules
|
// Parse validation rules
|
||||||
let validationRules = {};
|
let validationRules = {};
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,40 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Check if toast was dismissed in this session
|
|
||||||
const toastDismissed = sessionStorage.getItem('update-toast-dismissed');
|
|
||||||
if (toastDismissed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for updates
|
|
||||||
fetch('/check_update')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.update_available) {
|
|
||||||
const toast = document.getElementById('update-toast');
|
|
||||||
const currentVersionEl = document.getElementById('toast-current-version');
|
|
||||||
const latestVersionEl = document.getElementById('toast-latest-version');
|
|
||||||
const compareLink = document.getElementById('toast-compare-link');
|
|
||||||
const closeBtn = document.getElementById('toast-close');
|
|
||||||
|
|
||||||
// Set versions (don't add 'v' prefix for dev versions)
|
|
||||||
currentVersionEl.textContent = (data.current_version === 'dev' ? '' : 'v') + data.current_version;
|
|
||||||
latestVersionEl.textContent = (data.latest_version === 'dev' ? '' : 'v') + data.latest_version;
|
|
||||||
|
|
||||||
// Set compare link (current version to latest version)
|
|
||||||
compareLink.href = `https://git.jdbnet.co.uk/jamie/ipam/compare/v${data.current_version}...v${data.latest_version}`;
|
|
||||||
|
|
||||||
// Show toast
|
|
||||||
toast.classList.remove('hidden');
|
|
||||||
|
|
||||||
// Close button handler
|
|
||||||
closeBtn.addEventListener('click', function() {
|
|
||||||
toast.classList.add('hidden');
|
|
||||||
sessionStorage.setItem('update-toast-dismissed', 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error checking for updates:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded",function(){let e=sessionStorage.getItem("update-toast-dismissed");!e&&fetch("/check_update").then(e=>e.json()).then(e=>{if(e.update_available){let t=document.getElementById("update-toast"),n=document.getElementById("toast-current-version"),s=document.getElementById("toast-latest-version"),a=document.getElementById("toast-compare-link"),d=document.getElementById("toast-close");n.textContent=("dev"===e.current_version?"":"v")+e.current_version,s.textContent=("dev"===e.latest_version?"":"v")+e.latest_version,a.href=`https://git.jdbnet.co.uk/jamie/ipam/compare/v${e.current_version}...v${e.latest_version}`,t.classList.remove("hidden"),d.addEventListener("click",function(){t.classList.add("hidden"),sessionStorage.setItem("update-toast-dismissed","true")})}}).catch(e=>{console.error("Error checking for updates:",e)})});
|
|
||||||
+1
-34
@@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||||
</a>
|
</a>
|
||||||
{% if has_permission('view_tags') and is_feature_enabled('device_tags') %}
|
{% if has_permission('view_tags') %}
|
||||||
<a href="/tags" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
|
<a href="/tags" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<i class="fas fa-tags text-3xl text-gray-600 dark:text-gray-400"></i>
|
<i class="fas fa-tags text-3xl text-gray-600 dark:text-gray-400"></i>
|
||||||
@@ -148,39 +148,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feature Flags Section -->
|
|
||||||
{% if has_permission('manage_users') %}
|
|
||||||
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md mt-8">
|
|
||||||
<h2 class="text-2xl font-bold mb-6">Feature Flags</h2>
|
|
||||||
<form action="/admin/feature_flags" method="POST">
|
|
||||||
<div class="space-y-4">
|
|
||||||
{% for flag in feature_flags %}
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-300 dark:bg-zinc-700 rounded-lg">
|
|
||||||
<div class="flex-1">
|
|
||||||
<label for="feature_{{ flag.key }}" class="text-lg font-semibold cursor-pointer">
|
|
||||||
{{ flag.key|replace('_', ' ')|title }}
|
|
||||||
</label>
|
|
||||||
{% if flag.description %}
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ flag.description }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input type="checkbox" name="feature_{{ flag.key }}" id="feature_{{ flag.key }}"
|
|
||||||
class="sr-only peer" {% if flag.enabled %}checked{% endif %}>
|
|
||||||
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-gray-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gray-500 dark:peer-checked:bg-zinc-600"></div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end mt-6">
|
|
||||||
<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 font-medium">
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,7 @@
|
|||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<button onclick="showTab('assign-ips')" id="tab-assign-ips" class="tab-btn px-4 py-2 font-medium border-b-2 border-gray-600 text-gray-900 dark:text-gray-100 hover:cursor-pointer">Bulk IP Assignment</button>
|
<button onclick="showTab('assign-ips')" id="tab-assign-ips" class="tab-btn px-4 py-2 font-medium border-b-2 border-gray-600 text-gray-900 dark:text-gray-100 hover:cursor-pointer">Bulk IP Assignment</button>
|
||||||
<button onclick="showTab('create-devices')" id="tab-create-devices" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Device Creation</button>
|
<button onclick="showTab('create-devices')" id="tab-create-devices" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Device Creation</button>
|
||||||
{% if is_feature_enabled('device_tags') %}
|
|
||||||
<button onclick="showTab('assign-tags')" id="tab-assign-tags" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Tag Assignment</button>
|
<button onclick="showTab('assign-tags')" id="tab-assign-tags" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Tag Assignment</button>
|
||||||
{% endif %}
|
|
||||||
<button onclick="showTab('export')" id="tab-export" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Export</button>
|
<button onclick="showTab('export')" id="tab-export" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Export</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +94,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bulk Tag Assignment -->
|
<!-- Bulk Tag Assignment -->
|
||||||
{% if is_feature_enabled('device_tags') %}
|
|
||||||
<div id="panel-assign-tags" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
<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 %}
|
{% if can_assign_device_tag %}
|
||||||
<h2 class="text-2xl font-bold mb-4">Bulk Tag Assignment</h2>
|
<h2 class="text-2xl font-bold mb-4">Bulk Tag Assignment</h2>
|
||||||
@@ -126,7 +123,6 @@
|
|||||||
<p class="text-gray-500">You don't have permission to assign tags to devices.</p>
|
<p class="text-gray-500">You don't have permission to assign tags to devices.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Bulk Export -->
|
<!-- Bulk Export -->
|
||||||
<div id="panel-export" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
<div id="panel-export" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
<th class="text-left p-3">Field Key</th>
|
<th class="text-left p-3">Field Key</th>
|
||||||
<th class="text-left p-3">Type</th>
|
<th class="text-left p-3">Type</th>
|
||||||
<th class="text-center p-3">Required</th>
|
<th class="text-center p-3">Required</th>
|
||||||
<th class="text-center p-3">Searchable</th>
|
|
||||||
<th class="text-center p-3">Actions</th>
|
<th class="text-center p-3">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -80,13 +79,6 @@
|
|||||||
<i class="fas fa-times text-gray-400"></i>
|
<i class="fas fa-times text-gray-400"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="p-3 text-center">
|
|
||||||
{% if field.searchable %}
|
|
||||||
<i class="fas fa-check text-green-500"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-times text-gray-400"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-center">
|
<td class="p-3 text-center">
|
||||||
<div class="flex items-center justify-center space-x-2">
|
<div class="flex items-center justify-center space-x-2">
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
@@ -132,7 +124,6 @@
|
|||||||
<th class="text-left p-3">Field Key</th>
|
<th class="text-left p-3">Field Key</th>
|
||||||
<th class="text-left p-3">Type</th>
|
<th class="text-left p-3">Type</th>
|
||||||
<th class="text-center p-3">Required</th>
|
<th class="text-center p-3">Required</th>
|
||||||
<th class="text-center p-3">Searchable</th>
|
|
||||||
<th class="text-center p-3">Actions</th>
|
<th class="text-center p-3">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -160,13 +151,6 @@
|
|||||||
<i class="fas fa-times text-gray-400"></i>
|
<i class="fas fa-times text-gray-400"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="p-3 text-center">
|
|
||||||
{% if field.searchable %}
|
|
||||||
<i class="fas fa-check text-green-500"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fas fa-times text-gray-400"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="p-3 text-center">
|
<td class="p-3 text-center">
|
||||||
<div class="flex items-center justify-center space-x-2">
|
<div class="flex items-center justify-center space-x-2">
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
@@ -262,10 +246,6 @@
|
|||||||
<input type="number" name="display_order" id="field-display-order" value="0" min="0"
|
<input type="number" name="display_order" id="field-display-order" value="0" min="0"
|
||||||
class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
|
class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<input type="checkbox" name="searchable" id="field-searchable" class="w-4 h-4">
|
|
||||||
<label for="field-searchable" class="text-sm font-medium">Searchable</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Validation Rules Section -->
|
<!-- Validation Rules Section -->
|
||||||
<div id="validation-rules-section" class="border-t border-gray-600 pt-4">
|
<div id="validation-rules-section" class="border-t border-gray-600 pt-4">
|
||||||
|
|||||||
@@ -115,7 +115,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Tags Section -->
|
<!-- Tags Section -->
|
||||||
{% if is_feature_enabled('device_tags') %}
|
|
||||||
<div class="tags-section mb-6">
|
<div class="tags-section mb-6">
|
||||||
<h3 class="text-lg font-bold mb-2">Tags:</h3>
|
<h3 class="text-lg font-bold mb-2">Tags:</h3>
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
@@ -156,7 +155,6 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form action="/update_device_description" method="POST" class="mb-6 mt-4">
|
<form action="/update_device_description" method="POST" class="mb-6 mt-4">
|
||||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
|
|||||||
@@ -16,16 +16,14 @@
|
|||||||
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
|
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
|
||||||
<div class="flex flex-row justify-center gap-4 mb-6 flex-wrap">
|
<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="/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>
|
||||||
{% if is_feature_enabled('bulk_operations') %}
|
|
||||||
<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="/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>
|
||||||
{% endif %}
|
|
||||||
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Filters Section -->
|
<!-- Filters Section -->
|
||||||
<div class="mb-6 space-y-4">
|
<div class="mb-6 space-y-4">
|
||||||
<!-- Tag Filter -->
|
<!-- Tag Filter -->
|
||||||
{% if is_feature_enabled('device_tags') and all_tag_names %}
|
{% if all_tag_names %}
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<label class="text-sm font-medium">Filter by tag:</label>
|
<label class="text-sm font-medium">Filter by tag:</label>
|
||||||
<select id="tag-filter" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600">
|
<select id="tag-filter" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600">
|
||||||
@@ -69,7 +67,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
{% if is_feature_enabled('device_tags') %}
|
|
||||||
{% set tags = device_tags.get(device.id, []) %}
|
{% set tags = device_tags.get(device.id, []) %}
|
||||||
{% if tags %}
|
{% if tags %}
|
||||||
<div class="flex flex-wrap gap-1 mt-2">
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
@@ -81,7 +78,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
+2
-43
@@ -25,7 +25,7 @@
|
|||||||
<span>Devices</span>
|
<span>Devices</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_permission('view_racks') and is_feature_enabled('racks') %}
|
{% if has_permission('view_racks') %}
|
||||||
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
<span>Racks</span>
|
<span>Racks</span>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<span>Devices</span>
|
<span>Devices</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_permission('view_racks') and is_feature_enabled('racks') %}
|
{% if has_permission('view_racks') %}
|
||||||
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
<span>Racks</span>
|
<span>Racks</span>
|
||||||
@@ -122,45 +122,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script src="/static/js/header.min.js"></script>
|
<script src="/static/js/header.min.js"></script>
|
||||||
|
|
||||||
<!-- Update Available Toast -->
|
|
||||||
<div id="update-toast" class="hidden fixed bottom-4 right-4 bg-gray-200 dark:bg-zinc-800 border border-gray-400 dark:border-zinc-600 rounded-lg shadow-lg max-w-md z-50">
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="flex items-start justify-between mb-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i class="fas fa-bell text-gray-900 dark:text-gray-100"></i>
|
|
||||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">Update Available</h3>
|
|
||||||
</div>
|
|
||||||
<button id="toast-close" class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:cursor-pointer">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
|
||||||
Version <span id="toast-latest-version" class="font-semibold"></span> is now available. You're currently on <span id="toast-current-version" class="font-semibold"></span>.
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2 mt-3">
|
|
||||||
<a id="toast-compare-link" href="#" target="_blank" rel="noopener noreferrer" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 text-gray-900 dark:text-gray-100 px-3 py-2 rounded text-center text-sm hover:cursor-pointer">
|
|
||||||
View Changes
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#update-toast {
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script src="/static/js/update_toast.min.js"></script>
|
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -251,7 +251,6 @@
|
|||||||
<td class="text-left align-top hidden sm:table-cell desc-col">
|
<td class="text-left align-top hidden sm:table-cell desc-col">
|
||||||
{% set device_desc = ip[4] if ip[4] else '' %}
|
{% set device_desc = ip[4] if ip[4] else '' %}
|
||||||
{% set ip_notes = ip[5] if ip|length > 5 and ip[5] else '' %}
|
{% set ip_notes = ip[5] if ip|length > 5 and ip[5] else '' %}
|
||||||
{% if ip_notes_enabled %}
|
|
||||||
{% set combined_desc = '' %}
|
{% set combined_desc = '' %}
|
||||||
{% if device_desc %}
|
{% if device_desc %}
|
||||||
{% set combined_desc = device_desc %}
|
{% set combined_desc = device_desc %}
|
||||||
@@ -268,10 +267,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<textarea readonly rows="1" class="border border-gray-600 rounded w-full cursor-pointer p-2" style="overflow: hidden; resize: none;">{{ combined_desc }}</textarea>
|
<textarea readonly rows="1" class="border border-gray-600 rounded w-full cursor-pointer p-2" style="overflow: hidden; resize: none;">{{ combined_desc }}</textarea>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
{# IP notes disabled - show only device description, read-only #}
|
|
||||||
<textarea readonly rows="1" class="border border-gray-600 rounded w-full cursor-pointer p-2 bg-gray-200 dark:bg-zinc-800" style="overflow: hidden; resize: none;">{{ device_desc }}</textarea>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user