Add service filter, batch operations, and snapshot features

- Service Filter Bar: search by name, filter by status (online/offline)
- Batch Operations: multi-select containers for start/stop/restart
- Container Snapshots: create and manage Docker checkpoints
- Added filter bar and batch action bar to index.html
- Added snapshot button to Admin tools section
- New JS modules: service-filter.js, batch-operations.js, snapshot.js
- Updated build.js to include new modules in bundle
This commit is contained in:
Krystie
2026-05-15 02:17:24 -07:00
parent 0cec447bf1
commit 77688daec7
7 changed files with 642 additions and 186 deletions
+57
View File
@@ -0,0 +1,57 @@
// ========== SERVICE FILTER ==========
(function() {
const searchInput = document.getElementById('service-filter-search');
const statusSelect = document.getElementById('service-filter-status');
const countSpan = document.getElementById('service-filter-count');
function updateFilter() {
const query = searchInput.value.toLowerCase().trim();
const statusFilter = statusSelect.value; // 'all', 'on', or 'off'
const cards = document.querySelectorAll('#cards .card');
let visibleCount = 0;
cards.forEach(card => {
const name = card.querySelector('.name')?.textContent?.toLowerCase() || '';
const app = card.dataset.app?.toLowerCase() || '';
const status = card.dataset.status || 'off'; // 'on' or 'off'
const matchesSearch = !query || name.includes(query) || app.includes(query);
const matchesStatus = statusFilter === 'all' || status === statusFilter;
if (matchesSearch && matchesStatus) {
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
if (countSpan) {
const total = cards.length;
countSpan.textContent = `${visibleCount} of ${total} services`;
}
}
// Debounce helper
function debounce(fn, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}
searchInput?.addEventListener('input', debounce(updateFilter, 200));
statusSelect?.addEventListener('change', updateFilter);
// Initial count on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(updateFilter, 500));
} else {
setTimeout(updateFilter, 500);
}
// Expose for external triggers
window.refreshServiceFilter = updateFilter;
})();