This commit is contained in:
Holger Sielaff
2025-08-27 09:55:55 +02:00
commit 90c0ff61ed
107 changed files with 8535 additions and 0 deletions

View 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>

View 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 %}

View 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 %}

View 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">&nbsp;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">&nbsp;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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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">&nbsp;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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@@ -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">&laquo;&laquo;</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">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">&laquo;&laquo;</span></li>
<li class="page-item disabled"><span class="page-link">&laquo;</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">&raquo;</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">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">&raquo;</span></li>
<li class="page-item disabled"><span class="page-link">&raquo;&raquo;</span></li>
{% endif %}
</ul>
</nav>

View 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">&laquo;&laquo;</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">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">&laquo;&laquo;</span></li>
<li class="page-item disabled"><span class="page-link">&laquo;</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">&raquo;</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">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">&raquo;</span></li>
<li class="page-item disabled"><span class="page-link">&raquo;&raquo;</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>

View 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 %}

View 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 %}

View 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 %}