feat: service categories end-to-end + monitoring widgets on main dashboard
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

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:
Hermes
2026-06-10 01:49:27 -07:00
parent ea9bdf9598
commit 1d8919532b
15 changed files with 1040 additions and 345 deletions
+45 -2
View File
@@ -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;
})();