settings overhaul, many QOL improvements

This commit is contained in:
serversdwn
2025-12-09 02:08:00 +00:00
parent 690669c697
commit 6fc8721830
11 changed files with 1188 additions and 348 deletions

View File

@@ -149,6 +149,116 @@
} else if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark');
}
// Helper function: Convert timestamp to relative time
function timeAgo(dateString) {
if (!dateString) return 'Never';
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) {
const remainingMins = minutes % 60;
return remainingMins > 0 ? `${hours}h ${remainingMins}m ago` : `${hours}h ago`;
}
const days = Math.floor(hours / 24);
if (days < 7) {
const remainingHours = hours % 24;
return remainingHours > 0 ? `${days}d ${remainingHours}h ago` : `${days}d ago`;
}
const weeks = Math.floor(days / 7);
if (weeks < 4) {
const remainingDays = days % 7;
return remainingDays > 0 ? `${weeks}w ${remainingDays}d ago` : `${weeks}w ago`;
}
const months = Math.floor(days / 30);
return `${months}mo ago`;
}
// Helper function: Get user's selected timezone
function getTimezone() {
return localStorage.getItem('timezone') || 'America/New_York';
}
// Helper function: Format timestamp with tooltip (timezone-aware)
function formatTimestamp(dateString) {
if (!dateString) return '<span class="text-gray-400">Never</span>';
const date = new Date(dateString);
const timezone = getTimezone();
const fullDate = date.toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: timezone,
timeZoneName: 'short'
});
return `<span title="${fullDate}" class="cursor-help">${timeAgo(dateString)}</span>`;
}
// Helper function: Format timestamp as full date/time (no relative time)
// Format: "9/10/2020 8:00 AM EST"
function formatFullTimestamp(dateString) {
if (!dateString) return 'Never';
const date = new Date(dateString);
const timezone = getTimezone();
return date.toLocaleString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZone: timezone,
timeZoneName: 'short'
});
}
// Update all timestamps on page load and periodically
function updateAllTimestamps() {
document.querySelectorAll('[data-timestamp]').forEach(el => {
const timestamp = el.getAttribute('data-timestamp');
el.innerHTML = formatTimestamp(timestamp);
});
}
// Run on load and every minute
updateAllTimestamps();
setInterval(updateAllTimestamps, 60000);
// Copy to clipboard helper
function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(() => {
// Visual feedback
const originalHTML = button.innerHTML;
button.innerHTML = '<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
button.classList.add('text-green-500');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('text-green-500');
}, 1500);
}).catch(err => {
console.error('Failed to copy:', err);
});
}
</script>
{% block extra_scripts %}{% endblock %}

View File

@@ -10,9 +10,15 @@
</div>
{% endif %}
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Fleet overview and recent activity</p>
<div class="mb-8 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Fleet overview and recent activity</p>
</div>
<div class="text-right">
<p class="text-xs text-gray-500 dark:text-gray-400">Last updated</p>
<p id="last-refresh" class="text-sm text-gray-700 dark:text-gray-300 font-mono">--</p>
</div>
</div>
<!-- Dashboard cards with auto-refresh -->
@@ -48,23 +54,35 @@
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
<p class="text-xs text-gray-500 dark:text-gray-500 mb-2 italic">Deployed Status:</p>
<div class="flex justify-between items-center mb-2">
<div class="flex justify-between items-center mb-2" title="Units reporting normally (last seen < 12 hours)">
<div class="flex items-center">
<span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span>
<span class="w-3 h-3 rounded-full bg-green-500 mr-2 flex items-center justify-center">
<svg class="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
</span>
<span class="text-sm text-gray-600 dark:text-gray-400">OK</span>
</div>
<span id="status-ok" class="font-semibold text-green-600 dark:text-green-400">--</span>
</div>
<div class="flex justify-between items-center mb-2">
<div class="flex justify-between items-center mb-2" title="Units with delayed reports (12-24 hours)">
<div class="flex items-center">
<span class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></span>
<span class="w-3 h-3 rounded-full bg-yellow-500 mr-2 flex items-center justify-center">
<svg class="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
</svg>
</span>
<span class="text-sm text-gray-600 dark:text-gray-400">Pending</span>
</div>
<span id="status-pending" class="font-semibold text-yellow-600 dark:text-yellow-400">--</span>
</div>
<div class="flex justify-between items-center">
<div class="flex justify-between items-center" title="Units not reporting (> 24 hours)">
<div class="flex items-center">
<span class="w-3 h-3 rounded-full bg-red-500 mr-2"></span>
<span class="w-3 h-3 rounded-full bg-red-500 mr-2 flex items-center justify-center">
<svg class="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</span>
<span class="text-sm text-gray-600 dark:text-gray-400">Missing</span>
</div>
<span id="status-missing" class="font-semibold text-red-600 dark:text-red-400">--</span>
@@ -185,6 +203,17 @@ function updateDashboard(event) {
try {
const data = JSON.parse(event.detail.xhr.response);
// Update "Last updated" timestamp with timezone
const now = new Date();
const timezone = localStorage.getItem('timezone') || 'America/New_York';
document.getElementById('last-refresh').textContent = now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: timezone,
timeZoneName: 'short'
});
// ===== Fleet summary numbers =====
document.getElementById('total-units').textContent = data.summary?.total ?? 0;
document.getElementById('deployed-units').textContent = data.summary?.active ?? 0;

View File

@@ -46,5 +46,11 @@
{% endfor %}
</div>
{% else %}
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-8">No active units</p>
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No active units</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get started by adding a new unit to the fleet.</p>
</div>
{% endif %}

View File

@@ -46,5 +46,11 @@
{% endfor %}
</div>
{% else %}
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-8">No benched units</p>
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No benched units</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Units awaiting deployment will appear here.</p>
</div>
{% endif %}

View File

@@ -34,6 +34,21 @@
<!-- Fleet Roster with Tabs -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<!-- Search Bar -->
<div class="mb-4">
<div class="relative">
<input
type="text"
id="roster-search"
placeholder="Search by Unit ID, Type, or Note..."
class="w-full px-4 py-2 pl-10 pr-4 text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-seismo-orange"
onkeyup="filterRosterTable()">
<svg class="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<!-- Tab Bar -->
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
<button
@@ -805,6 +820,31 @@
alert(`Error: ${error.message}`);
}
}
// Filter roster table based on search input
function filterRosterTable() {
const searchInput = document.getElementById('roster-search').value.toLowerCase();
const table = document.querySelector('#roster-content table tbody');
if (!table) return;
const rows = table.getElementsByTagName('tr');
for (let row of rows) {
const cells = row.getElementsByTagName('td');
if (cells.length === 0) continue; // Skip header or empty rows
const unitId = cells[1]?.textContent?.toLowerCase() || '';
const unitType = cells[2]?.textContent?.toLowerCase() || '';
const note = cells[6]?.textContent?.toLowerCase() || '';
const matches = unitId.includes(searchInput) ||
unitType.includes(searchInput) ||
note.includes(searchInput);
row.style.display = matches ? '' : 'none';
}
}
</script>
<style>

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,44 @@
{% block content %}
<div class="mb-6">
<a href="/roster" class="text-seismo-orange hover:text-seismo-burgundy inline-flex items-center mb-4">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Back to Fleet Roster
</a>
<!-- Breadcrumb Navigation -->
<nav class="flex mb-4 text-sm" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<li class="inline-flex items-center">
<a href="/" class="text-gray-500 hover:text-seismo-orange dark:text-gray-400">
Dashboard
</a>
</li>
<li>
<div class="flex items-center">
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
<a href="/roster" class="ml-1 text-gray-500 hover:text-seismo-orange dark:text-gray-400">
Fleet Roster
</a>
</div>
</li>
<li aria-current="page">
<div class="flex items-center">
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
<span class="ml-1 text-gray-700 dark:text-gray-300 font-medium" id="breadcrumb-unit">Unit</span>
</div>
</li>
</ol>
</nav>
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white" id="pageTitle">Loading...</h1>
<div class="flex items-center gap-2">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white" id="pageTitle">Loading...</h1>
<button onclick="copyToClipboard(window.currentUnitId, this)" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-1" title="Copy Unit ID">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
<div class="flex gap-3">
<button id="editButton" onclick="enterEditMode()" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -323,8 +353,10 @@ async function loadUnitData() {
// Populate view mode (read-only display)
function populateViewMode() {
// Update page title
// Update page title and store unit ID for copy function
window.currentUnitId = currentUnit.id;
document.getElementById('pageTitle').textContent = `Unit ${currentUnit.id}`;
document.getElementById('breadcrumb-unit').textContent = currentUnit.id;
// Get status info from snapshot
let unitStatus = null;
@@ -348,7 +380,14 @@ function populateViewMode() {
document.getElementById('statusIndicator').className = `w-3 h-3 rounded-full ${statusColors[unitStatus.status] || 'bg-gray-400'}`;
document.getElementById('statusText').className = `font-semibold ${statusTextColors[unitStatus.status] || 'text-gray-600'}`;
document.getElementById('statusText').textContent = unitStatus.status || 'Unknown';
document.getElementById('lastSeen').textContent = unitStatus.last || '--';
// Format "Last Seen" with timezone-aware formatting
if (unitStatus.last && typeof formatFullTimestamp === 'function') {
document.getElementById('lastSeen').textContent = formatFullTimestamp(unitStatus.last);
} else {
document.getElementById('lastSeen').textContent = unitStatus.last || '--';
}
document.getElementById('age').textContent = unitStatus.age || '--';
} else {
document.getElementById('statusIndicator').className = 'w-3 h-3 rounded-full bg-gray-400';