fix: improve roster behavior with in-place rerfresh.
docs: update for 0.9.4
This commit is contained in:
@@ -1231,12 +1231,22 @@
|
||||
|
||||
// Refresh device list (applies current client-side filters after load)
|
||||
function refreshDeviceList() {
|
||||
const scrollY = window.scrollY;
|
||||
htmx.ajax('GET', '/partials/devices-all', {
|
||||
target: '#device-content',
|
||||
swap: 'innerHTML'
|
||||
}).then(() => {
|
||||
// Re-apply filters after content loads
|
||||
setTimeout(filterDevices, 100);
|
||||
setTimeout(() => {
|
||||
filterDevices();
|
||||
// Re-apply sort if one was active
|
||||
if (window.currentSort && window.currentSort.column) {
|
||||
// sortTable toggles direction, so pre-flip so the toggle lands on the correct value
|
||||
window.currentSort.direction = window.currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
sortTable(window.currentSort.column);
|
||||
}
|
||||
// Restore scroll position
|
||||
window.scrollTo(0, scrollY);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1249,13 +1259,114 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Silently refresh only the status/age/last-seen fields in the existing rows
|
||||
// without touching the DOM structure, so sort/scroll/filters are undisturbed.
|
||||
async function refreshStatusInPlace() {
|
||||
let snapshot;
|
||||
try {
|
||||
const resp = await fetch('/api/roster/status-snapshot');
|
||||
if (!resp.ok) return;
|
||||
snapshot = await resp.json();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const units = snapshot.units || {};
|
||||
|
||||
// Helper: map status string → dot color class
|
||||
function statusDotClass(status) {
|
||||
if (status === 'OK') return 'bg-green-500';
|
||||
if (status === 'Pending') return 'bg-yellow-500';
|
||||
if (status === 'Missing') return 'bg-red-500';
|
||||
return 'bg-gray-400';
|
||||
}
|
||||
|
||||
// --- Desktop table rows ---
|
||||
document.querySelectorAll('#roster-tbody tr[data-id]').forEach(row => {
|
||||
const uid = row.dataset.id;
|
||||
const u = units[uid];
|
||||
if (!u) return;
|
||||
|
||||
const newStatus = u.status || 'Missing';
|
||||
const newAge = u.age || 'N/A';
|
||||
const newLast = u.last || '';
|
||||
|
||||
// Update data attributes used by sort/filter
|
||||
row.dataset.health = newStatus;
|
||||
row.dataset.age = newAge;
|
||||
row.dataset.lastSeen = newLast;
|
||||
|
||||
// Status dot (first span inside first td)
|
||||
const dot = row.querySelector('td:first-child span:first-child');
|
||||
if (dot && row.dataset.status === 'deployed') {
|
||||
dot.className = `w-3 h-3 rounded-full ${statusDotClass(newStatus)}`;
|
||||
dot.title = newStatus;
|
||||
}
|
||||
|
||||
// Age cell (6th td — index 5)
|
||||
const cells = row.querySelectorAll('td');
|
||||
if (cells[5]) {
|
||||
const ageDiv = cells[5].querySelector('div');
|
||||
if (ageDiv) {
|
||||
ageDiv.textContent = newAge;
|
||||
ageDiv.className = `text-sm ${
|
||||
newStatus === 'Missing' ? 'text-red-600 dark:text-red-400 font-semibold' :
|
||||
newStatus === 'Pending' ? 'text-yellow-600 dark:text-yellow-400' :
|
||||
'text-gray-500 dark:text-gray-400'
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Last-seen cell (5th td — index 4)
|
||||
if (cells[4]) {
|
||||
const lsDiv = cells[4].querySelector('.last-seen-cell');
|
||||
if (lsDiv) {
|
||||
lsDiv.dataset.iso = newLast;
|
||||
lsDiv.textContent = newLast;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Mobile cards ---
|
||||
document.querySelectorAll('.device-card[data-unit-id]').forEach(card => {
|
||||
const uid = card.dataset.unitId;
|
||||
const u = units[uid];
|
||||
if (!u) return;
|
||||
|
||||
const newStatus = u.status || 'Missing';
|
||||
const newAge = u.age || 'N/A';
|
||||
|
||||
card.dataset.health = newStatus;
|
||||
card.dataset.age = newAge;
|
||||
|
||||
// Status dot (first span in header div)
|
||||
const dot = card.querySelector('span.rounded-full:first-child');
|
||||
if (dot && card.dataset.status === 'deployed') {
|
||||
dot.className = `w-4 h-4 rounded-full ${statusDotClass(newStatus)}`;
|
||||
dot.title = newStatus;
|
||||
}
|
||||
|
||||
// Age text — the div containing the clock emoji
|
||||
card.querySelectorAll('.text-sm').forEach(div => {
|
||||
if (div.textContent.includes('🕐')) {
|
||||
div.textContent = `🕐 ${newAge}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update the "last updated" timestamp
|
||||
const ts = document.getElementById('last-updated');
|
||||
if (ts) ts.textContent = new Date().toLocaleTimeString();
|
||||
|
||||
// Re-apply active filters (sort order is untouched since DOM rows weren't moved)
|
||||
filterDevices();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-refresh device list every 30 seconds (increased from 10s to reduce flicker)
|
||||
// Poll status every 30 seconds — updates cells in-place, no DOM restructuring
|
||||
setInterval(() => {
|
||||
const deviceContent = document.getElementById('device-content');
|
||||
if (deviceContent && !isAnyModalOpen()) {
|
||||
// Only auto-refresh if no modal is open
|
||||
refreshDeviceList();
|
||||
if (!isAnyModalOpen()) {
|
||||
refreshStatusInPlace();
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user