fix: move inline JavaScript to external file to comply with CSP

This commit is contained in:
SW 2026-10-01 14:48:16 +02:00
parent 406ff2f9c2
commit a0ca704ac1
4 changed files with 97 additions and 64 deletions

79
static/app.js Normal file
View file

@ -0,0 +1,79 @@
document.addEventListener('DOMContentLoaded', function () {
// Open allocation modal
document.querySelectorAll('.allocate-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
openAllocateModal(btn);
});
});
// Open release modal from history table
document.querySelectorAll('.release-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
const id = btn.dataset.id;
const modal = document.getElementById('release-modal-' + id);
if (modal) {
modal.style.display = 'flex';
}
});
});
// Close modal via Cancel button
document.querySelectorAll('.cancel-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
const modalId = btn.dataset.modal;
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
});
});
// Close modal by clicking on the overlay background
document.querySelectorAll('.modal-overlay').forEach(function (overlay) {
overlay.addEventListener('click', function (event) {
if (event.target === overlay) {
overlay.style.display = 'none';
}
});
});
// Prevent clicks inside the modal from closing it
document.querySelectorAll('.modal').forEach(function (modal) {
modal.addEventListener('click', function (event) {
event.stopPropagation();
});
});
});
async function openAllocateModal(btn) {
const slug = btn.dataset.slug;
const name = btn.dataset.name;
const select = document.getElementById('prefix-' + slug);
const prefixLength = select ? select.value : '';
document.getElementById('modal-pool-slug').value = slug;
document.getElementById('modal-prefix-length').value = prefixLength;
document.getElementById('modal-pool-name').textContent = name;
document.getElementById('modal-prefix-display').textContent = '/' + prefixLength;
try {
const resp = await fetch('/partials/pools/' + slug + '/preview?prefix_length=' + prefixLength);
const html = await resp.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const cidr = doc.getElementById('preview-cidr')?.textContent || '–';
const addressCount = doc.getElementById('preview-address-count')?.textContent || '–';
const usableHosts = doc.getElementById('preview-usable-hosts')?.textContent || '–';
const warning = doc.getElementById('preview-warning')?.textContent || '';
document.getElementById('modal-cidr').textContent = cidr;
document.getElementById('modal-address-count').textContent = addressCount;
document.getElementById('modal-usable-hosts').textContent = usableHosts;
document.getElementById('modal-warning').textContent = warning;
} catch (e) {
document.getElementById('modal-warning').textContent = 'Could not load preview.';
}
document.getElementById('allocate-modal').style.display = 'flex';
}

View file

@ -76,7 +76,7 @@
</div>
{% if allocation.status == "active" || allocation.status == "reserved" %}
<button class="btn btn-danger" onclick="document.getElementById('release-modal').style.display='flex'">Release subnet</button>
<button class="btn btn-danger release-btn" data-id="{{ allocation.id }}">Release subnet</button>
{% endif %}
<a href="/" class="btn btn-secondary">Back</a>
@ -108,8 +108,8 @@
</section>
{% endif %}
<div id="release-modal" class="modal-overlay" style="display:none;" onclick="if (event.target === this) this.style.display='none';">
<div class="modal" onclick="event.stopPropagation();">
<div id="release-modal-{{ allocation.id }}" class="modal-overlay" style="display:none;">
<div class="modal">
<h2 class="modal-title">Release subnet {{ allocation.cidr }}</h2>
<form method="post" action="/allocations/{{ allocation.id }}/release" class="modal-body" id="release-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@ -117,10 +117,12 @@
<label for="release-note">Release note *</label>
<textarea id="release-note" name="release_note" rows="3" required minlength="3" maxlength="500"></textarea>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('release-modal').style.display='none'">Cancel</button>
<button type="button" class="btn btn-secondary cancel-btn" data-modal="release-modal-{{ allocation.id }}">Cancel</button>
<button type="submit" class="btn btn-danger">Release</button>
</div>
</form>
</div>
</div>
<script src="/static/app.js"></script>
{% endblock %}

View file

@ -24,7 +24,7 @@
<td data-label="Actions">
<a href="/allocations/{{ a.id }}">Detail</a>
{% if a.status == "active" || a.status == "reserved" %}
| <button type="button" class="btn-link" onclick="document.getElementById('release-modal-{{ a.id }}').style.display='flex'">Release</button>
| <button type="button" class="btn-link release-btn" data-id="{{ a.id }}">Release</button>
{% endif %}
</td>
</tr>
@ -38,8 +38,8 @@
{% for a in allocations %}
{% if a.status == "active" || a.status == "reserved" %}
<div id="release-modal-{{ a.id }}" class="modal-overlay" style="display:none;" onclick="if (event.target === this) this.style.display='none';">
<div class="modal" onclick="event.stopPropagation();">
<div id="release-modal-{{ a.id }}" class="modal-overlay" style="display:none;">
<div class="modal">
<h2 class="modal-title">Release subnet {{ a.cidr }}</h2>
<form method="post" action="/allocations/{{ a.id }}/release" class="modal-body" id="release-form-{{ a.id }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@ -47,7 +47,7 @@
<label for="release-note-{{ a.id }}">Release note *</label>
<textarea id="release-note-{{ a.id }}" name="release_note" rows="3" required minlength="3" maxlength="500"></textarea>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('release-modal-{{ a.id }}').style.display='none'">Cancel</button>
<button type="button" class="btn btn-secondary cancel-btn" data-modal="release-modal-{{ a.id }}">Cancel</button>
<button type="submit" class="btn btn-danger">Release</button>
</div>
</form>

View file

@ -27,7 +27,7 @@
hx-indicator="#indicator-{{ pool.slug }}"
>
<label for="prefix-{{ pool.slug }}">Subnet size</label>
<select name="prefix_length" id="prefix-{{ pool.slug }}" onchange="updateModalPrefix('{{ pool.slug }}', this.value)">
<select name="prefix_length" id="prefix-{{ pool.slug }}">
{% for pl in pool.allowed_prefixes %}
<option value="{{ pl }}" {% if pl == "24" %}selected{% endif %}>/{{ pl }}</option>
{% endfor %}
@ -37,9 +37,10 @@
</form>
<button
class="btn"
class="btn allocate-btn"
style="margin-top: 1rem;"
onclick="openAllocateModal('{{ pool.slug }}')"
data-slug="{{ pool.slug }}"
data-name="{{ pool.name }}"
>
[ Allocate next free subnet ]
</button>
@ -85,8 +86,8 @@
</div>
</section>
<div id="allocate-modal" class="modal-overlay" style="display:none;" onclick="if (event.target === this) closeAllocateModal();">
<div class="modal" onclick="event.stopPropagation();">
<div id="allocate-modal" class="modal-overlay" style="display:none;">
<div class="modal">
<h2 class="modal-title">Confirm subnet allocation</h2>
<form method="post" action="/allocations" class="modal-body" id="allocate-form">
<input type="hidden" name="pool_slug" id="modal-pool-slug">
@ -125,61 +126,12 @@
<input type="text" id="ticket" name="ticket" maxlength="100">
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeAllocateModal()">Cancel</button>
<button type="button" class="btn btn-secondary cancel-btn" data-modal="allocate-modal">Cancel</button>
<button type="submit" class="btn">Confirm allocation</button>
</div>
</form>
</div>
</div>
<script>
const poolData = {
{% for pool in pools %}
"{{ pool.slug }}": {
name: "{{ pool.name }}",
allowed_prefixes: {{ pool.allowed_prefixes_json|safe }}
},
{% endfor %}
};
function updateModalPrefix(slug, prefixLength) {
// preview is updated via HTMX automatically
}
async function openAllocateModal(slug) {
const select = document.getElementById('prefix-' + slug);
const prefixLength = select.value;
const modal = document.getElementById('allocate-modal');
document.getElementById('modal-pool-slug').value = slug;
document.getElementById('modal-prefix-length').value = prefixLength;
document.getElementById('modal-pool-name').textContent = poolData[slug].name;
document.getElementById('modal-prefix-display').textContent = '/' + prefixLength;
try {
const resp = await fetch('/partials/pools/' + slug + '/preview?prefix_length=' + prefixLength);
const html = await resp.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const cidr = doc.getElementById('preview-cidr')?.textContent || '–';
const addressCount = doc.getElementById('preview-address-count')?.textContent || '–';
const usableHosts = doc.getElementById('preview-usable-hosts')?.textContent || '–';
const warning = doc.getElementById('preview-warning')?.textContent || '';
document.getElementById('modal-cidr').textContent = cidr;
document.getElementById('modal-address-count').textContent = addressCount;
document.getElementById('modal-usable-hosts').textContent = usableHosts;
document.getElementById('modal-warning').textContent = warning;
} catch (e) {
document.getElementById('modal-warning').textContent = 'Could not load preview.';
}
modal.style.display = 'flex';
}
function closeAllocateModal() {
document.getElementById('allocate-modal').style.display = 'none';
}
</script>
<script src="/static/app.js"></script>
{% endblock %}