fix: improve roster behavior with in-place rerfresh.

docs: update for 0.9.4
This commit is contained in:
serversdwn
2026-04-10 22:22:25 +00:00
parent 3e0d20d62d
commit f84d0818d2
4 changed files with 140 additions and 9 deletions

View File

@@ -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);
});