feat: service categories end-to-end + monitoring widgets on main dashboard
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
This commit is contained in:
@@ -2,11 +2,50 @@
|
||||
(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;
|
||||
@@ -15,11 +54,13 @@
|
||||
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) {
|
||||
if (matchesSearch && matchesStatus && matchesCategory) {
|
||||
card.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
@@ -44,6 +85,7 @@
|
||||
|
||||
searchInput?.addEventListener('input', debounce(updateFilter, 200));
|
||||
statusSelect?.addEventListener('change', updateFilter);
|
||||
categorySelect?.addEventListener('change', updateFilter);
|
||||
|
||||
// Initial count on page load
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -52,6 +94,7 @@
|
||||
setTimeout(updateFilter, 500);
|
||||
}
|
||||
|
||||
// Expose for external triggers
|
||||
// Expose for external triggers (called after buildGrid to repopulate categories)
|
||||
window.refreshServiceFilter = updateFilter;
|
||||
window.refreshCategoryDropdown = refreshCategoryDropdown;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user