Service categories (described in README roadmap, never wired): - Backend: POST /services now persists category/containerId/port/ip/tailscaleOnly - Backend: POST /services/update accepts category for in-place changes - Frontend: category <select> in add-service modal (local + external) - Frontend: category <select> in edit-service modal with current value - Frontend: All Categories dropdown in service filter bar (auto-populated from both API categories and any categories present on rendered cards) - Frontend: colored category badge (icon + name) on service cards - Frontend: filter auto-refreshes after buildGrid Monitoring on main dashboard (replaces orphaned monitoring-dashboard.html): - New monitoring-widgets.js embeds a 5-card System Overview panel above the filter bar: Services, Containers Up, Avg CPU, Avg Memory, Health - Pulls /api/v1/monitoring/stats + /api/v1/health-checks/status - Auto-refreshes on DC.POLL.STATS (5s), color-coded bars (warn >=65%, bad >=85%) Build: - Added monitoring-widgets.js to init.js bundle in build.js - Rebuilt dist/ bundles (core.js, features.js, init.js) - sw.js cache version bumped automatically - CSP hash regenerated
101 lines
3.7 KiB
JavaScript
101 lines
3.7 KiB
JavaScript
// ========== SERVICE FILTER ==========
|
|
(function() {
|
|
const searchInput = document.getElementById('service-filter-search');
|
|
const statusSelect = document.getElementById('service-filter-status');
|
|
const categorySelect = document.getElementById('service-filter-category');
|
|
const countSpan = document.getElementById('service-filter-count');
|
|
|
|
// Build a single category list from both the API categories and any
|
|
// categories present on the actual rendered cards (covers custom services
|
|
// whose category isn't in TEMPLATE_CATEGORIES).
|
|
function getCategoryList() {
|
|
const seen = new Set();
|
|
const fromCards = new Set();
|
|
document.querySelectorAll('#cards .card[data-category]').forEach(c => {
|
|
const cat = c.dataset.category.trim();
|
|
if (cat) fromCards.add(cat);
|
|
});
|
|
const apiCats = (window.DC_CATEGORIES || (typeof DC !== 'undefined' && DC.CATEGORIES)) || {};
|
|
const all = Object.keys(apiCats).concat([...fromCards].filter(c => !apiCats[c]));
|
|
all.forEach(c => seen.add(c));
|
|
return { list: [...seen], apiCats };
|
|
}
|
|
|
|
function refreshCategoryDropdown() {
|
|
if (!categorySelect) return;
|
|
const { list, apiCats } = getCategoryList();
|
|
const current = categorySelect.value;
|
|
categorySelect.innerHTML = '<option value="all">All Categories</option>';
|
|
list.sort().forEach(name => {
|
|
const info = apiCats[name];
|
|
const opt = document.createElement('option');
|
|
opt.value = name;
|
|
opt.textContent = info ? `${info.icon || ''} ${name}`.trim() : name;
|
|
categorySelect.appendChild(opt);
|
|
});
|
|
// Restore selection if it still exists
|
|
if (current && [...categorySelect.options].some(o => o.value === current)) {
|
|
categorySelect.value = current;
|
|
} else {
|
|
categorySelect.value = 'all';
|
|
}
|
|
}
|
|
|
|
function updateFilter() {
|
|
refreshCategoryDropdown();
|
|
const query = searchInput.value.toLowerCase().trim();
|
|
const statusFilter = statusSelect.value; // 'all', 'on', or 'off'
|
|
const categoryFilter = categorySelect ? categorySelect.value : 'all';
|
|
|
|
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 category = card.dataset.category || '';
|
|
|
|
const matchesSearch = !query || name.includes(query) || app.includes(query);
|
|
const matchesStatus = statusFilter === 'all' || status === statusFilter;
|
|
const matchesCategory = categoryFilter === 'all' || category === categoryFilter;
|
|
|
|
if (matchesSearch && matchesStatus && matchesCategory) {
|
|
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);
|
|
categorySelect?.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 (called after buildGrid to repopulate categories)
|
|
window.refreshServiceFilter = updateFilter;
|
|
window.refreshCategoryDropdown = refreshCategoryDropdown;
|
|
})();
|