6 Commits

Author SHA1 Message Date
Jamie 671b750bc4 Merge pull request #9 from JDB-NET/release-please--branches--main
chore(main): release 1.5.0
2025-11-21 20:43:15 +00:00
github-actions[bot] bc1078f673 chore(main): release 1.5.0 2025-11-21 20:42:49 +00:00
jamie ad1e576da4 feat: device tags 2025-11-21 20:42:20 +00:00
Jamie 0029abb8cd Merge pull request #7 from JDB-NET/release-please--branches--main
chore(main): release 1.4.2
2025-11-08 23:11:50 +00:00
github-actions[bot] ee72a89287 chore(main): release 1.4.2 2025-11-08 23:11:35 +00:00
jamie 5c1ad03990 fix: 🐛 ensure all fields are updated by api 2025-11-08 23:11:15 +00:00
15 changed files with 1747 additions and 173 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
".": "1.4.1" ".": "1.5.0"
} }
+14
View File
@@ -1,5 +1,19 @@
# Changelog # Changelog
## [1.5.0](https://github.com/JDB-NET/ipam/compare/v1.4.2...v1.5.0) (2025-11-21)
### Features
* :sparkles: device tags ([ad1e576](https://github.com/JDB-NET/ipam/commit/ad1e576da42bf90c59347f7f7a4cce13c6842204))
## [1.4.2](https://github.com/JDB-NET/ipam/compare/v1.4.1...v1.4.2) (2025-11-08)
### Bug Fixes
* :bug: ensure all fields are updated by api ([5c1ad03](https://github.com/JDB-NET/ipam/commit/5c1ad039904b2c8c8629242b5558b03da5ad782c))
## [1.4.1](https://github.com/JDB-NET/ipam/compare/v1.4.0...v1.4.1) (2025-11-06) ## [1.4.1](https://github.com/JDB-NET/ipam/compare/v1.4.0...v1.4.1) (2025-11-06)
+35 -6
View File
@@ -11,10 +11,11 @@ A Flask-based web application for comprehensive IP Address Management (IPAM). Ma
- **Subnet Management**: Create and manage IP subnets with CIDR notation (supports /24 to /32) - **Subnet Management**: Create and manage IP subnets with CIDR notation (supports /24 to /32)
- **IP Address Tracking**: Automatic IP address generation and tracking for each subnet - **IP Address Tracking**: Automatic IP address generation and tracking for each subnet
- **Device Management**: Track devices with types (Server, VM, Switch, Firewall, WiFi AP, Printer, Other) - **Device Management**: Track devices with types (Server, VM, Switch, Firewall, WiFi AP, Printer, Other)
- **Device Tagging**: Organize devices with customizable tags featuring colors and descriptions
- **IP Assignment**: Assign IP addresses to devices with automatic hostname updates - **IP Assignment**: Assign IP addresses to devices with automatic hostname updates
- **DHCP Pool Configuration**: Configure DHCP pools with start/end IP ranges and excluded IPs - **DHCP Pool Configuration**: Configure DHCP pools with start/end IP ranges and excluded IPs
- **Rack Management**: Physical infrastructure tracking with U positions and front/back sides - **Rack Management**: Physical infrastructure tracking with U positions and front/back sides
- **Site Organization**: Organize subnets and devices by site/location - **Site Organisation**: Organize subnets and devices by site/location
- **Audit Logging**: Complete audit trail of all changes with user, action, details, and timestamps - **Audit Logging**: Complete audit trail of all changes with user, action, details, and timestamps
- **User Management**: Multi-user support with secure password authentication - **User Management**: Multi-user support with secure password authentication
- **Role-Based Access Control (RBAC)**: Granular permission system with default roles (admin, user, view_only) and custom role creation - **Role-Based Access Control (RBAC)**: Granular permission system with default roles (admin, user, view_only) and custom role creation
@@ -36,7 +37,7 @@ docker run -d \
-e MYSQL_PASSWORD=your_password \ -e MYSQL_PASSWORD=your_password \
-e MYSQL_DATABASE=ipam \ -e MYSQL_DATABASE=ipam \
-e SECRET_KEY=your_secret_key \ -e SECRET_KEY=your_secret_key \
-e NAME="Your Organization" \ -e NAME="Your Organisation" \
-e LOGO_PNG="https://example.com/logo.png" \ -e LOGO_PNG="https://example.com/logo.png" \
ghcr.io/jdb-net/ipam:latest ghcr.io/jdb-net/ipam:latest
``` ```
@@ -59,7 +60,7 @@ services:
- MYSQL_PASSWORD=your_password - MYSQL_PASSWORD=your_password
- MYSQL_DATABASE=ipam - MYSQL_DATABASE=ipam
- SECRET_KEY=your_secret_key - SECRET_KEY=your_secret_key
- NAME=Your Organization - NAME=Your Organisation
- LOGO_PNG=https://example.com/logo.png - LOGO_PNG=https://example.com/logo.png
``` ```
@@ -72,8 +73,8 @@ services:
- `MYSQL_PASSWORD`: Database password (default: password) - `MYSQL_PASSWORD`: Database password (default: password)
- `MYSQL_DATABASE`: Database name (default: ipam) - `MYSQL_DATABASE`: Database name (default: ipam)
- `SECRET_KEY`: Flask secret key for sessions (**REQUIRED in production!**) - `SECRET_KEY`: Flask secret key for sessions (**REQUIRED in production!**)
- `NAME`: Organization name displayed in header (default: JDB-NET) - `NAME`: Organisation name displayed in header (default: JDB-NET)
- `LOGO_PNG`: URL or path to organization logo (default: JDB-NET logo) - `LOGO_PNG`: URL or path to organisation logo (default: JDB-NET logo)
### Database Setup ### Database Setup
@@ -135,6 +136,22 @@ FLUSH PRIVILEGES;
- **Height**: Rack height in U units - **Height**: Rack height in U units
3. Open a rack to assign devices to specific U positions (front or back) 3. Open a rack to assign devices to specific U positions (front or back)
### Device Tagging
1. **Managing Tags** (Admin only):
- Navigate to "Admin" > "Tag Management"
- Click "Add Tag" to create new tags with custom colors and descriptions
- Edit or delete existing tags as needed
2. **Assigning Tags to Devices**:
- Open any device from the Devices page
- Use the tag assignment dropdown to add multiple tags
- Remove tags by clicking the × button next to the tag name
3. **Filtering by Tags**:
- Use the tag filter dropdown on the Devices page to view devices with specific tags
- Tags appear as colored badges throughout the interface for easy identification
### Audit Log ### Audit Log
View all changes and actions in the "Audit Log" section, with filtering by user, subnet, action type, or device name. View all changes and actions in the "Audit Log" section, with filtering by user, subnet, action type, or device name.
@@ -182,6 +199,9 @@ The application includes a comprehensive REST API for programmatic access:
3. **Available Endpoints**: 3. **Available Endpoints**:
- **Devices**: `GET`, `POST`, `PUT`, `DELETE /api/v1/devices` - **Devices**: `GET`, `POST`, `PUT`, `DELETE /api/v1/devices`
- **Device Tags**: `GET /api/v1/devices/by-tag/{tag}` (filter devices by tag)
- **Tags**: `GET`, `POST`, `PUT`, `DELETE /api/v1/tags` (with `?format=simple` option)
- **Tag Assignment**: `GET`, `POST /api/v1/devices/{id}/tags`, `DELETE /api/v1/devices/{id}/tags/{tag_id}`
- **Subnets**: `GET`, `POST`, `PUT`, `DELETE /api/v1/subnets` - **Subnets**: `GET`, `POST`, `PUT`, `DELETE /api/v1/subnets`
- **Racks**: `GET`, `POST`, `DELETE /api/v1/racks` - **Racks**: `GET`, `POST`, `DELETE /api/v1/racks`
- **Device Types**: `GET /api/v1/device-types` - **Device Types**: `GET /api/v1/device-types`
@@ -193,10 +213,19 @@ The application includes a comprehensive REST API for programmatic access:
5. **Documentation**: Full API documentation is available in the Help page of the web interface. 5. **Documentation**: Full API documentation is available in the Help page of the web interface.
**Example API Request**: **Example API Requests**:
```bash ```bash
# List all devices
curl -H "X-API-Key: your_api_key" \ curl -H "X-API-Key: your_api_key" \
https://your-server:5000/api/v1/devices https://your-server:5000/api/v1/devices
# Get devices with a specific tag
curl -H "X-API-Key: your_api_key" \
https://your-server:5000/api/v1/devices/by-tag/production
# List all tags in simple format
curl -H "X-API-Key: your_api_key" \
https://your-server:5000/api/v1/tags?format=simple
``` ```
## Kubernetes Deployment ## Kubernetes Deployment
+1 -1
View File
@@ -1 +1 @@
1.4.1 1.5.0
+35 -2
View File
@@ -199,6 +199,30 @@ def init_db(app=None):
if not cursor.fetchone(): if not cursor.fetchone():
cursor.execute('ALTER TABLE User ADD COLUMN api_key VARCHAR(255) DEFAULT NULL UNIQUE') cursor.execute('ALTER TABLE User ADD COLUMN api_key VARCHAR(255) DEFAULT NULL UNIQUE')
# Create Tag table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Tag (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL UNIQUE,
color VARCHAR(7) DEFAULT '#6B7280',
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# Create DeviceTag junction table
cursor.execute('''
CREATE TABLE IF NOT EXISTS DeviceTag (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
device_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_device_tag (device_id, tag_id),
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES Tag(id) ON DELETE CASCADE
)
''')
# Define all permissions with categories # Define all permissions with categories
permissions = [ permissions = [
# View permissions # View permissions
@@ -246,6 +270,14 @@ def init_db(app=None):
('edit_device_type', 'Edit device type', 'Device Type'), ('edit_device_type', 'Edit device type', 'Device Type'),
('delete_device_type', 'Delete device type', 'Device Type'), ('delete_device_type', 'Delete device type', 'Device Type'),
# Tag permissions
('view_tags', 'View tags', 'Tag'),
('add_tag', 'Add new tag', 'Tag'),
('edit_tag', 'Edit tag', 'Tag'),
('delete_tag', 'Delete tag', 'Tag'),
('assign_device_tag', 'Assign tag to device', 'Tag'),
('remove_device_tag', 'Remove tag from device', 'Tag'),
# Admin permissions # Admin permissions
('manage_users', 'Manage users (add, edit, delete)', 'Admin'), ('manage_users', 'Manage users (add, edit, delete)', 'Admin'),
('manage_roles', 'Manage roles and permissions', 'Admin'), ('manage_roles', 'Manage roles and permissions', 'Admin'),
@@ -306,7 +338,8 @@ def init_db(app=None):
'add_rack', 'delete_rack', 'add_device_to_rack', 'remove_device_from_rack', 'add_rack', 'delete_rack', 'add_device_to_rack', 'remove_device_from_rack',
'add_nonnet_device_to_rack', 'export_rack_csv', 'add_nonnet_device_to_rack', 'export_rack_csv',
'configure_dhcp', 'configure_dhcp',
'add_device_type', 'edit_device_type', 'delete_device_type' 'add_device_type', 'edit_device_type', 'delete_device_type',
'view_tags', 'add_tag', 'edit_tag', 'delete_tag', 'assign_device_tag', 'remove_device_tag'
] ]
for perm_name in non_admin_permissions: for perm_name in non_admin_permissions:
@@ -325,7 +358,7 @@ def init_db(app=None):
view_only_permissions = [ view_only_permissions = [
'view_index', 'view_devices', 'view_device', 'view_subnet', 'view_racks', 'view_rack', 'view_index', 'view_devices', 'view_device', 'view_subnet', 'view_racks', 'view_rack',
'view_audit', 'view_device_types', 'view_device_type_stats', 'view_devices_by_type', 'view_audit', 'view_device_types', 'view_device_type_stats', 'view_devices_by_type',
'view_dhcp', 'view_help' 'view_dhcp', 'view_help', 'view_tags'
] ]
for perm_name in view_only_permissions: for perm_name in view_only_permissions:
+840 -66
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
// API Documentation Interactive Functions
function getApiKey() {
return document.getElementById('apiKey').value;
}
function showStatus(message, isError = false) {
const status = document.getElementById('connectionStatus');
status.textContent = message;
status.className = `mt-2 text-sm ${isError ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`;
}
async function testConnection() {
const apiKey = getApiKey();
if (!apiKey) {
showStatus('Please enter your API key', true);
return;
}
try {
const response = await axios.get('/api/v1/devices', {
headers: { 'X-API-Key': apiKey }
});
showStatus('✓ Connection successful');
} catch (error) {
if (error.response?.status === 401) {
showStatus('✗ Invalid API key', true);
} else if (error.response?.status === 403) {
showStatus('✗ Insufficient permissions', true);
} else {
showStatus('✗ Connection failed', true);
}
}
}
async function tryEndpoint(method, url, data, responseId) {
const apiKey = getApiKey();
if (!apiKey) {
showStatus('Please enter your API key first', true);
return;
}
try {
const config = {
method: method,
url: url,
headers: { 'X-API-Key': apiKey }
};
if (data) {
config.data = data;
}
const response = await axios(config);
document.getElementById(responseId + '-response').classList.remove('hidden');
document.getElementById(responseId).textContent = JSON.stringify(response.data, null, 2);
} catch (error) {
document.getElementById(responseId + '-response').classList.remove('hidden');
const errorMessage = error.response?.data?.error || error.message;
document.getElementById(responseId).textContent = `Error (${error.response?.status || 'Network'}): ${errorMessage}`;
}
}
async function tryEndpointWithId(method, baseUrl, inputId, responseId) {
const id = document.getElementById(inputId).value;
if (!id) {
alert('Please enter an ID');
return;
}
await tryEndpoint(method, baseUrl + encodeURIComponent(id), null, responseId);
}
// Auto-populate API key if user is logged in
document.addEventListener('DOMContentLoaded', function() {
const apiKeyInput = document.getElementById('apiKey');
if (apiKeyInput && apiKeyInput.value) {
testConnection();
}
});
+13
View File
@@ -1,5 +1,18 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Tag filter functionality
const tagFilter = document.getElementById('tag-filter');
if (tagFilter) {
tagFilter.addEventListener('change', function() {
const selectedTag = this.value;
if (selectedTag) {
window.location.href = '/devices?tag=' + encodeURIComponent(selectedTag);
} else {
window.location.href = '/devices';
}
});
}
// Expand/collapse site groups // Expand/collapse site groups
document.querySelectorAll('.site-header').forEach(header => { document.querySelectorAll('.site-header').forEach(header => {
header.addEventListener('click', function(e) { header.addEventListener('click', function(e) {
+69
View File
@@ -0,0 +1,69 @@
// Tag Management JavaScript
function showAddTagModal() {
document.getElementById('add-tag-modal').classList.remove('hidden');
document.getElementById('add-tag-name').value = '';
document.getElementById('add-tag-color').value = '#6B7280';
document.getElementById('add-tag-description').value = '';
updateColorPreview('add');
}
function closeAddTagModal() {
document.getElementById('add-tag-modal').classList.add('hidden');
}
function editTag(tagId, name, color, description) {
document.getElementById('edit-tag-id').value = tagId;
document.getElementById('edit-tag-name').value = name;
document.getElementById('edit-tag-color').value = color;
document.getElementById('edit-tag-description').value = description || '';
updateColorPreview('edit');
document.getElementById('edit-tag-modal').classList.remove('hidden');
}
function closeEditTagModal() {
document.getElementById('edit-tag-modal').classList.add('hidden');
}
function updateColorPreview(mode) {
const colorInput = document.getElementById(`${mode}-tag-color`);
const preview = document.getElementById(`${mode}-color-preview`);
preview.textContent = colorInput.value.toUpperCase();
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
const addColorInput = document.getElementById('add-tag-color');
const editColorInput = document.getElementById('edit-tag-color');
if (addColorInput) {
addColorInput.addEventListener('input', () => updateColorPreview('add'));
}
if (editColorInput) {
editColorInput.addEventListener('input', () => updateColorPreview('edit'));
}
// Handle edit tag button clicks
document.querySelectorAll('.edit-tag-btn').forEach(button => {
button.addEventListener('click', function() {
const tagId = this.dataset.tagId;
const tagName = this.dataset.tagName;
const tagColor = this.dataset.tagColor;
const tagDescription = this.dataset.tagDescription;
editTag(tagId, tagName, tagColor, tagDescription);
});
});
});
// Close modals when clicking outside
window.onclick = function(event) {
const addModal = document.getElementById('add-tag-modal');
const editModal = document.getElementById('edit-tag-modal');
if (event.target === addModal) {
closeAddTagModal();
}
if (event.target === editModal) {
closeEditTagModal();
}
}
+23 -1
View File
@@ -21,7 +21,7 @@
{% endif %} {% endif %}
<!-- Quick Links --> <!-- Quick Links -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<a href="/audit" 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="/audit" 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-clipboard-list text-3xl text-gray-600 dark:text-gray-400"></i> <i class="fas fa-clipboard-list text-3xl text-gray-600 dark:text-gray-400"></i>
@@ -42,6 +42,28 @@
</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') %}
<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">
<i class="fas fa-tags text-3xl text-gray-600 dark:text-gray-400"></i>
<div>
<h3 class="text-lg font-bold">Tag Management</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Manage device tags</p>
</div>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</a>
{% endif %}
<a href="/api-docs" 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">
<i class="fas fa-code text-3xl text-gray-600 dark:text-gray-400"></i>
<div>
<h3 class="text-lg font-bold">API Documentation</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Interactive API reference</p>
</div>
</div>
<i class="fas fa-chevron-right text-gray-400"></i>
</a>
</div> </div>
<!-- Subnet Management Section --> <!-- Subnet Management Section -->
+330
View File
@@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation - IPAM</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">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</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-7xl mx-auto">
<h1 class="text-3xl font-bold mb-8 text-center">API Documentation</h1>
<!-- Authentication Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8 mb-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">Authentication</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<p class="mb-4">All API requests require authentication using an API key. You can provide the API key in one of three ways:</p>
<ul class="list-disc list-inside space-y-2 ml-4">
<li><strong>Header:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">X-API-Key: your_api_key</code></li>
<li><strong>Authorization Header:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">Authorization: Bearer your_api_key</code></li>
<li><strong>Query Parameter:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">?api_key=your_api_key</code></li>
</ul>
<p class="mt-4"><strong>Base URL:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">/api/v1</code></p>
</div>
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h3 class="font-semibold mb-2">Your API Key</h3>
<div class="flex items-center space-x-2">
<input type="text" id="apiKey" value="{{ api_key or '' }}" readonly
class="flex-1 px-3 py-2 bg-gray-100 dark:bg-zinc-600 border border-gray-400 dark:border-zinc-500 rounded text-sm font-mono"
placeholder="API key not found">
<button onclick="testConnection()" class="px-4 py-2 bg-gray-500 hover:bg-gray-600 dark:bg-zinc-600 dark:hover:bg-zinc-500 text-white rounded text-sm transition-colors">
<i class="fas fa-plug mr-2"></i>Test
</button>
</div>
<div id="connectionStatus" class="mt-2 text-sm"></div>
</div>
</div>
</div>
<!-- Interactive Endpoints -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8 mb-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-play-circle mr-2"></i>Interactive Testing
</h2>
<p class="text-gray-600 dark:text-gray-300 mb-6">Test GET endpoints directly in your browser. Other methods are documented below.</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- GET /devices -->
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 bg-green-600 text-white rounded text-xs font-mono">GET</span>
<code class="text-sm">/api/v1/devices</code>
</div>
<button onclick="tryEndpoint('GET', '/api/v1/devices', null, 'devices-list')"
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 dark:bg-zinc-600 dark:hover:bg-zinc-500 text-white rounded text-xs transition-colors">
<i class="fas fa-play mr-1"></i>Try
</button>
</div>
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">List all devices</p>
<div id="devices-list-response" class="hidden">
<pre class="bg-gray-100 dark:bg-zinc-600 p-2 rounded text-xs overflow-x-auto max-h-32" id="devices-list"></pre>
</div>
</div>
<!-- GET /devices/{id} -->
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 bg-green-600 text-white rounded text-xs font-mono">GET</span>
<code class="text-sm">/api/v1/devices/{id}</code>
</div>
<div class="flex items-center space-x-1">
<input type="number" id="device-id" placeholder="ID" class="px-2 py-1 border rounded text-xs w-16">
<button onclick="tryEndpointWithId('GET', '/api/v1/devices/', 'device-id', 'device-detail')"
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 dark:bg-zinc-600 dark:hover:bg-zinc-500 text-white rounded text-xs transition-colors">
<i class="fas fa-play mr-1"></i>Try
</button>
</div>
</div>
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">Get device by ID</p>
<div id="device-detail-response" class="hidden">
<pre class="bg-gray-100 dark:bg-zinc-600 p-2 rounded text-xs overflow-x-auto max-h-32" id="device-detail"></pre>
</div>
</div>
<!-- GET /devices/by-tag/{tag} -->
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 bg-green-600 text-white rounded text-xs font-mono">GET</span>
<code class="text-sm">/api/v1/devices/by-tag/{tag}</code>
</div>
<div class="flex items-center space-x-1">
<input type="text" id="tag-name" placeholder="Tag" class="px-2 py-1 border rounded text-xs w-20">
<button onclick="tryEndpointWithId('GET', '/api/v1/devices/by-tag/', 'tag-name', 'devices-by-tag')"
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 dark:bg-zinc-600 dark:hover:bg-zinc-500 text-white rounded text-xs transition-colors">
<i class="fas fa-play mr-1"></i>Try
</button>
</div>
</div>
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">Filter devices by tag</p>
<div id="devices-by-tag-response" class="hidden">
<pre class="bg-gray-100 dark:bg-zinc-600 p-2 rounded text-xs overflow-x-auto max-h-32" id="devices-by-tag"></pre>
</div>
</div>
<!-- GET /tags -->
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 bg-green-600 text-white rounded text-xs font-mono">GET</span>
<code class="text-sm">/api/v1/tags</code>
</div>
<button onclick="tryEndpoint('GET', '/api/v1/tags', null, 'tags-list')"
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 dark:bg-zinc-600 dark:hover:bg-zinc-500 text-white rounded text-xs transition-colors">
<i class="fas fa-play mr-1"></i>Try
</button>
</div>
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">List all tags</p>
<div id="tags-list-response" class="hidden">
<pre class="bg-gray-100 dark:bg-zinc-600 p-2 rounded text-xs overflow-x-auto max-h-32" id="tags-list"></pre>
</div>
</div>
</div>
</div>
<!-- Complete API Documentation -->
<div class="space-y-6">
<!-- Devices Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-server mr-2"></i>Devices
</h2>
<div class="space-y-4">
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h4 class="font-semibold mb-2">Read Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/devices</code> - List all devices</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/devices/{id}</code> - Get device details</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/devices/by-tag/{tag}</code> - Get devices by tag</li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-2">Write Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/devices</code> - Create device</li>
<li><code class="bg-blue-200 dark:bg-blue-800 px-2 py-1 rounded text-xs">PUT /api/v1/devices/{id}</code> - Update device</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/devices/{id}</code> - Delete device</li>
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/devices/{id}/ips</code> - Add IP to device</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/devices/{id}/ips/{ip_id}</code> - Remove IP</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Subnets Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-network-wired mr-2"></i>Subnets
</h2>
<div class="space-y-4">
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h4 class="font-semibold mb-2">Read Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/subnets</code> - List all subnets</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/subnets/{id}</code> - Get subnet details</li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-2">Write Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/subnets</code> - Create subnet</li>
<li><code class="bg-blue-200 dark:bg-blue-800 px-2 py-1 rounded text-xs">PUT /api/v1/subnets/{id}</code> - Update subnet</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/subnets/{id}</code> - Delete subnet</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Racks Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-building mr-2"></i>Racks
</h2>
<div class="space-y-4">
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h4 class="font-semibold mb-2">Read Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/racks</code> - List all racks</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/racks/{id}</code> - Get rack details</li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-2">Write Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/racks</code> - Create rack</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/racks/{id}</code> - Delete rack</li>
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/racks/{id}/devices</code> - Add device to rack</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/racks/{id}/devices/{device_id}</code> - Remove device</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Tags Section -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-tags mr-2"></i>Tags
</h2>
<div class="space-y-4">
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h4 class="font-semibold mb-2">Read Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/tags</code> - List all tags</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/tags?format=simple</code> - List tags in simple format</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/tags/{id}</code> - Get tag details</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/devices/{id}/tags</code> - Get device tags</li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-2">Write Operations</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/tags</code> - Create tag</li>
<li><code class="bg-blue-200 dark:bg-blue-800 px-2 py-1 rounded text-xs">PUT /api/v1/tags/{id}</code> - Update tag</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/tags/{id}</code> - Delete tag</li>
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/devices/{id}/tags</code> - Assign tag to device</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">DELETE /api/v1/devices/{id}/tags/{tag_id}</code> - Remove tag</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Additional Endpoints -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-cogs mr-2"></i>Additional Endpoints
</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h4 class="font-semibold mb-3"><i class="fas fa-info-circle mr-2"></i>System Information</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/info</code> - System information</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/device-types</code> - List device types</li>
</ul>
</div>
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h4 class="font-semibold mb-3"><i class="fas fa-dharmachakra mr-2"></i>DHCP Management</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/subnets/{id}/dhcp</code> - Get DHCP config</li>
<li><code class="bg-orange-200 dark:bg-orange-800 px-2 py-1 rounded text-xs">POST /api/v1/subnets/{id}/dhcp</code> - Generate DHCP config</li>
</ul>
</div>
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h4 class="font-semibold mb-3"><i class="fas fa-users mr-2"></i>User & Role Management</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/users</code> - List users</li>
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/roles</code> - List roles</li>
</ul>
</div>
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h4 class="font-semibold mb-3"><i class="fas fa-clipboard-list mr-2"></i>Audit Log</h4>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-gray-400 dark:bg-zinc-600 px-2 py-1 rounded text-xs">GET /api/v1/audit</code> - List audit entries</li>
</ul>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2">Supports filtering with query parameters</p>
</div>
</div>
</div>
<!-- Response Format & Permissions -->
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-300 dark:border-zinc-600 pb-2">
<i class="fas fa-info-circle mr-2"></i>Response Format & Permissions
</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">Success Responses</h3>
<p class="mb-3 text-sm">All API responses are in JSON format. Successful requests return:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-green-200 dark:bg-green-800 px-2 py-1 rounded text-xs">200 OK</code> - Request successful</li>
<li><code class="bg-green-200 dark:bg-green-800 px-2 py-1 rounded text-xs">201 Created</code> - Resource created</li>
<li><code class="bg-green-200 dark:bg-green-800 px-2 py-1 rounded text-xs">204 No Content</code> - Success with no response body</li>
</ul>
</div>
<div class="bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">Error Responses</h3>
<p class="mb-3 text-sm">Error responses include descriptive messages:</p>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">400 Bad Request</code> - Invalid request data</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">401 Unauthorized</code> - Missing or invalid API key</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">403 Forbidden</code> - Insufficient permissions</li>
<li><code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">404 Not Found</code> - Resource not found</li>
</ul>
</div>
</div>
<div class="mt-6 bg-gray-300 dark:bg-zinc-700 rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3"><i class="fas fa-shield-alt mr-2"></i>Permissions</h3>
<p class="text-sm">API endpoints respect the same role-based permissions as the web interface. Users can only perform actions that their role allows. If a user lacks the required permission, the API will return a <code class="bg-red-200 dark:bg-red-800 px-2 py-1 rounded text-xs">403 Forbidden</code> error with details about the missing permission.</p>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/api_docs.js"></script>
</body>
</html>
+44 -1
View File
@@ -60,7 +60,7 @@
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg w-full">Add IP</button> <button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg w-full">Add IP</button>
</div> </div>
</form> </form>
<div class="allocated-ips"> <div class="allocated-ips mb-6">
<h3 class="text-lg font-bold mb-2">Allocated IPs:</h3> <h3 class="text-lg font-bold mb-2">Allocated IPs:</h3>
<ul class="space-y-2"> <ul class="space-y-2">
{% for ip in device_ips %} {% for ip in device_ips %}
@@ -74,6 +74,49 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<!-- Tags Section -->
<div class="tags-section mb-6">
<h3 class="text-lg font-bold mb-2">Tags:</h3>
<div class="flex flex-wrap gap-2 mb-4">
{% if device_tags %}
{% for tag in device_tags %}
<div class="flex items-center space-x-1 px-3 py-1 rounded-full text-sm" style="background-color: {{ tag.color }}20; border: 1px solid {{ tag.color }}">
<div class="w-2 h-2 rounded-full" style="background-color: {{ tag.color }}"></div>
<span>{{ tag.name }}</span>
{% if can_remove_device_tag %}
<form action="/device/{{ device.id }}/remove_tag" method="POST" class="inline">
<input type="hidden" name="tag_id" value="{{ tag.id }}">
<button type="submit" class="ml-1 text-red-500 hover:text-red-700 text-xs" title="Remove tag">
<i class="fas fa-times"></i>
</button>
</form>
{% endif %}
</div>
{% endfor %}
{% else %}
<span class="text-gray-500">No tags assigned</span>
{% endif %}
</div>
{% if can_assign_device_tag and all_tags %}
<form action="/device/{{ device.id }}/assign_tag" method="POST" class="flex gap-2">
<select name="tag_id" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 flex-1" required>
<option value="" disabled selected>Select a tag to assign...</option>
{% for tag in all_tags %}
{% set already_assigned = device_tags|selectattr('id', 'equalto', tag.id)|list|length > 0 %}
{% if not already_assigned %}
<option value="{{ tag.id }}">{{ tag.name }}</option>
{% endif %}
{% endfor %}
</select>
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg">
<i class="fas fa-plus mr-1"></i>Assign Tag
</button>
</form>
{% endif %}
</div>
<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 }}">
<label for="description" class="block mb-2 text-lg font-bold">Description</label> <label for="description" class="block mb-2 text-lg font-bold">Description</label>
+36 -2
View File
@@ -18,8 +18,28 @@
<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>
<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>
<div class="mb-6">
<!-- Filters Section -->
<div class="mb-6 space-y-4">
<input type="text" id="search" placeholder="Search devices or IPs..." class="border p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"> <input type="text" id="search" placeholder="Search devices or IPs..." class="border p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full">
<!-- Tag Filter -->
{% if all_tag_names %}
<div class="flex items-center space-x-2">
<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">
<option value="">All devices</option>
{% for tag_name in all_tag_names %}
<option value="{{ tag_name }}" {% if current_tag_filter == tag_name %}selected{% endif %}>{{ tag_name }}</option>
{% endfor %}
</select>
{% if current_tag_filter %}
<a href="/devices" class="text-red-500 hover:text-red-700 text-sm">
<i class="fas fa-times"></i> Clear filter
</a>
{% endif %}
</div>
{% endif %}
</div> </div>
<div id="site-list" class="space-y-6"> <div id="site-list" class="space-y-6">
{% for site, devices in sites_devices.items() %} {% for site, devices in sites_devices.items() %}
@@ -33,7 +53,8 @@
<ul class="device-list hidden px-6 pb-4"> <ul class="device-list hidden px-6 pb-4">
{% for device in devices %} {% for device in devices %}
<li class="my-2"> <li class="my-2">
<a href="/device/{{ device.id }}" class="flex items-center justify-between bg-gray-300 hover:bg-gray-100 dark:bg-zinc-900 dark:hover:bg-gray-700 dark:text-gray-200 font-semibold rounded-lg px-4 py-2 shadow transition-colors duration-150"> <a href="/device/{{ device.id }}" class="block bg-gray-300 hover:bg-gray-100 dark:bg-zinc-900 dark:hover:bg-gray-700 dark:text-gray-200 font-semibold rounded-lg px-4 py-2 shadow transition-colors duration-150">
<div class="flex items-center justify-between">
<span><i class="fas {{ device.icon_class or 'fa-server' }} mr-2"></i>{{ device.name }}</span> <span><i class="fas {{ device.icon_class or 'fa-server' }} mr-2"></i>{{ device.name }}</span>
{% set ips = device_ips.get(device.id, []) %} {% set ips = device_ips.get(device.id, []) %}
<span class="flex flex-row flex-wrap justify-end items-center ml-4 text-xs text-blue-300 font-normal align-middle"> <span class="flex flex-row flex-wrap justify-end items-center ml-4 text-xs text-blue-300 font-normal align-middle">
@@ -45,6 +66,19 @@
<span class="text-gray-400">No IPs</span> <span class="text-gray-400">No IPs</span>
{% endif %} {% endif %}
</span> </span>
</div>
<!-- Tags -->
{% set tags = device_tags.get(device.id, []) %}
{% if tags %}
<div class="flex flex-wrap gap-1 mt-2">
{% for tag in tags %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs" style="background-color: {{ tag.color }}20; border: 1px solid {{ tag.color }}; color: {{ tag.color }}">
<div class="w-1.5 h-1.5 rounded-full mr-1" style="background-color: {{ tag.color }}"></div>
{{ tag.name }}
</span>
{% endfor %}
</div>
{% endif %}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
+33 -79
View File
@@ -10,10 +10,11 @@
</head> </head>
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col"> <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' %} {% include 'header.html' %}
<div class="flex-1 flex items-center justify-center mx-4"> <div class="flex-1 mx-4 py-8 pt-20">
<div class="container py-8 max-w-2xl pt-20"> <div class="container max-w-full mx-auto lg:px-32">
<h1 class="text-3xl font-bold mb-8 text-center">Help & User Guide</h1> <h1 class="text-3xl font-bold mb-8 text-center">Help & User Guide</h1>
<div class="space-y-10 text-lg">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8"> <div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">Subnets & Devices</h2> <h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">Subnets & Devices</h2>
<div class="space-y-4"> <div class="space-y-4">
@@ -52,6 +53,35 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">Device Tags</h2>
<div class="space-y-4">
<div>
<h3 class="text-xl font-semibold mb-1">Managing Tags</h3>
<p>Tags help you organize and categorize your devices. Administrators can manage tags from the <a href="/tags" class="text-blue-600 dark:text-blue-400 hover:underline">Tag Management</a> page accessible from the Admin panel. Each tag has a name, customizable colour, and optional description.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Creating Tags</h3>
<p>Click <span class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded">Add Tag</span> to create a new tag. Choose a descriptive name (e.g., "Production", "Development", "Critical") and select a colour to visually distinguish the tag. Tags can be used to group devices by environment, importance, location, or any other criteria.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Assigning Tags to Devices</h3>
<p>From any device's detail page, you can assign multiple tags using the tag assignment dropdown. Each device can have unlimited tags, and removing a tag is as simple as clicking the × button next to the tag name.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Filtering Devices by Tags</h3>
<p>On the <a href="/devices" class="text-blue-600 dark:text-blue-400 hover:underline">Devices</a> page, use the tag filter dropdown to view only devices with a specific tag. This makes it easy to focus on devices in a particular environment or category.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Tag Colours and Visual Organisation</h3>
<p>Tags appear as coloured badges throughout the interface. Use consistent colour schemes (e.g., red for production, blue for development) to create visual patterns that help users quickly identify device categories.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Tag Permissions</h3>
<p>Tag management respects role-based permissions. Users need appropriate permissions to view, create, edit, delete tags, or assign/remove tags from devices. View-only users can see tags but cannot modify them.</p>
</div>
</div>
</div>
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8"> <div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">User Management & Audit</h2> <h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">User Management & Audit</h2>
<div class="space-y-4"> <div class="space-y-4">
@@ -86,82 +116,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-200 dark:bg-zinc-800 rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">API Documentation</h2>
<div class="space-y-4">
<div>
<h3 class="text-xl font-semibold mb-1">Authentication</h3>
<p>All API requests require authentication using an API key. You can provide the API key in one of three ways:</p>
<ul class="list-disc list-inside mt-2 space-y-1 ml-4">
<li><strong>Header:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">X-API-Key: your_api_key</code></li>
<li><strong>Authorization Header:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">Authorization: Bearer your_api_key</code></li>
<li><strong>Query Parameter:</strong> <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">?api_key=your_api_key</code></li>
</ul>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Base URL</h3>
<p>All API endpoints are prefixed with <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">/api/v1</code></p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Available Endpoints</h3>
<div class="space-y-3 mt-2">
<div>
<h4 class="font-semibold">Devices</h4>
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/devices</code> - List all devices</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/devices/{id}</code> - Get device details</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/devices</code> - Create device</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">PUT /api/v1/devices/{id}</code> - Update device</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/devices/{id}</code> - Delete device</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/devices/{id}/ips</code> - Add IP to device</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/devices/{id}/ips/{ip_id}</code> - Remove IP from device</li>
</ul>
</div>
<div>
<h4 class="font-semibold">Subnets</h4>
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/subnets</code> - List all subnets</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/subnets/{id}</code> - Get subnet details</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/subnets</code> - Create subnet</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">PUT /api/v1/subnets/{id}</code> - Update subnet</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/subnets/{id}</code> - Delete subnet</li>
</ul>
</div>
<div>
<h4 class="font-semibold">Racks</h4>
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/racks</code> - List all racks</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/racks/{id}</code> - Get rack details</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/racks</code> - Create rack</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/racks/{id}</code> - Delete rack</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/racks/{id}/devices</code> - Add device to rack</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">DELETE /api/v1/racks/{id}/devices/{rack_device_id}</code> - Remove device from rack</li>
</ul>
</div>
<div>
<h4 class="font-semibold">Other</h4>
<ul class="list-disc list-inside ml-4 text-sm space-y-1">
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/info</code> - Get API info and user details</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/device-types</code> - List device types</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/subnets/{id}/dhcp</code> - Get DHCP pools</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">POST /api/v1/subnets/{id}/dhcp</code> - Configure DHCP pools</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/audit</code> - Get audit log</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/users</code> - List users (admin only)</li>
<li><code class="bg-gray-300 dark:bg-zinc-700 px-1 rounded">GET /api/v1/roles</code> - List roles (admin only)</li>
</ul>
</div>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Permissions</h3>
<p>API endpoints respect the same role-based permissions as the web interface. Users can only perform actions that their role allows. If a user lacks the required permission, the API will return a <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">403 Forbidden</code> error.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-1">Response Format</h3>
<p>All API responses are in JSON format. Successful requests return <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">200 OK</code> or <code class="bg-gray-300 dark:bg-zinc-700 px-2 py-1 rounded text-sm">201 Created</code> with the requested data. Errors return appropriate HTTP status codes with an error message in the response body.</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
+180
View File
@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tag Management</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-7xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">Tag Management</h1>
{% if can_add_tag %}
<button onclick="showAddTagModal()" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg font-medium">
<i class="fas fa-plus mr-2"></i>Add Tag
</button>
{% endif %}
</div>
{% if error %}
<div class="bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200 p-4 rounded-lg mb-6">
{{ error }}
</div>
{% endif %}
{% if tags %}
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-600">
<th class="text-left p-3">Name</th>
<th class="text-left p-3">Colour</th>
<th class="text-left p-3">Description</th>
<th class="text-center p-3">Devices</th>
<th class="text-center p-3">Created</th>
<th class="text-center p-3">Actions</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr class="border-b border-gray-600 hover:bg-gray-300 dark:hover:bg-zinc-700">
<td class="p-3">
<div class="flex items-center space-x-2">
<div class="w-4 h-4 rounded-full border border-gray-600" style="background-color: {{ tag.color }}"></div>
<span class="font-medium">{{ tag.name }}</span>
</div>
</td>
<td class="p-3">
<span class="font-mono text-sm">{{ tag.color }}</span>
</td>
<td class="p-3">
<span class="text-sm">{{ tag.description or '-' }}</span>
</td>
<td class="p-3 text-center">
{% if tag.device_count > 0 %}
<a href="/api/v1/devices/by-tag/{{ tag.name }}" class="text-blue-400 hover:text-blue-600">
{{ tag.device_count }}
</a>
{% else %}
<span class="text-gray-500">0</span>
{% endif %}
</td>
<td class="p-3 text-center text-sm">
{{ tag.created_at.strftime('%Y-%m-%d') if tag.created_at else '-' }}
</td>
<td class="p-3 text-center">
<div class="flex items-center justify-center space-x-2">
{% if can_edit_tag %}
<button class="edit-tag-btn text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer"
title="Edit Tag"
data-tag-id="{{ tag.id }}"
data-tag-name="{{ tag.name }}"
data-tag-color="{{ tag.color }}"
data-tag-description="{{ tag.description or '' }}">
<i class="fas fa-edit"></i>
</button>
{% endif %}
{% if can_delete_tag %}
<form action="/tags" method="POST" onsubmit="return confirm('Are you sure you want to delete this tag? This will remove it from all devices.');" class="inline">
<input type="hidden" name="action" value="delete_tag">
<input type="hidden" name="tag_id" value="{{ tag.id }}">
<button type="submit" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer" title="Delete Tag">
<i class="fas fa-trash"></i>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
<div class="text-center py-8 text-gray-500">
<i class="fas fa-tags text-4xl mb-4"></i>
<p>No tags found. Add your first tag to get started.</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Add Tag Modal -->
<div id="add-tag-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Add New Tag</h2>
<button onclick="closeAddTagModal()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/tags" method="POST">
<input type="hidden" name="action" value="add_tag">
<div class="space-y-4">
<input type="text" name="name" id="add-tag-name" placeholder="Tag Name"
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" required>
<div class="flex items-center space-x-2">
<label class="text-sm font-medium">Colour:</label>
<input type="color" name="color" id="add-tag-color" value="#6B7280"
class="w-12 h-10 border border-gray-600 rounded cursor-pointer">
<span id="add-color-preview" class="text-sm font-mono">#6B7280</span>
</div>
<textarea name="description" id="add-tag-description" placeholder="Description (optional)" rows="3"
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 resize-y"></textarea>
</div>
<div class="flex justify-end space-x-2 mt-6">
<button type="button" onclick="closeAddTagModal()"
class="px-4 py-2 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit"
class="px-4 py-2 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Add Tag</button>
</div>
</form>
</div>
</div>
<!-- Edit Tag Modal -->
<div id="edit-tag-modal" class="hidden fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50">
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-lg max-w-md w-full mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Edit Tag</h2>
<button onclick="closeEditTagModal()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<form action="/tags" method="POST">
<input type="hidden" name="action" value="edit_tag">
<input type="hidden" name="tag_id" id="edit-tag-id">
<div class="space-y-4">
<input type="text" name="name" id="edit-tag-name" placeholder="Tag Name"
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" required>
<div class="flex items-center space-x-2">
<label class="text-sm font-medium">Colour:</label>
<input type="color" name="color" id="edit-tag-color"
class="w-12 h-10 border border-gray-600 rounded cursor-pointer">
<span id="edit-color-preview" class="text-sm font-mono"></span>
</div>
<textarea name="description" id="edit-tag-description" placeholder="Description (optional)" rows="3"
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 resize-y"></textarea>
</div>
<div class="flex justify-end space-x-2 mt-6">
<button type="button" onclick="closeEditTagModal()"
class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
<button type="submit"
class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Save Changes</button>
</div>
</form>
</div>
</div>
<script src="/static/js/tags.js"></script>
</body>
</html>