diff --git a/static/js/pages/dhcpv6_leases.js b/static/js/pages/dhcpv6_leases.js
new file mode 100644
index 0000000..81ae498
--- /dev/null
+++ b/static/js/pages/dhcpv6_leases.js
@@ -0,0 +1,166 @@
+document.addEventListener('DOMContentLoaded', function() {
+ if (document.getElementById('poolSelect')) {
+ refreshLeaseTable();
+ }
+});
+
+function refreshLeaseTable() {
+ const tableBody = document.getElementById('leaseTableBody');
+ const refreshButton = document.getElementById('refreshButton');
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshSpinner = document.getElementById('refreshSpinner');
+ const poolSelect = document.getElementById('poolSelect');
+
+ if (!tableBody || !poolSelect || !refreshButton || !refreshIcon || !refreshSpinner) {
+ console.error("Not all elements required for DHCPv6 leases were found.");
+ return;
+ }
+
+ const selectedPool = poolSelect.value;
+ if (!selectedPool) {
+ tableBody.innerHTML = '
| No pool selected or available. |
';
+ return;
+ }
+
+ refreshButton.disabled = true;
+ refreshIcon.classList.add('d-none');
+ refreshSpinner.classList.remove('d-none');
+
+ const loadingHTML = `
+
+ |
+
+ Loading...
+
+ |
+
`;
+ tableBody.innerHTML = loadingHTML;
+
+ fetch(`/dhcpv6-leases/json?pool=${selectedPool}`)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ tableBody.innerHTML = '';
+
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ if (data.data && data.data.length > 0) {
+ data.data.forEach((lease, index) => {
+ const row = document.createElement('tr');
+ row.setAttribute('data-index', index);
+
+ row.innerHTML = `
+ ${escapeHTML(lease.prefix)} |
+ ${escapeHTML(lease.state)} |
+ ${escapeHTML(lease.last_communication)} |
+ ${escapeHTML(lease.lease_expiration)} |
+ ${escapeHTML(lease.remaining)} |
+ ${escapeHTML(lease.type)} |
+ ${escapeHTML(lease.pool)} |
+ ${escapeHTML(lease.duid)} |
+ `;
+ tableBody.appendChild(row);
+ });
+ } else {
+ tableBody.innerHTML = '| No active leases found for this pool. |
';
+ }
+ })
+ .catch(error => {
+ console.error('Error fetching DHCPv6 leases:', error);
+ tableBody.innerHTML = `| Failed to retrieve data: ${error.message} |
`;
+ })
+ .finally(() => {
+ refreshButton.disabled = false;
+ refreshIcon.classList.remove('d-none');
+ refreshSpinner.classList.add('d-none');
+ });
+}
+
+function filterTable(searchInputId, tableId) {
+ const input = document.getElementById(searchInputId);
+ if (!input) return;
+ const filter = input.value.toUpperCase();
+ const table = document.getElementById(tableId);
+ if (!table) return;
+
+ const tr = table.getElementsByTagName("tr");
+
+ for (let i = 1; i < tr.length; i++) {
+ if (tr[i].parentNode.tagName.toLowerCase() !== 'tbody') {
+ continue;
+ }
+
+ tr[i].style.display = "none";
+ const td = tr[i].getElementsByTagName("td");
+
+ for (let j = 0; j < td.length; j++) {
+ if (td[j]) {
+ const txtValue = td[j].textContent || td[j].innerText;
+ if (txtValue.toUpperCase().indexOf(filter) > -1) {
+ tr[i].style.display = "";
+ break;
+ }
+ }
+ }
+ }
+}
+
+function sortTable(tableId, columnIndex, th) {
+ const table = document.getElementById(tableId);
+ if (!table || !table.tBodies[0]) return;
+
+ const tbody = table.tBodies[0];
+ const rows = Array.from(tbody.rows);
+ const headers = Array.from(th.parentNode.children);
+
+ headers.forEach(header => {
+ if (header !== th) {
+ header.classList.remove('asc', 'desc');
+ header.setAttribute('data-sort-state', 'none');
+ }
+ });
+
+ let currentState = th.getAttribute('data-sort-state') || 'none';
+ let newState = currentState === 'none' ? 'asc' : currentState === 'asc' ? 'desc' : 'none';
+
+ th.classList.remove('asc', 'desc');
+ if (newState !== 'none') th.classList.add(newState);
+ th.setAttribute('data-sort-state', newState);
+
+ if (newState === 'none') {
+ rows.sort((a, b) => parseInt(a.getAttribute('data-index')) - parseInt(b.getAttribute('data-index')));
+ } else {
+ rows.sort((a, b) => {
+ const aText = a.cells[columnIndex]?.textContent.trim() || '';
+ const bText = b.cells[columnIndex]?.textContent.trim() || '';
+
+ const aNum = parseFloat(aText.replace(/[^0-9.-]/g, ''));
+ const bNum = parseFloat(bText.replace(/[^0-9.-]/g, ''));
+ const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aText.match(/^[0-9.-]/) && bText.match(/^[0-9.-]/);
+
+ return newState === 'asc'
+ ? (isNumeric ? aNum - bNum : aText.localeCompare(bText))
+ : (isNumeric ? bNum - aNum : bText.localeCompare(aText));
+ });
+ }
+
+ rows.forEach(row => tbody.appendChild(row));
+}
+
+function escapeHTML(str) {
+ if (str === null || str === undefined) {
+ return 'N/A';
+ }
+ return str.toString()
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
\ No newline at end of file
diff --git a/static/js/pages/history.js b/static/js/pages/history.js
index 74e9dc0..054ff13 100644
--- a/static/js/pages/history.js
+++ b/static/js/pages/history.js
@@ -5,16 +5,66 @@ document.addEventListener('DOMContentLoaded', function () {
let currentRange = '24h';
const timeRangeButtons = document.querySelectorAll('.btn-group .btn');
+ const customRangeBtn = document.getElementById('customRangeBtn');
+ const startDateInput = document.getElementById('startDate');
+ const endDateInput = document.getElementById('endDate');
+
+ function formatDateForInput(date) {
+ const pad = (num) => num.toString().padStart(2, '0');
+ const year = date.getFullYear();
+ const month = pad(date.getMonth() + 1);
+ const day = pad(date.getDate());
+ const hours = pad(date.getHours());
+ const minutes = pad(date.getMinutes());
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
+ }
+
+ function updateDateInputs(range) {
+ const endDate = new Date();
+ let startDate = new Date();
+
+ switch (range) {
+ case '24h':
+ startDate.setHours(startDate.getHours() - 24);
+ break;
+ case '7d':
+ startDate.setDate(startDate.getDate() - 7);
+ break;
+ case '30d':
+ startDate.setDate(startDate.getDate() - 30);
+ break;
+ case '90d':
+ startDate.setDate(startDate.getDate() - 90);
+ break;
+ }
+
+ startDateInput.value = formatDateForInput(startDate);
+ endDateInput.value = formatDateForInput(endDate);
+ }
timeRangeButtons.forEach(button => {
button.addEventListener('click', () => {
currentRange = button.getAttribute('data-range');
timeRangeButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
- fetchDataAndRenderCharts(currentRange);
+
+ updateDateInputs(currentRange);
+ fetchDataAndRenderCharts({ range: currentRange });
});
});
+ customRangeBtn.addEventListener('click', () => {
+ const startDate = startDateInput.value;
+ const endDate = endDateInput.value;
+
+ if (startDate && endDate) {
+ timeRangeButtons.forEach(btn => btn.classList.remove('active'));
+ fetchDataAndRenderCharts({ startDate, endDate });
+ } else {
+ alert('Please select both a start and end date.');
+ }
+ });
+
function updateChartScale(chartInstance) {
const dataset = chartInstance.data.datasets[0];
const validData = (dataset.data || []).filter(val => typeof val === 'number');
@@ -38,9 +88,16 @@ document.addEventListener('DOMContentLoaded', function () {
chartInstance.update();
}
- async function fetchDataAndRenderCharts(range) {
+ async function fetchDataAndRenderCharts(params) {
+ let url = '/history/api/total-routes';
+ if (params.range) {
+ url += `?range=${params.range}`;
+ } else if (params.startDate && params.endDate) {
+ url += `?start_date=${params.startDate}&end_date=${params.endDate}`;
+ }
+
try {
- const response = await fetch(`/history/api/total-routes?range=${range}`);
+ const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
renderCharts(data);
@@ -167,5 +224,6 @@ document.addEventListener('DOMContentLoaded', function () {
updateChartScale(ipv6Chart);
}
- fetchDataAndRenderCharts(currentRange);
+ updateDateInputs(currentRange);
+ fetchDataAndRenderCharts({ range: currentRange });
});
\ No newline at end of file