initial
This commit is contained in:
99
frontend/templates/frontend/base.html
Normal file
99
frontend/templates/frontend/base.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!-- frontend/templates/frontend/base.html -->
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Container Management{% block title %}{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet"/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
.form-control, select, input, textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
select, input, textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
i.bi {
|
||||
margin-right:.5em;
|
||||
}
|
||||
.btn-group i.bi{
|
||||
margin: initial;
|
||||
}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'frontend:dashboard' %}">Container Management</a>
|
||||
{% if user.is_authenticated %}
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'frontend:dashboard' %}">Dev Container</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'frontend:dns_list' %}">DNS Management</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'frontend:faq_list' %}">FAQ</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'tasklogger:task_list' %}">Tasks</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<span class="nav-link">{{ user.username }} ({{ user.profile.user_group }})</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'frontend:logout' %}?next={{ request.path }}">Logout</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{% url 'manager:sync_all' %}"><i class="btn bi bi-arrow-repeat"></i> </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-5">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show"
|
||||
onclick="$(this).fadeOut(function(){$(this).slideUp();})" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
48
frontend/templates/frontend/container_details.html
Normal file
48
frontend/templates/frontend/container_details.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!-- frontend/templates/frontend/container_detail.html -->
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Container Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% url 'frontend:edit_container' as editurl %}
|
||||
{% url 'frontend:delete_container' as deleteurl %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Container: {{ container.name }}</h2>
|
||||
<div>
|
||||
<a href="{{ editurl }}" class="btn btn-warning">Edit</a>
|
||||
<a href="{{ deleteurl }}" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Container details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Name:</strong> {{ container.name }}</p>
|
||||
<p><strong>IP-Adresse:</strong> {{ container.address }}</p>
|
||||
<p><strong>Hostname:</strong> {{ container.hostname }}</p>
|
||||
<p><strong>MAC-Adresse:</strong> {{ container.hwaddr }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Status:</strong> {{ container.status }}</p>
|
||||
<p><strong>Speicher:</strong> {{ container.lxc.memory }} MB</p>
|
||||
<p><strong>CPU-Kerne:</strong> {{ container.lxc.cores }}</p>
|
||||
<p><strong>Festplatte:</strong> {{ container.lxc.disksize }} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>DNS-Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>DNS-Name:</strong> {{ container.dns.name|default:"--" }}</p>
|
||||
<p><strong>DNS-Regex:</strong> {{ container.dns.regexp|default:"--" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
342
frontend/templates/frontend/create_container.html
Normal file
342
frontend/templates/frontend/create_container.html
Normal file
@@ -0,0 +1,342 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} - Create Dev Container{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<!-- Rest of your existing content -->
|
||||
<div class="col-12 col-md-10 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">Create Dev Container</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_uuid" value="{{ task_uuid }}">
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-10">
|
||||
<label for="{{ form.hostname.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.hostname.label }}</label>
|
||||
{{ form.hostname }}
|
||||
{% if form.hostname.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.hostname.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.hostname.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.hostname.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2" title="{{ form.as_regexp.help_text }}">
|
||||
<label for="{{ form.as_regexp.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.as_regexp.label }}</label>
|
||||
{{ form.as_regexp }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-{% if request.GET.clone_lxc %}12{% else %}6{% endif %}">
|
||||
|
||||
<label for="{{ form.vm.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.vm.label }}</label>
|
||||
{{ form.vm }}
|
||||
{% if form.vm.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.vm.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.vm.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.vm.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% if not request.GET.clone_lxc %}
|
||||
<div class="col-md-1 text-center">
|
||||
<label class="form-label fw-bold">or</label>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="{{ form.template.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.template.label }}</label>
|
||||
{{ form.template }}
|
||||
{% if form.template.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.template.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.template.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.template.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Network-Feld -->
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.network.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.network.label }}</label>
|
||||
{{ form.network }}
|
||||
{% if form.network.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.network.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.network.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.network.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Dritte Zeile: cores, memory und disksize -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.cores.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.cores.label }}</label>
|
||||
{{ form.cores }}
|
||||
{% if form.cores.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.cores.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.cores.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.cores.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.memory.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.memory.label }}</label>
|
||||
{{ form.memory }}
|
||||
{% if form.memory.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.memory.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.memory.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.memory.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.disksize.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.disksize.label }}</label>
|
||||
{{ form.disksize }}
|
||||
{% if form.disksize.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.disksize.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.disksize.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.disksize.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beschreibung (falls vorhanden) -->
|
||||
{% if form.description %}
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.description.id_for_label }}"
|
||||
class="form-label fw-bold">{{ form.description.label }}</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ form.description.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in form.description.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for field in form %}
|
||||
{% if field.name != 'name' and field.name != 'hostname' and field.name != 'as_regexp' and field.name != 'template' and field.name != 'vm' and field.name != 'network' and field.name != 'cores' and field.name != 'memory' and field.name != 'disksize' and field.name != 'description' %}
|
||||
<div class="mb-4">
|
||||
<label for="{{ field.id_for_label }}"
|
||||
class="form-label fw-bold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text small text-muted mt-1">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block mt-1">
|
||||
{% for error in field.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
|
||||
<a href="{% url 'frontend:dashboard' %}"
|
||||
class="btn btn-outline-secondary me-md-2 mb-2 mb-md-0 w-100 w-md-auto">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-primary w-100 w-md-auto" id="create-btn">Create
|
||||
Container
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Progress Overlay -->
|
||||
<div id="task-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 9999; color: white;">
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e1e; padding: 30px; border-radius: 10px; min-width: 500px; max-width: 700px;">
|
||||
<h4>Creating Container...</h4>
|
||||
<div class="task-info">
|
||||
<small>Task UUID: <span id="task-uuid">{{ task_uuid }}</span></small>
|
||||
</div>
|
||||
<div id="task-logs" style="background: #000; padding: 15px; border-radius: 5px; margin: 15px 0; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 12px;">
|
||||
<div class="log-entry">Initializing...</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- jQuery und Select2 JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
let taskMonitorInterval = null;
|
||||
|
||||
$(document).ready(function () {
|
||||
// Select2 für alle select-Elemente aktivieren
|
||||
$('#id_network').select2({
|
||||
width: '100%'
|
||||
});
|
||||
$('#id_vm').select2({
|
||||
width: '100%'
|
||||
});
|
||||
$('#id_template').children('option').each(function () {
|
||||
$(this).text($(this).text().split('/')[1]);
|
||||
});
|
||||
$('#id_template').select2({
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
// Form submission with async task monitoring
|
||||
$('form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$('#create-btn').prop('disabled', true).text('Creating Container...');
|
||||
|
||||
// Submit form via AJAX
|
||||
$.ajax({
|
||||
url: $(this).attr('action'),
|
||||
method: 'POST',
|
||||
data: $(this).serialize(),
|
||||
success: function(response) {
|
||||
if (response.status === 'task_started') {
|
||||
// Show overlay and start monitoring
|
||||
$('#task-uuid').text(response.task_uuid);
|
||||
$('#task-overlay').show();
|
||||
startTaskMonitoring(response.task_uuid);
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
$('#create-btn').prop('disabled', false).text('Create Container');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMsg = 'Network error';
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
errorMsg = response.message || errorMsg;
|
||||
} catch (e) {}
|
||||
|
||||
alert('Error: ' + errorMsg);
|
||||
$('#create-btn').prop('disabled', false).text('Create Container');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function startTaskMonitoring(taskUuid) {
|
||||
let entryCount = 0;
|
||||
|
||||
taskMonitorInterval = setInterval(function() {
|
||||
$.get('{% url "tasklogger:task_status" %}', {'task_uuid': taskUuid})
|
||||
.done(function(data) {
|
||||
if (data.status === 'success') {
|
||||
const task = data.task;
|
||||
const entries = task.entries || [];
|
||||
|
||||
// Update logs (only new entries)
|
||||
if (entries.length > entryCount) {
|
||||
const newEntries = entries.slice(entryCount);
|
||||
newEntries.forEach(function(entry) {
|
||||
const logEntry = $('<div class="log-entry"></div>');
|
||||
const timestamp = new Date(entry.created_at).toLocaleTimeString();
|
||||
let message = '';
|
||||
|
||||
if (typeof entry.message === 'object') {
|
||||
message = JSON.stringify(entry.message, null, 2);
|
||||
} else {
|
||||
message = entry.message || 'No message';
|
||||
}
|
||||
|
||||
logEntry.html('<span style="color: #666;">' + timestamp + '</span> ' + message);
|
||||
$('#task-logs').append(logEntry);
|
||||
});
|
||||
|
||||
// Scroll to bottom
|
||||
$('#task-logs').scrollTop($('#task-logs')[0].scrollHeight);
|
||||
entryCount = entries.length;
|
||||
}
|
||||
|
||||
// Check if task completed
|
||||
if (task.status === 'completed') {
|
||||
clearInterval(taskMonitorInterval);
|
||||
$('.progress-bar').removeClass('progress-bar-animated').addClass('bg-success').css('width', '100%');
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.href = "{% url 'frontend:dashboard' %}";
|
||||
}, 2000);
|
||||
} else if (task.status === 'error') {
|
||||
clearInterval(taskMonitorInterval);
|
||||
$('.progress-bar').removeClass('progress-bar-animated').addClass('bg-danger').css('width', '100%');
|
||||
|
||||
// Add close button
|
||||
$('#task-overlay .progress').after('<button class="btn btn-danger mt-3" onclick="window.location.href=\'{% url "frontend:dashboard" %}\'">Close</button>');
|
||||
}
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
console.error('Failed to get task status');
|
||||
});
|
||||
}, 1000); // Check every second
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
387
frontend/templates/frontend/dashboard.html
Normal file
387
frontend/templates/frontend/dashboard.html
Normal file
@@ -0,0 +1,387 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>
|
||||
<i class="bi bi-pc-display-horizontal"></i> Dev Container
|
||||
</h3>
|
||||
{% if request.user.is_staff %}
|
||||
<div role="group">
|
||||
<a href="{% url 'frontend:create_container' %}"
|
||||
class="btn btn-primary"
|
||||
><i class="bi bi-plus-circle"></i> Create Dev Container</a>
|
||||
{% if default_template %}
|
||||
<a href="{% url 'frontend:create_container' %}?clone_template={{ default_template.pk }}"
|
||||
class="btn btn-secondary"
|
||||
><i class="bi bi-plus-circle"></i> Create from default Template</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% url 'frontend:dashboard' as search_action_url %}
|
||||
{% include 'frontend/includes/pagination_snippet.html' %}
|
||||
|
||||
<div id="cardView" class="row">
|
||||
{% for container in page_obj %}
|
||||
<div class="col-12 col-md-6 col-lg-4 mb-3 container-item" data-crow="{{ container.internal_id }}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- LXC Status -->
|
||||
<div class="me-2" title="LXC Status: {{ container.lxc.status }}">
|
||||
<span class="status-circle status-lxc {{ container.lxc.status }} lxc-status-{{ container.internal_id }}"></span>
|
||||
</div>
|
||||
<div class="me-3" title="Network Status: {{ container.lease.status }}">
|
||||
<span class="status-circle status-lease {{ container.lxc.status }} lease_status_{{ container.internal_id }}"></span>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-0">{{ container.vmid }} {{ container.name }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2"><strong
|
||||
style="width:25%;display: inline-block">Hostname:</strong> {{ container.hostname }}
|
||||
</div>
|
||||
<div class="mb-2"><strong
|
||||
style="width:25%;display: inline-block">IP-Address:</strong> {{ container.address }}
|
||||
</div>
|
||||
<div class="mb-2"><strong style="width: 25%;display: inline-block;">Root
|
||||
Disk:</strong> {{ container.lxc.disksize }} GB
|
||||
</div>
|
||||
<div class="mb-2"><strong
|
||||
style="width: 25%;display: inline-block;">Memory:</strong> {{ container.lxc.memory }}
|
||||
MB
|
||||
</div>
|
||||
<div class="mb-2"><strong
|
||||
style="width: 25%;display: inline-block;">Cores:</strong> {{ container.lxc.cores }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button onclick="openProxmoxConsole('{{ container.lxc.proxmox_console_url }}', '{{ container.lxc.vmid }}')"
|
||||
class="btn btn-secondary terminal terminal-{{ container.pk }}"
|
||||
title="Open Proxmox Console">
|
||||
<i class="bi bi-terminal"></i>
|
||||
</button>
|
||||
<a href="{% url 'frontend:container_detail' container.pk %}"
|
||||
class="btn btn-info" title="Details">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:edit_container' container.pk %}"
|
||||
class="btn btn-warning" title="Edit">
|
||||
<i class="bi bi-gear"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:create_container' %}?clone_lxc={{ container.pk }}"
|
||||
class="btn btn-secondary btn-sm" title="Clone">
|
||||
<i class="bi bi-copy"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:delete_container' container.pk %}"
|
||||
class="btn btn-danger" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info text-center">
|
||||
{% if request.GET.search %}
|
||||
No Container for "{{ request.GET.search }}".
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{% url 'frontend:dashboard' %}" class="btn btn-sm btn-secondary mt-2"><i
|
||||
class="bi bi-trash"> Clear Search</i></a>
|
||||
</div>
|
||||
{% else %}
|
||||
No Containers found
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="listView" class="row d-none">
|
||||
<div class="col-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stati</th>
|
||||
<th>Name</th>
|
||||
<th>Hostname</th>
|
||||
<th>IP-Address</th>
|
||||
<th>Root Disk</th>
|
||||
<th>Memory</th>
|
||||
<th>Cores</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for container in page_obj %}
|
||||
<tr class="container-item" data-crow="{{ container.internal_id }}">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- LXC Status -->
|
||||
<div class="me-2" title="LXC Status: {{ container.lxc.status }}">
|
||||
<span class="status-circle {{ container.lxc.status }} lxc_status_{{ container.internal_id }}"></span>
|
||||
</div>
|
||||
<!-- Network Status -->
|
||||
<div title="Network Status: {{ container.status }}">
|
||||
<span class="status-circle {{ container.lease.status }} lease_status_{{ container.internal_id }}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ container.vmid }} {{ container.name }}</td>
|
||||
<td>{{ container.hostname }}</td>
|
||||
<td>{{ container.address }}</td>
|
||||
<td>{{ container.lxc.disksize }} GB</td>
|
||||
<td>{{ container.lxc.memory }} MB</td>
|
||||
<td>{{ container.lxc.cores }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<button onclick="openProxmoxConsole('{{ container.lxc.proxmox_console_url }}', '{{ container.lxc.vmid }}')"
|
||||
class="btn btn-sm btn-secondary terminal terminal-{{ container.pk }}"
|
||||
title="Open Proxmox Console">
|
||||
<i class="bi bi-terminal"></i>
|
||||
</button>
|
||||
<a href="{% url 'frontend:container_detail' container.pk %}"
|
||||
class="btn btn-info btn-sm" title="Details">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:edit_container' container.pk %}"
|
||||
class="btn btn-warning btn-sm" title="Edit">
|
||||
<i class="bi bi-gear"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:create_container' %}?clone_lxc={{ container.lxc.pk }}"
|
||||
class="btn btn-secondary btn-sm" title="Clone">
|
||||
<i class="bi bi-copy"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:delete_container' container.pk %}"
|
||||
class="btn btn-danger btn-sm" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">
|
||||
{% if request.GET.search %}
|
||||
No Container for "{{ request.GET.search }}".
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{% url 'frontend:dashboard' %}"
|
||||
class="btn btn-sm btn-secondary mt-2"><i class="bi bi-trash"> Clear
|
||||
Search</i></a>
|
||||
</div>
|
||||
{% else %}
|
||||
No Containers found
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'frontend/includes/pagination_snippet.html' %}
|
||||
|
||||
<style>
|
||||
.status-circle {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
.status-low,
|
||||
.status-low td,
|
||||
.status-low div {
|
||||
background-color: #f9aea8 !important;
|
||||
}
|
||||
|
||||
.status-circle.active,
|
||||
.status-circle.running,
|
||||
.status-circle.bound {
|
||||
background-color: darkgreen;
|
||||
}
|
||||
|
||||
.status-circle.pending {
|
||||
background-color: orange;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.status-circle.error {
|
||||
background-color: red;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.terminal {
|
||||
background-color: #444444;
|
||||
border-color: #444444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.terminal-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var respd = false;
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const cardViewBtn = document.getElementById('cardViewBtn');
|
||||
const listViewBtn = document.getElementById('listViewBtn');
|
||||
const cardView = document.getElementById('cardView');
|
||||
const listView = document.getElementById('listView');
|
||||
|
||||
let viewMode = localStorage.getItem('containerViewMode') || '{{ user_profile.settings.dashboard_view }}';
|
||||
|
||||
'{{page_ids }}'.split(',').forEach(function (id) {
|
||||
if (!id) {
|
||||
console.warn('Current Container called but id is empty - {{ page_ids }}');
|
||||
return;
|
||||
}
|
||||
$.get("{% url 'frontend:current_container_details' %}", {'ids': id}, function (container) {
|
||||
container = container[0]
|
||||
if (container.is_low) {
|
||||
$('[data-crow="' + id + '"]').addClass(
|
||||
'status-low'
|
||||
).attr(
|
||||
'title',
|
||||
'Low Warning: Disk: ' + container.disk_percent + '%, Memory: ' + container.mem_percent + '%'
|
||||
);
|
||||
}
|
||||
let lxc_status = $('.lxc_status_' + id);
|
||||
let lease_status = $('.lease_status_' + id);
|
||||
lxc_status.removeClass('stopped,running)').addClass(container.lxc_status).css('cursor', 'pointer');
|
||||
/*
|
||||
if(container.lxc_status === 'running'){
|
||||
$('.terminal-' + id).removeClass('terminal-none')
|
||||
}else{
|
||||
$('.terminal-' + id).addClass('terminal-none')
|
||||
}
|
||||
*/
|
||||
lxc_status.attr('title', 'LXC Status: ' + container.lxc_status);
|
||||
let stopurl = "{% url 'frontend:stop_lxc' 0 %}";
|
||||
let starturl = "{% url 'frontend:start_lxc' 0 %}";
|
||||
|
||||
lxc_status.click(function () {
|
||||
if (container.lxc_status === 'stopped') {
|
||||
performContainerAction(id, 'start', starturl, lxc_status);
|
||||
} else {
|
||||
performContainerAction(id, 'stop', stopurl, lxc_status);
|
||||
}
|
||||
});
|
||||
lease_status.removeClass('bound,waiting)').addClass(container.lease_status);
|
||||
lease_status.attr('title', 'Network Status: ' + container.lease_status);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function checkScreenSizeAndAdjustView() {
|
||||
const isMobileOrTablet = window.innerWidth < 768;
|
||||
|
||||
if (isMobileOrTablet && viewMode === 'list') {
|
||||
cardView.classList.remove('d-none');
|
||||
listView.classList.add('d-none');
|
||||
cardViewBtn.classList.add('active');
|
||||
listViewBtn.classList.remove('active');
|
||||
} else {
|
||||
if (viewMode === 'list') {
|
||||
cardView.classList.add('d-none');
|
||||
listView.classList.remove('d-none');
|
||||
cardViewBtn.classList.remove('active');
|
||||
listViewBtn.classList.add('active');
|
||||
} else {
|
||||
cardView.classList.remove('d-none');
|
||||
listView.classList.add('d-none');
|
||||
cardViewBtn.classList.add('active');
|
||||
listViewBtn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkScreenSizeAndAdjustView();
|
||||
|
||||
window.addEventListener('resize', checkScreenSizeAndAdjustView);
|
||||
|
||||
function chView(vm) {
|
||||
viewMode = vm;
|
||||
localStorage.setItem('containerViewMode', viewMode);
|
||||
checkScreenSizeAndAdjustView();
|
||||
/*
|
||||
$.post('{ % url 'manager:set_view_mode' % }', {'mode': viewMode}, function (data) {
|
||||
console.log(data);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
cardViewBtn.addEventListener('click', function () {
|
||||
chView('card');
|
||||
});
|
||||
|
||||
listViewBtn.addEventListener('click', function () {
|
||||
chView('list');
|
||||
});
|
||||
});
|
||||
|
||||
function openProxmoxConsole(url, vmid) {
|
||||
if (!localStorage.getItem('pm-warning-confirmed')) {
|
||||
if (!confirm('You have to be logged in in the Proxmox to open a Console here.\n\n')) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('pm-warning-confirmed', 'true');
|
||||
}
|
||||
window.open(
|
||||
url,
|
||||
'proxmox-console-' + vmid,
|
||||
'width=800,height=600,scrollbars=yes,resizable=yes,status=yes,toolbar=yes,menubar=no,location=yes'
|
||||
);
|
||||
}
|
||||
|
||||
function performContainerAction(containerId, action, url, statusElement) {
|
||||
// Simply redirect to the URL (synchronous operation with form-blocking)
|
||||
window.location.href = url + '?id=' + containerId;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
138
frontend/templates/frontend/delete_container.html
Normal file
138
frontend/templates/frontend/delete_container.html
Normal file
@@ -0,0 +1,138 @@
|
||||
<!-- frontend/templates/frontend/delete_container.html -->
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Container löschen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Container löschen</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Sind Sie sicher, dass Sie den Container <strong>{{ container.name }}</strong> löschen möchten?</p>
|
||||
<p class="text-danger">Diese Aktion kann nicht rückgängig gemacht werden!</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_uuid" value="{{ task_uuid }}" />
|
||||
<button type="submit" class="btn btn-danger" id="delete-btn">Ja, löschen</button>
|
||||
<a href="{% url 'frontend:container_detail' container.internal_id %}" class="btn btn-secondary">Abbrechen</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Progress Overlay -->
|
||||
<div id="task-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 9999; color: white;">
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e1e; padding: 30px; border-radius: 10px; min-width: 500px; max-width: 700px;">
|
||||
<h4>Deleting Container...</h4>
|
||||
<div class="task-info">
|
||||
<small>Task UUID: <span id="task-uuid"></span></small>
|
||||
</div>
|
||||
<div id="task-logs" style="background: #000; padding: 15px; border-radius: 5px; margin: 15px 0; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 12px;">
|
||||
<div class="log-entry">Initializing...</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let taskMonitorInterval = null;
|
||||
|
||||
$(document).ready(function () {
|
||||
// Form submission with async task monitoring
|
||||
$('form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$('#delete-btn').prop('disabled', true).text('Lösche Container...');
|
||||
|
||||
// Submit form via AJAX
|
||||
$.ajax({
|
||||
url: $(this).attr('action'),
|
||||
method: 'POST',
|
||||
data: $(this).serialize(),
|
||||
success: function(response) {
|
||||
if (response.status === 'task_started') {
|
||||
// Show overlay and start monitoring
|
||||
$('#task-uuid').text(response.task_uuid);
|
||||
$('#task-overlay').show();
|
||||
startTaskMonitoring(response.task_uuid);
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
$('#delete-btn').prop('disabled', false).text('Ja, löschen');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMsg = 'Network error';
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
errorMsg = response.message || errorMsg;
|
||||
} catch (e) {}
|
||||
|
||||
alert('Error: ' + errorMsg);
|
||||
$('#delete-btn').prop('disabled', false).text('Ja, löschen');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function startTaskMonitoring(taskUuid) {
|
||||
let entryCount = 0;
|
||||
|
||||
taskMonitorInterval = setInterval(function() {
|
||||
$.get('{% url "tasklogger:task_status" %}', {'task_uuid': taskUuid})
|
||||
.done(function(data) {
|
||||
if (data.status === 'success') {
|
||||
const task = data.task;
|
||||
const entries = task.entries || [];
|
||||
|
||||
// Update logs (only new entries)
|
||||
if (entries.length > entryCount) {
|
||||
const newEntries = entries.slice(entryCount);
|
||||
newEntries.forEach(function(entry) {
|
||||
const logEntry = $('<div class="log-entry"></div>');
|
||||
const timestamp = new Date(entry.created_at).toLocaleTimeString();
|
||||
let message = '';
|
||||
|
||||
if (typeof entry.message === 'object') {
|
||||
message = JSON.stringify(entry.message, null, 2);
|
||||
} else {
|
||||
message = entry.message || 'No message';
|
||||
}
|
||||
|
||||
logEntry.html('<span style="color: #666;">' + timestamp + '</span> ' + message);
|
||||
$('#task-logs').append(logEntry);
|
||||
});
|
||||
|
||||
// Scroll to bottom
|
||||
$('#task-logs').scrollTop($('#task-logs')[0].scrollHeight);
|
||||
entryCount = entries.length;
|
||||
}
|
||||
|
||||
// Check if task completed
|
||||
if (task.status === 'completed') {
|
||||
clearInterval(taskMonitorInterval);
|
||||
$('.progress-bar').removeClass('progress-bar-animated').addClass('bg-success').css('width', '100%');
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.href = "{% url 'frontend:dashboard' %}";
|
||||
}, 2000);
|
||||
} else if (task.status === 'error') {
|
||||
clearInterval(taskMonitorInterval);
|
||||
$('.progress-bar').removeClass('progress-bar-animated').addClass('bg-danger').css('width', '100%');
|
||||
|
||||
// Add close button
|
||||
$('#task-overlay .progress').after('<button class="btn btn-danger mt-3" onclick="window.location.href=\'{% url "frontend:dashboard" %}\'">Close</button>');
|
||||
}
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
console.error('Failed to get task status');
|
||||
});
|
||||
}, 1000); // Check every second
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
75
frontend/templates/frontend/dns_delete.html
Normal file
75
frontend/templates/frontend/dns_delete.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Delete DNS Entry{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete DNS Entry
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<strong>Warning!</strong> This action cannot be undone.
|
||||
</div>
|
||||
|
||||
<p>Are you sure you want to delete the following DNS entry?</p>
|
||||
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Type:</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if dns_entry.name %}
|
||||
<span class="badge bg-primary">DNS Name</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">RegExp Pattern</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">
|
||||
{% if dns_entry.name %}Name:{% else %}Pattern:{% endif %}
|
||||
</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if dns_entry.name %}
|
||||
<strong>{{ dns_entry.name }}</strong>
|
||||
{% else %}
|
||||
<code>{{ dns_entry.regexp }}</code>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">IP Address:</dt>
|
||||
<dd class="col-sm-9">
|
||||
<span class="badge bg-secondary">{{ dns_entry.address }}</span>
|
||||
</dd>
|
||||
|
||||
{% if dns_entry.comment %}
|
||||
<dt class="col-sm-3">Comment:</dt>
|
||||
<dd class="col-sm-9">{{ dns_entry.comment }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-4">
|
||||
{% csrf_token %}
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'frontend:dns_list' %}" class="btn btn-outline-secondary me-md-2">
|
||||
<i class="bi bi-arrow-left"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Delete DNS Entry
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
266
frontend/templates/frontend/dns_form.html
Normal file
266
frontend/templates/frontend/dns_form.html
Normal file
@@ -0,0 +1,266 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} - {{ title }}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
<!-- Select2 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
.form-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.exclusive-fields {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-book"></i>
|
||||
{{ title }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- IP Address Section -->
|
||||
<div class="form-section">
|
||||
<h5><i class="bi bi-router"></i> IP Address</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.container.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.container.label }}
|
||||
</label>
|
||||
{{ form.container }}
|
||||
{% if form.container.help_text %}
|
||||
<div class="form-text">{{ form.container.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.container.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.container.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="text-center my-3">
|
||||
<strong>OR</strong>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.address.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.address.label }}
|
||||
</label>
|
||||
{{ form.address }}
|
||||
{% if form.address.help_text %}
|
||||
<div class="form-text">{{ form.address.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.address.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.address.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Configuration Section -->
|
||||
<div class="form-section">
|
||||
<h5><i class="bi bi-dns"></i> DNS Configuration</h5>
|
||||
|
||||
<div class="exclusive-fields">
|
||||
<strong>Choose ONE of the following:</strong>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.name.label }}
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.help_text %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.name.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="text-center my-3">
|
||||
<strong>OR</strong>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.regexp.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.regexp.label }}
|
||||
</label>
|
||||
{{ form.regexp }}
|
||||
{% if form.regexp.help_text %}
|
||||
<div class="form-text">{{ form.regexp.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.regexp.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.regexp.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comment Section -->
|
||||
<div class="form-section">
|
||||
<h5><i class="bi bi-chat-text"></i> Additional Information</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.comment.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.comment.label }}
|
||||
</label>
|
||||
{{ form.comment }}
|
||||
{% if form.comment.help_text %}
|
||||
<div class="form-text">{{ form.comment.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.comment.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.comment.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Validation Errors -->
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Please correct the following errors:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'frontend:dns_list' %}" class="btn btn-outline-secondary me-md-2">
|
||||
<i class="bi bi-arrow-left"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> {{ submit_text }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- jQuery and Select2 JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2 for container selection
|
||||
$('.container-select').select2({
|
||||
placeholder: 'Search containers...',
|
||||
allowClear: true,
|
||||
ajax: {
|
||||
url: '{% url "frontend:dns_container_api" %}',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return {
|
||||
search: params.term,
|
||||
page: params.page || 1
|
||||
};
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: data.results
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
minimumInputLength: 0,
|
||||
});
|
||||
|
||||
// Auto-fill IP address when container is selected
|
||||
$('.container-select').on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
if (data.address) {
|
||||
$('#id_address').val(data.address);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear IP address when container selection is cleared
|
||||
$('.container-select').on('select2:clear', function (e) {
|
||||
$('#id_address').val('');
|
||||
});
|
||||
|
||||
// Mutual exclusion for name/regexp fields
|
||||
$('#id_name').on('input', function() {
|
||||
if ($(this).val()) {
|
||||
$('#id_regexp').prop('disabled', true);
|
||||
} else {
|
||||
$('#id_regexp').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
|
||||
$('#id_regexp').on('input', function() {
|
||||
if ($(this).val()) {
|
||||
$('#id_name').prop('disabled', true);
|
||||
} else {
|
||||
$('#id_name').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize mutual exclusion on page load
|
||||
if ($('#id_name').val()) {
|
||||
$('#id_regexp').prop('disabled', true);
|
||||
}
|
||||
if ($('#id_regexp').val()) {
|
||||
$('#id_name').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
144
frontend/templates/frontend/dns_list.html
Normal file
144
frontend/templates/frontend/dns_list.html
Normal file
@@ -0,0 +1,144 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} - DNS Management{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.dns-type-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.dns-name { background-color: #e3f2fd; color: #1565c0; }
|
||||
.dns-regexp { background-color: #f3e5f5; color: #7b1fa2; }
|
||||
|
||||
.search-form {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>
|
||||
<i class="bi bi-book"></i> DNS Management
|
||||
</h3>
|
||||
<a href="{% url 'frontend:dns_create' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add DNS Entry
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% include 'frontend/includes/dns_pagination_snippet.html' %}
|
||||
|
||||
<!-- DNS Entries Table -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if page_obj.object_list %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>DNS Name/Pattern</th>
|
||||
<th>IP Address</th>
|
||||
<th>Comment</th>
|
||||
<th>Created</th>
|
||||
<th width="120">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dns in page_obj %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if dns.name %}
|
||||
<span class="badge dns-name dns-type-badge">Name</span>
|
||||
{% else %}
|
||||
<span class="badge dns-regexp dns-type-badge">RegExp</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if dns.name %}
|
||||
<strong>{{ dns.name }}</strong>
|
||||
{% else %}
|
||||
<code>{{ dns.regexp }}</code>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ dns.address }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ dns.comment|default:"-" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{% if dns.created_at %}
|
||||
{{ dns.created_at|date:"M d, Y" }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if dns.pk not in dev_container_dns %}
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'frontend:dns_edit' dns.id %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Edit DNS Entry">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:dns_delete' dns.id %}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
title="Delete DNS Entry">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-globe" style="font-size: 3rem; color: #6c757d;"></i>
|
||||
<h4 class="mt-3 text-muted">No DNS Entries Found</h4>
|
||||
<p class="text-muted">
|
||||
{% if request.GET.search %}
|
||||
No DNS entries match your search criteria.
|
||||
{% else %}
|
||||
Start by creating your first DNS entry.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if not request.GET.search %}
|
||||
<a href="{% url 'frontend:dns_create' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create First DNS Entry
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'frontend/includes/dns_pagination_snippet.html' %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Auto-submit search form on type filter change
|
||||
document.getElementById('entryTypeSelect').addEventListener('change', function() {
|
||||
this.form.submit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
69
frontend/templates/frontend/edit_container.html
Normal file
69
frontend/templates/frontend/edit_container.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!-- frontend/templates/frontend/edit_container.html -->
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} - Edit Container{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10">
|
||||
<h4>Edit {{ container.name }}</h4>
|
||||
</div>
|
||||
<div class="col-12 col-md-2 text-end">
|
||||
<a href="{% url 'frontend:create_container' %}?clone_lxc={{ container.lxc.pk }}"
|
||||
class="btn btn-secondary">
|
||||
<i class="bi bi-copy"> Clone</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_uuid" value="{{ task_uuid }}"/>
|
||||
|
||||
<div class="row">
|
||||
{% for field in form %}
|
||||
<div class="col-{% if field.name in 'lxcdnslease' %}12 mb-3{% else %}4{% endif %}">
|
||||
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||
<input type="hidden" value="{{ field.value }}" name="{{ field.name }}"/>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary" id="save-btn">Speichern</button>
|
||||
<a href="{% url 'frontend:container_detail' container.internal_id %}" class="btn btn-secondary">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Form blocking - disable submit button on form submission
|
||||
$('form').on('submit', function () {
|
||||
$('#save-btn').prop('disabled', true).text('Aktualisiere Container...');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
52
frontend/templates/frontend/faq_delete.html
Normal file
52
frontend/templates/frontend/faq_delete.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load markdown_filters %}
|
||||
|
||||
{% block title %} - Delete FAQ Entry{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>
|
||||
<i class="bi bi-trash"></i> Delete FAQ Entry
|
||||
</h3>
|
||||
<a href="{% url 'frontend:faq_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to FAQ List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<h5><i class="bi bi-exclamation-triangle"></i> Confirm Deletion</h5>
|
||||
<p>Are you sure you want to delete this FAQ entry? This action cannot be undone.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="text-muted">FAQ Entry Details:</h5>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
<h6><strong>Title:</strong></h6>
|
||||
<p>{{ faq.title }}</p>
|
||||
|
||||
<h6><strong>Content:</strong></h6>
|
||||
<div style="max-height: 200px; overflow-y: auto;">{{ faq.content|markdown }}</div>
|
||||
|
||||
<h6><strong>Order:</strong></h6>
|
||||
<p>{{ faq.order }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'frontend:faq_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Delete FAQ Entry
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
92
frontend/templates/frontend/faq_form.html
Normal file
92
frontend/templates/frontend/faq_form.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - {{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>
|
||||
<i class="bi bi-question-circle"></i> {{ title }}
|
||||
</h3>
|
||||
<a href="{% url 'frontend:faq_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to FAQ List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.title.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.title.label }}
|
||||
</label>
|
||||
{{ form.title }}
|
||||
{% if form.title.help_text %}
|
||||
<div class="form-text">{{ form.title.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.title.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.title.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.content.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.content.label }}
|
||||
</label>
|
||||
{{ form.content }}
|
||||
{% if form.content.help_text %}
|
||||
<div class="form-text">{{ form.content.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.content.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.content.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.order.id_for_label }}" class="form-label fw-bold">
|
||||
{{ form.order.label }}
|
||||
</label>
|
||||
{{ form.order }}
|
||||
{% if form.order.help_text %}
|
||||
<div class="form-text">{{ form.order.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.order.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.order.errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{% for error in form.non_field_errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'frontend:faq_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> {{ submit_text }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
153
frontend/templates/frontend/faq_list.html
Normal file
153
frontend/templates/frontend/faq_list.html
Normal file
@@ -0,0 +1,153 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load markdown_filters %}
|
||||
|
||||
{% block title %} - FAQ{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.faq-item {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
.faq-header {
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.faq-header:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
.faq-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
.faq-content h1, .faq-content h2, .faq-content h3,
|
||||
.faq-content h4, .faq-content h5, .faq-content h6 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.faq-content p {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.faq-content code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.faq-content pre {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.faq-content blockquote {
|
||||
border-left: 4px solid #007bff;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
font-style: italic;
|
||||
color: #6c757d;
|
||||
}
|
||||
.faq-content ul, .faq-content ol {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.faq-category-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>
|
||||
<i class="bi bi-question-circle"></i> FAQ - Frequently Asked Questions
|
||||
</h3>
|
||||
<a href="{% url 'frontend:faq_create' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add FAQ Entry
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Items -->
|
||||
<div class="row">
|
||||
<form action="{% url 'frontend:faq_list' %}" method="GET" class="col-12">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="Search FAQ Entries" name="search" value="{{ request.GET.search }}">
|
||||
<button class="btn btn-outline-secondary" type="submit">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if faqs %}
|
||||
<div class="faq-container">
|
||||
{% for faq in faqs %}
|
||||
<div class="faq-item">
|
||||
<div class="faq-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center flex-grow-1" onclick="toggleFAQ({{ faq.pk }})" style="cursor: pointer;">
|
||||
<h5 class="mb-0 me-2">{{ faq.title }}</h5>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<a href="{% url 'frontend:faq_edit' faq.pk %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Edit FAQ">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'frontend:faq_delete' faq.pk %}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
title="Delete FAQ">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
<i class="bi bi-chevron-down" id="icon-{{ faq.pk }}" onclick="toggleFAQ({{ faq.pk }})" style="cursor: pointer;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="faq-content collapse" id="faq-{{ faq.pk }}">
|
||||
{{ faq.content|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-question-circle" style="font-size: 3rem; color: #6c757d;"></i>
|
||||
<h4 class="mt-3 text-muted">No FAQ Entries Available</h4>
|
||||
<p class="text-muted">
|
||||
FAQ entries will appear here once they are added by an administrator.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function toggleFAQ(faqId) {
|
||||
const content = document.getElementById('faq-' + faqId);
|
||||
const icon = document.getElementById('icon-' + faqId);
|
||||
|
||||
if (content.classList.contains('show')) {
|
||||
content.classList.remove('show');
|
||||
icon.classList.remove('bi-chevron-up');
|
||||
icon.classList.add('bi-chevron-down');
|
||||
} else {
|
||||
content.classList.add('show');
|
||||
icon.classList.remove('bi-chevron-down');
|
||||
icon.classList.add('bi-chevron-up');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
<!-- DNS Pagination -->
|
||||
<nav aria-label="DNS-Paginierung" class="mt-4 search-form">
|
||||
<ul class="pagination justify-content-left">
|
||||
<div class="d-flex align-items-center mb-3 me-3 flex-wrap" style="width: 100%">
|
||||
<form action="{% url 'frontend:dns_list' %}" method="get" class="me-3" style="flex: 1;">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search DNS entries..." name="search"
|
||||
value="{{ request.GET.search }}"/>
|
||||
<button type="submit" class="btn btn-outline-secondary" title="Search"><i class="bi bi-search"></i>
|
||||
</button>
|
||||
{% if request.GET.search %}
|
||||
<a href="{% url 'frontend:dns_list' %}{% if request.GET.entry_type %}?entry_type={{ request.GET.entry_type }}{% endif %}"
|
||||
class="btn btn-outline-secondary" title="Clear Search"><i class="bi bi-trash"></i></a>
|
||||
{% endif %}
|
||||
<a class="btn btn-outline-secondary" style="font-weight: bold;color:darkred">{{ dns_entries.count }}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="filterForm" action="{% url 'frontend:dns_list' %}" method="GET" class="d-flex align-items-center">
|
||||
<select name="entry_type" class="form-select me-2" id="entryTypeSelect" onchange="this.form.submit()">
|
||||
<option value="" {% if not request.GET.entry_type %}selected{% endif %}>All Types</option>
|
||||
<option value="name" {% if request.GET.entry_type == 'name' %}selected{% endif %}>Names only</option>
|
||||
<option value="regexp" {% if request.GET.entry_type == 'regexp' %}selected{% endif %}>RegExp only</option>
|
||||
</select>
|
||||
{% if request.GET.search %}
|
||||
<input type="hidden" name="search" value="{{ request.GET.search }}">
|
||||
{% endif %}
|
||||
{% if request.GET.page %}
|
||||
<input type="hidden" name="page" value="{{ request.GET.page }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.entry_type %}&entry_type={{ request.GET.entry_type }}{% endif %}"
|
||||
aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.entry_type %}&entry_type={{ request.GET.entry_type }}{% endif %}"
|
||||
aria-label="Prev">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">««</span></li>
|
||||
<li class="page-item disabled"><span class="page-link">«</span></li>
|
||||
{% endif %}
|
||||
|
||||
{% for i in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == i %}
|
||||
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
|
||||
{% elif i > page_obj.number|add:'-3' and i < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ i }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.entry_type %}&entry_type={{ request.GET.entry_type }}{% endif %}">{{ i }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.entry_type %}&entry_type={{ request.GET.entry_type }}{% endif %}"
|
||||
aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.entry_type %}&entry_type={{ request.GET.entry_type }}{% endif %}"
|
||||
aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">»</span></li>
|
||||
<li class="page-item disabled"><span class="page-link">»»</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
95
frontend/templates/frontend/includes/pagination_snippet.html
Normal file
95
frontend/templates/frontend/includes/pagination_snippet.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!-- Pagination -->
|
||||
<nav aria-label="Container-Paginierung" class="mt-4 search-form">
|
||||
<ul class="pagination justify-content-left">
|
||||
<div class="d-flex align-items-center mb-3 me-3 flex-wrap" style="width: 100%">
|
||||
<!-- Suchfeld -->
|
||||
<form action="{{ search_action_url }}" method="get" class="me-3" style="flex: 1;">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search" name="search"
|
||||
value="{{ request.GET.search }}"/>
|
||||
<button type="submit" class="btn btn-outline-secondary" title="Search"><i class="bi bi-search"></i>
|
||||
</button>
|
||||
{% if request.GET.search %}<a href="{% url 'frontend:dashboard' %}{% if request.GET.lxc_status %}?lxc_status={{ request.GET.lxc_status }}{% endif %}{% if request.GET.network_status %}{% if request.GET.lxc_status %}&{% else %}?{% endif %}network_status={{ request.GET.network_status }}{% endif %}"
|
||||
class="btn btn-outline-secondary" title="Clear Search"><i class="bi bi-trash"></i></a>{% endif %}
|
||||
<a class="btn btn-outline-secondary" style="font-weight: bold;color:darkred">{{ containers.count }}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Filter als Select-Boxen mit auto-submit -->
|
||||
<form id="filterForm" action="{{ search_action_url }}" method="GET" class="d-flex align-items-center">
|
||||
<select name="lxc_status" class="form-select me-2" id="lxcStatusSelect" onchange="this.form.submit()">
|
||||
<option value="" {% if not request.GET.lxc_status %}selected{% endif %}>All LXC</option>
|
||||
<option value="running" {% if request.GET.lxc_status == 'running' %}selected{% endif %}>Running Lxc</option>
|
||||
<option value="stopped" {% if request.GET.lxc_status == 'stopped' %}selected{% endif %}>Stopped Lxc</option>
|
||||
</select>
|
||||
<select name="network_status" class="form-select" id="networkStatusSelect"
|
||||
onchange="this.form.submit()">
|
||||
<option value="" {% if not request.GET.network_status %}selected{% endif %}>All Net</option>
|
||||
<option value="bound" {% if request.GET.network_status == 'bound' %}selected{% endif %}> Bound </option>
|
||||
<option value="waiting" {% if request.GET.network_status == 'waiting' %}selected{% endif %}>Waiting</option>
|
||||
</select>
|
||||
{% if request.GET.search %}
|
||||
<input type="hidden" name="search" value="{{ request.GET.search }}">{% endif %}
|
||||
{% if request.GET.page %}<input type="hidden" name="page" value="{{ request.GET.page }}">{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.lxc_status %}&lxc_status={{ request.GET.lxc_status }}{% endif %}{% if request.GET.network_status %}&network_status={{ request.GET.network_status }}{% endif %}"
|
||||
aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.lxc_status %}&lxc_status={{ request.GET.lxc_status }}{% endif %}{% if request.GET.network_status %}&network_status={{ request.GET.network_status }}{% endif %}"
|
||||
aria-label="Prev">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">««</span></li>
|
||||
<li class="page-item disabled"><span class="page-link">«</span></li>
|
||||
{% endif %}
|
||||
|
||||
{% for i in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == i %}
|
||||
<li class="page-item active"><span class="page-link">{{ i }}</span></li>
|
||||
{% elif i > page_obj.number|add:'-3' and i < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ i }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.lxc_status %}&lxc_status={{ request.GET.lxc_status }}{% endif %}{% if request.GET.network_status %}&network_status={{ request.GET.network_status }}{% endif %}">{{ i }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.lxc_status %}&lxc_status={{ request.GET.lxc_status }}{% endif %}{% if request.GET.network_status %}&network_status={{ request.GET.network_status }}{% endif %}"
|
||||
aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.lxc_status %}&lxc_status={{ request.GET.lxc_status }}{% endif %}{% if request.GET.network_status %}&network_status={{ request.GET.network_status }}{% endif %}"
|
||||
aria-label="Last">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">»</span></li>
|
||||
<li class="page-item disabled"><span class="page-link">»»</span></li>
|
||||
{% endif %}
|
||||
<div class="d-flex align-items-center mb-3 flex-wrap ms-2">
|
||||
|
||||
<!-- Ansichtsumschalter (Client-seitig per JS) -->
|
||||
<div class="btn-group me-3" role="group">
|
||||
<button type="button" id="cardViewBtn" class="btn btn-outline-secondary active"><i
|
||||
class="bi bi-grid-3x3-gap"></i></button>
|
||||
<button type="button" id="listViewBtn" class="btn btn-outline-secondary"><i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
30
frontend/templates/frontend/login.html
Normal file
30
frontend/templates/frontend/login.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!-- frontend/templates/frontend/login.html -->
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Anmelden</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
142
frontend/templates/frontend/lxc_console.html
Normal file
142
frontend/templates/frontend/lxc_console.html
Normal file
@@ -0,0 +1,142 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} - Console: {{ container.name }}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.console-container {
|
||||
height: 80vh;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.console-header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #444;
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-info {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.console-controls {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.console-iframe {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
border: none;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.console-status {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.console-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>
|
||||
<i class="bi bi-terminal"></i>
|
||||
Console: {{ container.name }}
|
||||
<small class="text-muted">({{ container.address }})</small>
|
||||
</h2>
|
||||
<a href="{% url 'frontend:dashboard' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="console-warning">
|
||||
<h5><i class="bi bi-exclamation-triangle text-warning"></i> Console Access</h5>
|
||||
<p class="mb-0">
|
||||
This opens a direct console connection to the LXC container <strong>{{ container.name }}</strong> (VM-ID: {{ container.lxc.vmid }}).
|
||||
The console connection is handled through the Proxmox web interface.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="console-container">
|
||||
<div class="console-header">
|
||||
<div class="console-info">
|
||||
<span class="console-status">●</span>
|
||||
{{ container.hostname }} | VM-ID: {{ container.lxc.vmid }} | IP: {{ container.address }}
|
||||
</div>
|
||||
<div class="console-controls">
|
||||
<button class="btn btn-sm btn-outline-light" onclick="reloadConsole()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Reload
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-light" onclick="openInNewTab()">
|
||||
<i class="bi bi-box-arrow-up-right"></i> New Tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
id="console-iframe"
|
||||
class="console-iframe"
|
||||
src="https://{{ proxmox_host }}:8006/?console=lxc&vmid={{ container.lxc.vmid }}&node={{ container.lxc.node }}"
|
||||
title="LXC Console for {{ container.name }}">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="bi bi-info-circle"></i> Console Help</h6>
|
||||
<ul class="mb-0">
|
||||
<li>This console provides direct terminal access to your LXC container</li>
|
||||
<li>You may need to press Enter to activate the console</li>
|
||||
<li>Use Ctrl+Alt+Del to send reset signal (if supported)</li>
|
||||
<li>If the console doesn't load, try refreshing or opening in a new tab</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function reloadConsole() {
|
||||
const iframe = document.getElementById('console-iframe');
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
|
||||
function openInNewTab() {
|
||||
const iframe = document.getElementById('console-iframe');
|
||||
window.open(iframe.src, '_blank');
|
||||
}
|
||||
|
||||
// Auto-reload console every 30 seconds to keep session alive
|
||||
setInterval(function() {
|
||||
const iframe = document.getElementById('console-iframe');
|
||||
// Ping the iframe to keep session alive without full reload
|
||||
try {
|
||||
iframe.contentWindow.postMessage('ping', '*');
|
||||
} catch (e) {
|
||||
// Cross-origin, ignore
|
||||
}
|
||||
}, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
163
frontend/templates/frontend/lxc_console_popup.html
Normal file
163
frontend/templates/frontend/lxc_console_popup.html
Normal file
@@ -0,0 +1,163 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %} - Console: {{ container.name }}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.console-launch {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.console-icon {
|
||||
font-size: 64px;
|
||||
color: #28a745;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.console-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.btn-console {
|
||||
background: linear-gradient(45deg, #28a745, #20c997);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
font-size: 18px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-console:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.console-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-terminal"></i>
|
||||
LXC Console Access
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="console-launch">
|
||||
<div class="console-icon">
|
||||
<i class="bi bi-terminal-fill"></i>
|
||||
</div>
|
||||
|
||||
<h5>{{ container.name }}</h5>
|
||||
<p class="text-muted">VM-ID: {{ container.lxc.vmid }} | IP: {{ container.address }}</p>
|
||||
|
||||
<div class="console-info">
|
||||
<h6><i class="bi bi-info-circle text-info"></i> Console Information</h6>
|
||||
<div class="row text-start">
|
||||
<div class="col-sm-4"><strong>Hostname:</strong></div>
|
||||
<div class="col-sm-8">{{ container.hostname }}</div>
|
||||
<div class="col-sm-4"><strong>Node:</strong></div>
|
||||
<div class="col-sm-8">{{ container.lxc.node }}</div>
|
||||
<div class="col-sm-4"><strong>Status:</strong></div>
|
||||
<div class="col-sm-8">
|
||||
<span class="badge bg-{{ container.lxc.status|yesno:'success,danger' }}">
|
||||
{{ container.lxc.status|title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="openConsole" class="btn btn-console">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
Open Console in New Window
|
||||
</button>
|
||||
|
||||
<div class="console-warning mt-3">
|
||||
<h6><i class="bi bi-exclamation-triangle text-warning"></i> Important Notes</h6>
|
||||
<ul class="text-start mb-0">
|
||||
<li>The console will open in the Proxmox web interface</li>
|
||||
<li>You will need to login to Proxmox if not already authenticated</li>
|
||||
<li>Please allow popups for this site if blocked</li>
|
||||
<li>Navigate to the container console once in Proxmox</li>
|
||||
<li>Alternatively, you can manually access: <code>https://{{ proxmox_host }}:8006</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="{% url 'frontend:dashboard' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
<a href="{% url 'frontend:container_detail' container.pk %}" class="btn btn-info ms-2">
|
||||
<i class="bi bi-info-circle"></i> Container Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('openConsole').addEventListener('click', function() {
|
||||
const consoleUrl = '{{ console_url|escapejs }}';
|
||||
|
||||
// Open console in popup window
|
||||
const popup = window.open(
|
||||
consoleUrl,
|
||||
'lxc-console-{{ container.lxc.vmid }}',
|
||||
'width=1024,height=768,scrollbars=yes,resizable=yes,status=yes,toolbar=no,menubar=no'
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
alert('Popup blocked! Please allow popups for this site and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus the popup
|
||||
popup.focus();
|
||||
|
||||
// Show success message
|
||||
this.innerHTML = '<i class="bi bi-check-circle"></i> Console Opened';
|
||||
this.disabled = true;
|
||||
this.classList.remove('btn-console');
|
||||
this.classList.add('btn', 'btn-success');
|
||||
|
||||
// Re-enable button after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.innerHTML = '<i class="bi bi-box-arrow-up-right"></i> Open Console in New Window';
|
||||
this.disabled = false;
|
||||
this.classList.remove('btn', 'btn-success');
|
||||
this.classList.add('btn-console');
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// Auto-open console if requested
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('autoopen') === 'true') {
|
||||
document.getElementById('openConsole').click();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user