batman (working version kinda)
This commit is contained in:
commit
6dd38036e7
65 changed files with 3950 additions and 0 deletions
50
app/templates/auth/login.html
Normal file
50
app/templates/auth/login.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm mt-5">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="card-title">Login</h2>
|
||||
<p class="text-muted">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-4 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">Remember me</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<p>Don't have an account? <a href="{{ url_for('auth.register') }}">Register here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
50
app/templates/auth/register.html
Normal file
50
app/templates/auth/register.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm mt-5">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="card-title">Register</h2>
|
||||
<p class="text-muted">Create a new account</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password_confirm" class="form-label">Confirm Password</label>
|
||||
<input type="password" class="form-control" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Register</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<p>Already have an account? <a href="{{ url_for('auth.login') }}">Login here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
173
app/templates/components/app_form.html
Normal file
173
app/templates/components/app_form.html
Normal file
|
@ -0,0 +1,173 @@
|
|||
<form hx-post="{{ action_url }}" hx-target="#content">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">App Name</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ app.name if app else '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">App Type</label>
|
||||
<select id="app-type" name="app_type" class="form-select">
|
||||
<option value="">-- Select Type --</option>
|
||||
<option value="web">Web Application</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="mail">Mail Server</option>
|
||||
<option value="file">File Server</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Server</label>
|
||||
<select name="server_id" class="form-select" required>
|
||||
{% for server in servers %}
|
||||
<option value="{{ server.id }}" {% if app and app.server_id==server.id %}selected{% endif %}>
|
||||
{{ server.hostname }} ({{ server.ip_address }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Documentation</label>
|
||||
{% include "components/markdown_editor.html" with content=app.documentation if app else "" %}
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex">
|
||||
<h3 class="card-title">Ports</h3>
|
||||
<div class="ms-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="suggest-ports">
|
||||
Suggest Ports
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="ports-container">
|
||||
{% if app and app.ports %}
|
||||
{% for port in app.ports %}
|
||||
<div class="port-entry mb-2 row">
|
||||
<div class="col-3">
|
||||
<input type="number" name="port[]" class="form-control" value="{{ port.port }}" placeholder="Port" min="1"
|
||||
max="65535">
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<select name="port_type[]" class="form-select">
|
||||
<option value="tcp" {% if port.type=='tcp' %}selected{% endif %}>TCP</option>
|
||||
<option value="udp" {% if port.type=='udp' %}selected{% endif %}>UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<input type="text" name="port_desc[]" class="form-control" value="{{ port.desc }}"
|
||||
placeholder="Description">
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button type="button" class="btn btn-sm btn-danger remove-port">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="port-entry mb-2 row">
|
||||
<div class="col-3">
|
||||
<input type="number" name="port[]" class="form-control" placeholder="Port" min="1" max="65535">
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<select name="port_type[]" class="form-select">
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<input type="text" name="port_desc[]" class="form-control" placeholder="Description">
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button type="button" class="btn btn-sm btn-danger remove-port">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" id="add-port">
|
||||
Add Port
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Add new port entry
|
||||
document.getElementById('add-port').addEventListener('click', function () {
|
||||
const template = document.querySelector('.port-entry').cloneNode(true);
|
||||
const inputs = template.querySelectorAll('input, select');
|
||||
inputs.forEach(input => input.value = '');
|
||||
if (inputs[0].type === 'number') inputs[0].value = '';
|
||||
|
||||
document.getElementById('ports-container').appendChild(template);
|
||||
|
||||
// Re-attach remove handlers
|
||||
attachRemoveHandlers();
|
||||
});
|
||||
|
||||
// Remove port entry
|
||||
function attachRemoveHandlers() {
|
||||
document.querySelectorAll('.remove-port').forEach(button => {
|
||||
button.onclick = function () {
|
||||
const portEntries = document.querySelectorAll('.port-entry');
|
||||
if (portEntries.length > 1) {
|
||||
this.closest('.port-entry').remove();
|
||||
} else {
|
||||
const inputs = this.closest('.port-entry').querySelectorAll('input, select');
|
||||
inputs.forEach(input => input.value = '');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
attachRemoveHandlers();
|
||||
|
||||
// Port suggestions based on app type
|
||||
document.getElementById('suggest-ports').addEventListener('click', function () {
|
||||
const appType = document.getElementById('app-type').value;
|
||||
|
||||
fetch(`/api/ports/suggest?type=${appType}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Clear existing ports
|
||||
const portsContainer = document.getElementById('ports-container');
|
||||
portsContainer.innerHTML = '';
|
||||
|
||||
// Add suggested ports
|
||||
data.forEach(port => {
|
||||
const template = document.createElement('div');
|
||||
template.className = 'port-entry mb-2 row';
|
||||
template.innerHTML = `
|
||||
<div class="col-3">
|
||||
<input type="number" name="port[]" class="form-control" value="${port.port}" min="1" max="65535">
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<select name="port_type[]" class="form-select">
|
||||
<option value="tcp" ${port.type === 'tcp' ? 'selected' : ''}>TCP</option>
|
||||
<option value="udp" ${port.type === 'udp' ? 'selected' : ''}>UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<input type="text" name="port_desc[]" class="form-control" value="${port.desc}" placeholder="Description">
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button type="button" class="btn btn-sm btn-danger remove-port">×</button>
|
||||
</div>
|
||||
`;
|
||||
portsContainer.appendChild(template);
|
||||
});
|
||||
|
||||
// Re-attach remove handlers
|
||||
attachRemoveHandlers();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
105
app/templates/components/markdown_editor.html
Normal file
105
app/templates/components/markdown_editor.html
Normal file
|
@ -0,0 +1,105 @@
|
|||
<div class="markdown-editor">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="editor-toolbar">
|
||||
<button type="button" data-action="bold" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-bold"></i>
|
||||
</button>
|
||||
<button type="button" data-action="italic" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-italic"></i>
|
||||
</button>
|
||||
<button type="button" data-action="heading" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-h-1"></i>
|
||||
</button>
|
||||
<button type="button" data-action="bulletList" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-list"></i>
|
||||
</button>
|
||||
<button type="button" data-action="orderedList" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-list-numbers"></i>
|
||||
</button>
|
||||
<button type="button" data-action="link" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-link"></i>
|
||||
</button>
|
||||
<button type="button" data-action="code" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="ti ti-code"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor-container" class="mt-2">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<input type="hidden" name="{{ field_name }}" id="markdown-content" value="{{ content }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Preview</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="markdown-preview" class="markdown-body p-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Initialize Tiptap editor
|
||||
const editor = new window.tiptap.Editor({
|
||||
element: document.getElementById('editor'),
|
||||
extensions: [
|
||||
window.tiptapStarterKit.StarterKit,
|
||||
window.tiptapLink.Link,
|
||||
window.tiptapCodeBlock.CodeBlock,
|
||||
window.tiptapImage.Image
|
||||
],
|
||||
content: document.getElementById('markdown-content').value || '',
|
||||
onUpdate: ({ editor }) => {
|
||||
// Update the hidden input with the markdown content
|
||||
const markdown = editor.storage.markdown.getMarkdown();
|
||||
document.getElementById('markdown-content').value = markdown;
|
||||
|
||||
// Update preview
|
||||
updatePreview(markdown);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the preview with the current content
|
||||
updatePreview(editor.storage.markdown.getMarkdown());
|
||||
|
||||
// Initialize the toolbar buttons
|
||||
document.querySelectorAll('.editor-toolbar button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const action = button.dataset.action;
|
||||
|
||||
if (action === 'link') {
|
||||
const url = prompt('URL');
|
||||
if (url) {
|
||||
editor.chain().focus().setLink({ href: url }).run();
|
||||
}
|
||||
} else if (action === 'heading') {
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run();
|
||||
} else {
|
||||
editor.chain().focus()[`toggle${action.charAt(0).toUpperCase() + action.slice(1)}`]().run();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Function to update preview
|
||||
function updatePreview(markdown) {
|
||||
const converter = new showdown.Converter({
|
||||
tables: true,
|
||||
tasklists: true,
|
||||
strikethrough: true,
|
||||
ghCodeBlocks: true
|
||||
});
|
||||
|
||||
const html = converter.makeHtml(markdown);
|
||||
document.getElementById('markdown-preview').innerHTML = html;
|
||||
}
|
||||
});
|
||||
</script>
|
6
app/templates/components/port_row.html
Normal file
6
app/templates/components/port_row.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div class="port-row" hx-target="this">
|
||||
<span>{{ port.number }}/{{ port.protocol }}</span>
|
||||
<button hx-get="/port/{{ port.id }}/copy" class="btn btn-sm">
|
||||
Kopieren
|
||||
</button>
|
||||
</div>
|
9
app/templates/dashboard.html
Normal file
9
app/templates/dashboard.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
<div id="server-list">
|
||||
<!-- Server list will be dynamically loaded here -->
|
||||
</div>
|
||||
<script src="/static/js/app.js"></script>
|
||||
{% endblock %}
|
57
app/templates/dashboard/app_form.html
Normal file
57
app/templates/dashboard/app_form.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Add New Application
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('dashboard.app_new') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">Application Name</label>
|
||||
<input type="text" class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">Server</label>
|
||||
<select class="form-select" name="server_id" required>
|
||||
<option value="">Select a server</option>
|
||||
{% for server in servers %}
|
||||
<option value="{{ server.id }}">{{ server.hostname }} ({{ server.ip_address }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Documentation</label>
|
||||
<textarea class="form-control" name="documentation" rows="6"
|
||||
placeholder="Use Markdown for formatting"></textarea>
|
||||
<small class="form-hint">Supports Markdown formatting</small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<a href="{{ url_for('dashboard.server_list') }}" class="btn btn-link me-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save Application</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
125
app/templates/dashboard/index.html
Normal file
125
app/templates/dashboard/index.html
Normal file
|
@ -0,0 +1,125 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">Dashboard</h2>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-primary d-none d-sm-inline-block">
|
||||
<i class="ti ti-plus"></i> New Server
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="h1 m-0">{{ server_count }}</div>
|
||||
<div class="text-muted mb-3">Servers</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{{ url_for('dashboard.server_list') }}" class="btn btn-sm btn-primary">
|
||||
View All
|
||||
</a>
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-sm btn-outline-primary ms-2">
|
||||
Add New
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="h1 m-0">{{ subnet_count }}</div>
|
||||
<div class="text-muted mb-3">Subnets</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{{ url_for('ipam.ipam_home') }}" class="btn btn-sm btn-primary">
|
||||
View All
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="h1 m-0">{{ app_count }}</div>
|
||||
<div class="text-muted mb-3">Applications</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Recent Servers</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if latest_servers %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for server in latest_servers %}
|
||||
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}"
|
||||
class="list-group-item list-group-item-action">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="avatar bg-primary text-white">
|
||||
{{ server.hostname[0].upper() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col text-truncate">
|
||||
<div class="d-block text-truncate">{{ server.hostname }}</div>
|
||||
<div class="text-muted text-truncate small">{{ server.ip_address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<div class="mb-3">No servers added yet</div>
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-outline-primary">
|
||||
Add Your First Server
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Subnet Utilization</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if subnets %}
|
||||
{% for subnet in subnets %}
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<div>{{ subnet.cidr }}</div>
|
||||
<div>{{ subnet.used_ips }} / 254 IPs</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-primary" style="width: {{ subnet.usage_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<div class="mb-3">No subnets added yet</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
61
app/templates/dashboard/server_form.html
Normal file
61
app/templates/dashboard/server_form.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Add New Server
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('dashboard.server_new') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">Hostname</label>
|
||||
<input type="text" class="form-control" name="hostname" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">IP Address</label>
|
||||
<input type="text" class="form-control" name="ip_address" placeholder="192.168.1.10" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">Subnet</label>
|
||||
<select class="form-select" name="subnet_id" required>
|
||||
<option value="">Select a subnet</option>
|
||||
{% for subnet in subnets %}
|
||||
<option value="{{ subnet.id }}">{{ subnet.cidr }} ({{ subnet.location }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Documentation</label>
|
||||
<textarea class="form-control" name="documentation" rows="6"
|
||||
placeholder="Use Markdown for formatting"></textarea>
|
||||
<small class="form-hint">Supports Markdown formatting</small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<a href="{{ url_for('dashboard.server_list') }}" class="btn btn-link me-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save Server</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
62
app/templates/dashboard/server_list.html
Normal file
62
app/templates/dashboard/server_list.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Servers
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i> Add Server
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
{% if servers %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP Address</th>
|
||||
<th>Subnet</th>
|
||||
<th>Created</th>
|
||||
<th class="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td>{{ server.hostname }}</td>
|
||||
<td>{{ server.ip_address }}</td>
|
||||
<td>{{ server.subnet.cidr if server.subnet else 'N/A' }}</td>
|
||||
<td>{{ server.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="btn btn-sm btn-primary">
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<div class="mb-3">No servers added yet</div>
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-primary">
|
||||
Add Your First Server
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
142
app/templates/dashboard/server_view.html
Normal file
142
app/templates/dashboard/server_view.html
Normal file
|
@ -0,0 +1,142 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
{{ server.hostname }}
|
||||
</h2>
|
||||
<div class="text-muted mt-1">{{ server.ip_address }}</div>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a href="{{ url_for('dashboard.server_list') }}" class="btn btn-link">
|
||||
Back to Servers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Documentation</h3>
|
||||
</div>
|
||||
<div class="card-body markdown-body">
|
||||
{% if server.documentation %}
|
||||
{{ markdown(server.documentation)|safe }}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
No documentation available for this server.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h3 class="card-title">Applications</h3>
|
||||
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-sm btn-primary">
|
||||
Add Application
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if apps %}
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Ports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for app in apps %}
|
||||
<tr>
|
||||
<td>{{ app.name }}</td>
|
||||
<td>
|
||||
{% for port in app.ports %}
|
||||
<span class="badge bg-primary">
|
||||
{{ port.port }}/{{ port.type }} {% if port.desc %}({{ port.desc }}){% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<div class="mb-3">No applications registered for this server</div>
|
||||
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-outline-primary">
|
||||
Add Application
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Server Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<strong>Hostname:</strong> {{ server.hostname }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>IP Address:</strong> {{ server.ip_address }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Subnet:</strong> {{ server.subnet.cidr if server.subnet else 'N/A' }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Location:</strong> {{ server.subnet.location if server.subnet else 'N/A' }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Created:</strong> {{ server.created_at.strftime('%Y-%m-%d') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Open Ports</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if server.get_open_ports() %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for port in server.get_open_ports() %}
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="badge bg-primary">{{ port.port }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="text-truncate">
|
||||
{{ port.type|upper }}
|
||||
{% if port.desc %}
|
||||
<span class="text-muted">{{ port.desc }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
No open ports detected.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
14
app/templates/errors/404.html
Normal file
14
app/templates/errors/404.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container text-center py-5">
|
||||
<div class="display-1 text-muted mb-3">404</div>
|
||||
<h1 class="h2 mb-3">Page not found</h1>
|
||||
<p class="h4 text-muted font-weight-normal mb-4">
|
||||
We are sorry but the page you are looking for was not found.
|
||||
</p>
|
||||
<a href="{{ url_for('dashboard.dashboard_home') }}" class="btn btn-primary">
|
||||
<i class="ti ti-arrow-left me-2"></i> Return to dashboard
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
14
app/templates/errors/500.html
Normal file
14
app/templates/errors/500.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container text-center py-5">
|
||||
<div class="display-1 text-muted mb-3">500</div>
|
||||
<h1 class="h2 mb-3">Internal Server Error</h1>
|
||||
<p class="h4 text-muted font-weight-normal mb-4">
|
||||
Something went wrong on our end. Please try again later.
|
||||
</p>
|
||||
<a href="{{ url_for('dashboard.dashboard_home') }}" class="btn btn-primary">
|
||||
<i class="ti ti-arrow-left me-2"></i> Return to dashboard
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
56
app/templates/import_form.html
Normal file
56
app/templates/import_form.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Import {{ model_name|capitalize }} Data</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data" id="import-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">CSV File</label>
|
||||
<input type="file" name="file" class="form-control" accept=".csv" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">Import</button>
|
||||
<a href="/import-export/template/{{ model_name }}" class="btn btn-outline-secondary">
|
||||
Download Template
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="import-result" class="mt-3" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('import-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('/import-export/import/{{ model_name }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const resultDiv = document.getElementById('import-result');
|
||||
resultDiv.style.display = 'block';
|
||||
|
||||
if (data.error) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-success">${data.success}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
document.getElementById('import-result').innerHTML =
|
||||
'<div class="alert alert-danger">An error occurred during import</div>';
|
||||
document.getElementById('import-result').style.display = 'block';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
60
app/templates/ipam/dashboard.html
Normal file
60
app/templates/ipam/dashboard.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>IPAM Dashboard</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Subnetz-Übersicht</h3>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-primary" hx-get="/ipam/subnet/new" hx-target="#modal-container">
|
||||
Neues Subnetz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CIDR</th>
|
||||
<th>Standort</th>
|
||||
<th>IP-Belegung</th>
|
||||
<th>Auto-Scan</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for subnet in subnets %}
|
||||
<tr>
|
||||
<td>{{ subnet.cidr }}</td>
|
||||
<td>{{ subnet.location }}</td>
|
||||
<td>
|
||||
<div class="progress progress-sm">
|
||||
{% set percentage = (subnet.used_ips / subnet.total_ips * 100) | int %}
|
||||
<div class="progress-bar bg-blue" style="width: {{ percentage }}%" role="progressbar"
|
||||
aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{ percentage }}%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ "Ja" if subnet.auto_scan else "Nein" }}</td>
|
||||
<td>
|
||||
<a href="/ipam/subnet/{{ subnet.id }}" class="btn btn-sm btn-outline-primary">Details</a>
|
||||
<button hx-post="/ipam/subnet/{{ subnet.id }}/scan" hx-swap="none"
|
||||
class="btn btn-sm btn-outline-secondary" {% if not subnet.auto_scan %}disabled{% endif %}>
|
||||
Scan starten
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-container"></div>
|
||||
{% endblock %}
|
81
app/templates/ipam/index.html
Normal file
81
app/templates/ipam/index.html
Normal file
|
@ -0,0 +1,81 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
IP Address Management
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<a href="{{ url_for('ipam.subnet_new') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i> Add Subnet
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
{% if subnets %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subnet</th>
|
||||
<th>Location</th>
|
||||
<th>Usage</th>
|
||||
<th>Auto Scan</th>
|
||||
<th class="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for subnet in subnets %}
|
||||
<tr>
|
||||
<td>{{ subnet.cidr }}</td>
|
||||
<td>{{ subnet.location }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-2">{{ subnet.used_ips }}/254</div>
|
||||
<div class="progress flex-grow-1" style="height: 5px;">
|
||||
<div class="progress-bar bg-primary" style="width: {{ subnet.usage_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if subnet.auto_scan %}
|
||||
<span class="badge bg-success">Enabled</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Disabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="btn btn-sm btn-primary">
|
||||
View
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
Scan
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<div class="mb-3">No subnets added yet</div>
|
||||
<a href="{{ url_for('ipam.subnet_new') }}" class="btn btn-primary">
|
||||
Add Your First Subnet
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
149
app/templates/ipam/subnet_detail.html
Normal file
149
app/templates/ipam/subnet_detail.html
Normal file
|
@ -0,0 +1,149 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="d-flex mb-3">
|
||||
<div>
|
||||
<h1>Subnetz: {{ subnet.cidr }}</h1>
|
||||
<p>Standort: {{ subnet.location }}</p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<button class="btn btn-outline-primary" hx-get="/ipam/subnet/{{ subnet.id }}/export" hx-swap="none">
|
||||
CSV-Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Subnetz-Details</h3>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Netzwerk-Adresse</label>
|
||||
<div>{{ subnet.cidr.split('/')[0] }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Präfixlänge</label>
|
||||
<div>{{ subnet.cidr.split('/')[1] }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Erste nutzbare IP</label>
|
||||
<div>{{ first_ip }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Letzte nutzbare IP</label>
|
||||
<div>{{ last_ip }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Gesamte IPs</label>
|
||||
<div>{{ total_ips }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Belegte IPs</label>
|
||||
<div>{{ used_ips }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">IP-Belegung</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="subnet-visualization" class="subnet-map">
|
||||
<!-- Wird via JavaScript befüllt -->
|
||||
<div class="text-center p-3">
|
||||
<div class="spinner-border text-blue" role="status"></div>
|
||||
<div>Lade Daten...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Load the subnet visualization
|
||||
fetch('/ipam/subnet/{{ subnet.id }}/visualization')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById('subnet-visualization');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create a grid of IP boxes
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'ip-grid';
|
||||
|
||||
data.forEach(ip => {
|
||||
const box = document.createElement('div');
|
||||
box.className = `ip-box ${ip.status}`;
|
||||
box.title = ip.hostname ? `${ip.ip} - ${ip.hostname}` : ip.ip;
|
||||
|
||||
const addressSpan = document.createElement('span');
|
||||
addressSpan.className = 'ip-address';
|
||||
addressSpan.textContent = ip.ip.split('.')[3]; // Only show last octet
|
||||
|
||||
box.appendChild(addressSpan);
|
||||
|
||||
if (ip.hostname) {
|
||||
const hostnameSpan = document.createElement('span');
|
||||
hostnameSpan.className = 'ip-hostname';
|
||||
hostnameSpan.textContent = ip.hostname;
|
||||
box.appendChild(hostnameSpan);
|
||||
}
|
||||
|
||||
grid.appendChild(box);
|
||||
});
|
||||
|
||||
container.appendChild(grid);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading subnet visualization:', error);
|
||||
document.getElementById('subnet-visualization').innerHTML =
|
||||
'<div class="alert alert-danger">Fehler beim Laden der Visualisierung</div>';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ip-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.ip-box {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ip-box.used {
|
||||
background-color: #e6f2ff;
|
||||
border-color: #99c2ff;
|
||||
}
|
||||
|
||||
.ip-box.available {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ip-hostname {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
51
app/templates/ipam/subnet_form.html
Normal file
51
app/templates/ipam/subnet_form.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Add New Subnet
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('ipam.subnet_new') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">CIDR Notation</label>
|
||||
<input type="text" class="form-control" name="cidr" placeholder="192.168.1.0/24" required>
|
||||
<small class="form-hint">Example: 192.168.1.0/24</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label required">Location</label>
|
||||
<input type="text" class="form-control" name="location" placeholder="Office, Datacenter, etc." required>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="auto_scan" name="auto_scan">
|
||||
<label class="form-check-label" for="auto_scan">Enable automatic scanning</label>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<a href="{{ url_for('ipam.ipam_home') }}" class="btn btn-link me-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save Subnet</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
132
app/templates/ipam/subnet_view.html
Normal file
132
app/templates/ipam/subnet_view.html
Normal file
|
@ -0,0 +1,132 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
{{ subnet.cidr }}
|
||||
</h2>
|
||||
<div class="text-muted mt-1">{{ subnet.location }}</div>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-search me-2"></i> Scan Subnet
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.subnet_visualize', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-chart-network me-2"></i> Visualize
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.ipam_home') }}" class="btn btn-link">
|
||||
Back to IPAM
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Registered Hosts</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if servers %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP Address</th>
|
||||
<th>Created</th>
|
||||
<th class="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td>{{ server.hostname }}</td>
|
||||
<td>{{ server.ip_address }}</td>
|
||||
<td>{{ server.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}"
|
||||
class="btn btn-sm btn-primary">
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<div class="mb-3">No hosts registered in this subnet</div>
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="btn btn-outline-primary">
|
||||
Add New Server
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}" class="btn btn-outline-primary ms-2">
|
||||
Scan Subnet
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Subnet Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<strong>Network:</strong> {{ subnet.cidr }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Location:</strong> {{ subnet.location }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Total IPs:</strong> {{ total_ips }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Used IPs:</strong> {{ used_ips }} ({{ '%.1f'|format(usage_percent) }}%)
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Available IPs:</strong> {{ total_ips - used_ips }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Auto Scan:</strong>
|
||||
{% if subnet.auto_scan %}
|
||||
<span class="badge bg-success">Enabled</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Disabled</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Created:</strong> {{ subnet.created_at.strftime('%Y-%m-%d') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Actions</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{{ url_for('ipam.subnet_scan', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-search me-2"></i> Scan Now
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.subnet_visualize', subnet_id=subnet.id) }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-chart-network me-2"></i> IP Visualization
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
206
app/templates/ipam/subnet_visualization.html
Normal file
206
app/templates/ipam/subnet_visualization.html
Normal file
|
@ -0,0 +1,206 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
IP Visualization - {{ subnet.cidr }}
|
||||
</h2>
|
||||
<div class="text-muted mt-1">{{ subnet.location }}</div>
|
||||
</div>
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="btn btn-link">
|
||||
Back to Subnet
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h3 class="card-title">IP Address Map</h3>
|
||||
<div>
|
||||
<span class="badge bg-success me-2">Available: {{ total_ips - used_ip_count }}</span>
|
||||
<span class="badge bg-danger">Used: {{ used_ip_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="ip-grid">
|
||||
{% set network_parts = subnet.cidr.split('/')[0].split('.') %}
|
||||
{% set network_prefix = network_parts[0] + '.' + network_parts[1] + '.' + network_parts[2] + '.' %}
|
||||
|
||||
{% for i in range(1, 255) %}
|
||||
{% set ip = network_prefix + i|string %}
|
||||
{% if ip in used_ips %}
|
||||
<div class="ip-cell used" title="{{ used_ips[ip] }}">
|
||||
{{ ip }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="ip-cell available" title="Available">
|
||||
{{ ip }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadIpMap();
|
||||
|
||||
// Handle scan button response
|
||||
document.body.addEventListener('htmx:afterRequest', function (event) {
|
||||
if (event.detail.target.matches('button[hx-post^="/ipam/subnet/"][hx-post$="/scan"]')) {
|
||||
if (event.detail.successful) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center show position-fixed bottom-0 end-0 m-3';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Subnet scan started. Results will be available shortly.
|
||||
</div>
|
||||
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 5000);
|
||||
|
||||
// Reload IP map after a delay to allow scan to complete
|
||||
setTimeout(() => {
|
||||
loadIpMap();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadIpMap() {
|
||||
const ipMap = document.getElementById('ip-map');
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
|
||||
fetch('/ipam/subnet/{{ subnet.id }}/visualization')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Hide loading indicator
|
||||
loadingIndicator.style.display = 'none';
|
||||
|
||||
// Create IP grid
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'ip-grid';
|
||||
|
||||
// Add each IP address to the grid
|
||||
data.forEach(ip => {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = `ip-cell ${ip.status}`;
|
||||
|
||||
// Add IP address
|
||||
const ipAddress = document.createElement('span');
|
||||
ipAddress.className = 'ip-address';
|
||||
ipAddress.textContent = ip.ip;
|
||||
cell.appendChild(ipAddress);
|
||||
|
||||
// Add hostname if exists
|
||||
if (ip.hostname) {
|
||||
const hostname = document.createElement('span');
|
||||
hostname.className = 'ip-hostname';
|
||||
hostname.textContent = ip.hostname;
|
||||
cell.appendChild(hostname);
|
||||
}
|
||||
|
||||
// Add click handler for details
|
||||
cell.addEventListener('click', () => {
|
||||
if (ip.status === 'used') {
|
||||
window.location.href = `/dashboard/server/${ip.server_id}`;
|
||||
}
|
||||
});
|
||||
|
||||
grid.appendChild(cell);
|
||||
});
|
||||
|
||||
ipMap.appendChild(grid);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading subnet visualization:', error);
|
||||
document.getElementById('ip-map').innerHTML =
|
||||
'<div class="alert alert-danger">Error loading subnet visualization. Please try again later.</div>';
|
||||
document.getElementById('loading-indicator').style.display = 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ip-grid-container {
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ip-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ip-cell {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ip-cell:hover {
|
||||
transform: scale(1.05);
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ip-cell.used {
|
||||
background-color: rgba(32, 107, 196, 0.1);
|
||||
border-color: rgba(32, 107, 196, 0.5);
|
||||
}
|
||||
|
||||
.ip-cell.available {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
border-color: rgba(5, 150, 105, 0.5);
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ip-hostname {
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
147
app/templates/layout.html
Normal file
147
app/templates/layout.html
Normal file
|
@ -0,0 +1,147 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title if title else 'Network Documentation' }}</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Tabler Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.22.0/tabler-icons.min.css">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
{% block styles %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="{{ 'auth-page' if not current_user.is_authenticated else '' }}">
|
||||
<!-- Notification Area -->
|
||||
<div id="notification-area"></div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Sidebar for authenticated users -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<span class="ti ti-network"></span>
|
||||
<span class="sidebar-brand-text">NetDocs</span>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-heading">Main</div>
|
||||
<a href="{{ url_for('dashboard.dashboard_home') }}"
|
||||
class="sidebar-item {{ 'active' if request.endpoint == 'dashboard.dashboard_home' }}">
|
||||
<span class="ti ti-dashboard me-2"></span> Dashboard
|
||||
</a>
|
||||
<a href="{{ url_for('dashboard.server_list') }}"
|
||||
class="sidebar-item {{ 'active' if request.endpoint and request.endpoint.startswith('dashboard.server') }}">
|
||||
<span class="ti ti-server me-2"></span> Servers
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.ipam_home') }}"
|
||||
class="sidebar-item {{ 'active' if request.endpoint and request.endpoint.startswith('ipam.') }}">
|
||||
<span class="ti ti-network me-2"></span> IPAM
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading">Management</div>
|
||||
<a href="{{ url_for('dashboard.server_new') }}" class="sidebar-item">
|
||||
<span class="ti ti-plus me-2"></span> Add Server
|
||||
</a>
|
||||
<a href="{{ url_for('ipam.subnet_new') }}" class="sidebar-item">
|
||||
<span class="ti ti-plus me-2"></span> Add Subnet
|
||||
</a>
|
||||
|
||||
<div class="sidebar-heading">User</div>
|
||||
<a href="{{ url_for('auth.logout') }}" class="sidebar-item">
|
||||
<span class="ti ti-logout me-2"></span> Logout
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content for authenticated users -->
|
||||
<div class="main-content">
|
||||
<!-- Top Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom mb-4">
|
||||
<div class="container-fluid">
|
||||
<button class="sidebar-toggler btn btn-outline-secondary d-lg-none me-2">
|
||||
<span class="ti ti-menu-2"></span>
|
||||
</button>
|
||||
|
||||
<span class="navbar-brand d-none d-lg-block">
|
||||
{{ title if title else 'Network Documentation' }}
|
||||
</span>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown">
|
||||
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle"
|
||||
id="user-dropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="d-none d-md-inline me-2">{{ current_user.email }}</span>
|
||||
<span class="ti ti-user"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
|
||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<div class="container-fluid mt-3">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Simple flash message container for non-authenticated users -->
|
||||
<div class="container mt-3">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ONLY ONE CONTENT BLOCK FOR BOTH AUTHENTICATED AND NON-AUTHENTICATED STATES -->
|
||||
<div class="{{ 'py-4' if current_user.is_authenticated else '' }}">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
</div> <!-- End of main-content div that was opened for authenticated users -->
|
||||
{% endif %}
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- HTMX for dynamic content -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
||||
<!-- Custom JS -->
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
<script>
|
||||
// Sidebar toggle for mobile - using modern event listener approach
|
||||
const sidebarToggler = document.querySelector('.sidebar-toggler');
|
||||
if (sidebarToggler) {
|
||||
sidebarToggler.addEventListener('click', () => {
|
||||
document.querySelector('.sidebar').classList.toggle('show');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
15
app/templates/server.html
Normal file
15
app/templates/server.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Server Details - {{ server.hostname }}</h2>
|
||||
<div>
|
||||
<p><strong>IP Address:</strong> {{ server.ip_address }}</p>
|
||||
<p><strong>Open Ports:</strong> {{ server.get_open_ports() | join(', ') }}</p>
|
||||
<div class="markdown-body">
|
||||
<h3>Documentation:</h3>
|
||||
<div id="markdown-preview">
|
||||
{{ markdown(server.documentation) | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue