v0.2.1. many features added and cleaned up.

This commit is contained in:
serversdwn
2025-12-03 21:23:18 +00:00
parent dc853806bb
commit 4cef580185
13 changed files with 1815 additions and 181 deletions

View File

@@ -1,36 +1,60 @@
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<table id="roster-table" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Status
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 select-none" onclick="sortTable('status')">
<div class="flex items-center gap-1">
Status
<span class="sort-indicator" data-column="status"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Unit ID
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 select-none" onclick="sortTable('id')">
<div class="flex items-center gap-1">
Unit ID
<span class="sort-indicator" data-column="id"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Type
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 select-none" onclick="sortTable('type')">
<div class="flex items-center gap-1">
Type
<span class="sort-indicator" data-column="type"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Details
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Last Seen
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 select-none" onclick="sortTable('last_seen')">
<div class="flex items-center gap-1">
Last Seen
<span class="sort-indicator" data-column="last_seen"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Age
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 select-none" onclick="sortTable('age')">
<div class="flex items-center gap-1">
Age
<span class="sort-indicator" data-column="age"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Note
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 select-none" onclick="sortTable('note')">
<div class="flex items-center gap-1">
Note
<span class="sort-indicator" data-column="note"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
<tbody id="roster-tbody" class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{% for unit in units %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
data-status="{{ unit.status }}"
data-id="{{ unit.id }}"
data-type="{{ unit.device_type }}"
data-last-seen="{{ unit.last_seen }}"
data-age="{{ unit.age }}"
data-note="{{ unit.note if unit.note else '' }}">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center space-x-2">
{% if unit.status == 'OK' %}
@@ -159,7 +183,108 @@
</div>
</div>
<style>
.sort-indicator::after {
content: '⇅';
opacity: 0.3;
font-size: 12px;
}
.sort-indicator.asc::after {
content: '↑';
opacity: 1;
}
.sort-indicator.desc::after {
content: '↓';
opacity: 1;
}
</style>
<script>
// Update timestamp
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString();
// Sorting state
let currentSort = { column: null, direction: 'asc' };
function sortTable(column) {
const tbody = document.getElementById('roster-tbody');
const rows = Array.from(tbody.getElementsByTagName('tr'));
// Determine sort direction
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'asc';
}
// Sort rows
rows.sort((a, b) => {
let aVal = a.getAttribute(`data-${column}`) || '';
let bVal = b.getAttribute(`data-${column}`) || '';
// Special handling for different column types
if (column === 'age') {
// Parse age strings like "2h 15m" or "45m" or "3d 5h"
aVal = parseAge(aVal);
bVal = parseAge(bVal);
} else if (column === 'status') {
// Sort by status priority: Missing > Pending > OK
const statusOrder = { 'Missing': 0, 'Pending': 1, 'OK': 2, '': 3 };
aVal = statusOrder[aVal] !== undefined ? statusOrder[aVal] : 999;
bVal = statusOrder[bVal] !== undefined ? statusOrder[bVal] : 999;
} else if (column === 'last_seen') {
// Sort by date
aVal = new Date(aVal).getTime() || 0;
bVal = new Date(bVal).getTime() || 0;
} else {
// String comparison (case-insensitive)
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (aVal < bVal) return currentSort.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return currentSort.direction === 'asc' ? 1 : -1;
return 0;
});
// Re-append rows in sorted order
rows.forEach(row => tbody.appendChild(row));
// Update sort indicators
updateSortIndicators();
}
function parseAge(ageStr) {
// Parse age strings like "2h 15m", "45m", "3d 5h", "2w 3d"
if (!ageStr) return 0;
let totalMinutes = 0;
const weeks = ageStr.match(/(\d+)w/);
const days = ageStr.match(/(\d+)d/);
const hours = ageStr.match(/(\d+)h/);
const minutes = ageStr.match(/(\d+)m/);
if (weeks) totalMinutes += parseInt(weeks[1]) * 7 * 24 * 60;
if (days) totalMinutes += parseInt(days[1]) * 24 * 60;
if (hours) totalMinutes += parseInt(hours[1]) * 60;
if (minutes) totalMinutes += parseInt(minutes[1]);
return totalMinutes;
}
function updateSortIndicators() {
// Clear all indicators
document.querySelectorAll('.sort-indicator').forEach(indicator => {
indicator.className = 'sort-indicator';
});
// Set current indicator
if (currentSort.column) {
const indicator = document.querySelector(`.sort-indicator[data-column="${currentSort.column}"]`);
if (indicator) {
indicator.className = `sort-indicator ${currentSort.direction}`;
}
}
}
</script>