Initial commit: DashCaddy v1.0

Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

384
status/js/core/grid.js Normal file
View File

@@ -0,0 +1,384 @@
// ========== GRID & STATUS HELPERS ==========
(function () {
/* Enhanced status helpers with response time tracking */
function setQuick(id, up, responseTime = null) {
const dot = document.getElementById(id + '-dot');
const pill = document.getElementById(id + '-pill');
const timeEl = document.getElementById(id + '-time');
const card = document.querySelector(`[data-app="${id}"]`);
if (dot) {
dot.classList.toggle('ok', up);
dot.classList.toggle('bad', !up);
}
if (pill) {
pill.textContent = up ? 'ON' : 'OFF';
pill.classList.toggle('on', up);
pill.classList.toggle('off', !up);
}
if (timeEl && responseTime !== null) {
timeEl.textContent = up ? `${responseTime}ms` : 'timeout';
timeEl.className = `response-time ${getResponseTimeClass(responseTime, up)}`;
}
// Update card status for icon coloring
if (card) {
card.setAttribute('data-status', up ? 'on' : 'off');
}
// Internet card packet blink effect
if (id === 'internet' && dot && up) {
blinkInternetPacket(dot);
}
}
// Internet card packet activity blink
let internetBlinkInterval = null;
function blinkInternetPacket(dot) {
// Alternate between rx (green) and tx (blue) to simulate bidirectional traffic
const isRx = Math.random() > 0.5;
dot.classList.add(isRx ? 'packet-rx' : 'packet-tx');
setTimeout(() => {
dot.classList.remove('packet-rx', 'packet-tx');
}, 150);
}
// Continuous packet simulation for Internet card when online
function startInternetPacketSimulation() {
if (internetBlinkInterval) return;
internetBlinkInterval = setInterval(() => {
const dot = document.getElementById('internet-dot');
const card = document.querySelector('[data-app="internet"]');
if (dot && card && card.getAttribute('data-status') === 'on') {
blinkInternetPacket(dot);
}
}, 300 + Math.random() * 400); // Random interval 300-700ms
}
function stopInternetPacketSimulation() {
if (internetBlinkInterval) {
clearInterval(internetBlinkInterval);
internetBlinkInterval = null;
}
}
// Start simulation on page load
startInternetPacketSimulation();
function getResponseTimeClass(time, isUp) {
if (!isUp) return 'timeout';
if (time < 200) return 'excellent';
if (time < 500) return 'good';
if (time < 1000) return 'fair';
return 'slow';
}
async function checkServiceWithTiming(id) {
const startTime = performance.now();
try {
const r = await fetch('/probe/' + id, { cache: 'no-store' });
const endTime = performance.now();
const responseTime = Math.round(endTime - startTime);
const isUp = (r.status >= 200 && r.status < 400) || r.status === 401 || r.status === 403;
return { isUp, responseTime };
} catch {
const endTime = performance.now();
const responseTime = Math.round(endTime - startTime);
return { isUp: false, responseTime };
}
}
/* App grid - loaded from API */
window.APPS = []; // Use window.APPS as the main array
// Load services from API
async function loadServices() {
try {
if (window.SkeletonLoader) window.SkeletonLoader.show(6);
const response = await fetch('/api/v1/services', { cache: 'no-store' });
if (response.ok) {
window.APPS = await response.json();
if (window.SkeletonLoader) window.SkeletonLoader.hide();
} else {
console.error('Failed to load services:', response.status);
if (window.SkeletonLoader) window.SkeletonLoader.hide();
}
} catch (error) {
console.error('Failed to load services:', error);
if (window.SkeletonLoader) window.SkeletonLoader.hide();
}
}
function serviceUrl(id) { return `https://${buildDomain(id)}`; }
function el(tag, cls, txt) { const n = document.createElement(tag); if (cls) n.className = cls; if (txt) n.textContent = txt; return n; }
function buildGrid() {
const root = document.getElementById('cards'); root.innerHTML = '';
for (let i = 0; i < window.APPS.length; i++) {
const s = window.APPS[i];
if (s.id === 'ca') continue; // DashCA lives in top anchor row
const card = el('div', 'card');
card.setAttribute('data-app', s.id);
card.setAttribute('data-status', 'off'); // Initial status
if (s.recipeId) card.setAttribute('data-recipe-id', s.recipeId);
const dot = el('span', 'dot bad at-bl'); dot.id = 'dot-' + s.id + '-grid'; card.appendChild(dot);
const row = el('div', 'row');
const wrap = el('div', 'logo-wrap');
// Use reliable PNG images with automatic CDN fallback
const img = document.createElement('img');
img.src = s.logo;
img.alt = s.name;
img.className = 'logo-img';
img.onerror = function() {
// Try CDN fallback with multiple naming strategies
// Use id, appTemplate, or derive from name
let appId = s.id || s.appTemplate;
if (!appId && s.name) {
// Derive ID from name (lowercase, remove spaces)
appId = s.name.toLowerCase().replace(/\s+/g, '-');
}
if (appId) {
// Try different CDN URL formats
const cdnUrls = [
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${appId}.png`,
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${appId.toLowerCase()}.png`,
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${appId.replace(/-/g, '')}.png`,
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${s.name.toLowerCase().replace(/\s+/g, '-')}.png`
];
// Remove duplicates
const uniqueUrls = [...new Set(cdnUrls)];
// Find the next URL to try
const currentIndex = uniqueUrls.indexOf(this.src);
const nextIndex = currentIndex + 1;
if (nextIndex < uniqueUrls.length) {
this.src = uniqueUrls[nextIndex];
} else {
this.style.display = 'none';
}
} else {
this.style.display = 'none';
}
};
wrap.appendChild(img);
row.appendChild(wrap);
const nameSpan = el('span', 'name', s.name);
row.appendChild(nameSpan);
// Add Tailscale badge if service is protected
if (s.tailscaleOnly) {
const tsBadge = el('span', 'ts-badge', '🔐');
tsBadge.title = 'Tailscale-only access';
tsBadge.style.cssText = 'margin-left: 6px; font-size: 0.75rem; opacity: 0.8;';
nameSpan.appendChild(tsBadge);
}
row.appendChild(el('span', 'spacer'));
const pill = el('span', 'badge off', 'OFF'); pill.id = 'badge-' + s.id; row.appendChild(pill);
// Update available badge (hidden by default, shown when update detected)
const updateBadge = el('span', 'update-available-badge', 'UPDATE');
updateBadge.id = 'update-badge-' + s.id;
updateBadge.title = 'Update available';
row.appendChild(updateBadge);
card.appendChild(row);
// Add response time row
const responseRow = el('div', 'response-row');
const timeSpan = el('span', 'response-time', '--'); timeSpan.id = 'time-' + s.id;
responseRow.appendChild(timeSpan);
card.appendChild(responseRow);
// Add health/uptime row
const healthRow = el('div', 'health-row');
healthRow.id = 'health-' + s.id;
const uptimeChip = el('span', 'uptime-chip', '--');
uptimeChip.id = 'uptime-' + s.id;
healthRow.appendChild(uptimeChip);
const uptimeMiniBar = document.createElement('div');
uptimeMiniBar.className = 'uptime-mini-bar';
const uptimeFill = document.createElement('div');
uptimeFill.className = 'fill';
uptimeFill.id = 'uptime-bar-' + s.id;
uptimeFill.style.width = '0%';
uptimeMiniBar.appendChild(uptimeFill);
healthRow.appendChild(uptimeMiniBar);
card.appendChild(healthRow);
const btnRow = el('div', 'btn-row');
// Add logs button for services with containerIds
if (s.containerId) {
const logsBtn = el('button', 'logs-btn', '📋');
logsBtn.title = 'View container logs';
logsBtn.onclick = (e) => {
e.stopPropagation();
window.openContainerLogsModal(s.containerId, s.name);
};
btnRow.appendChild(logsBtn);
// Add update button for Docker containers
const updateBtn = el('button', 'update-btn', '⬆️');
updateBtn.title = 'Update container to latest version';
updateBtn.id = `update-btn-${s.id}`;
updateBtn.onclick = (e) => {
e.stopPropagation();
window.updateContainer(s.containerId, s.name, s.id);
};
btnRow.appendChild(updateBtn);
}
// Add logs button for services with logPath (native apps)
if (s.logPath && !s.containerId) {
const logsBtn = el('button', 'logs-btn', '📋');
logsBtn.title = 'View application logs';
logsBtn.onclick = (e) => {
e.stopPropagation();
window.openFileLogsModal(s.logPath, s.name);
};
btnRow.appendChild(logsBtn);
}
// Add credentials button for services that support auto-login
if (s.isExternal || s.appTemplate || s.url) {
const credsBtn = el('button', 'creds-btn', '🔑');
credsBtn.title = 'Auto-login credentials';
credsBtn.id = `creds-btn-${s.id}`;
credsBtn.onclick = (e) => {
e.stopPropagation();
window.openServiceCredsModal(s);
};
btnRow.appendChild(credsBtn);
}
// Add options button for all services except 'internet'
if (s.id !== 'internet') {
const optBtn = el('button', 'options-btn', '⚙️');
optBtn.title = 'Edit service settings';
optBtn.onclick = (e) => {
e.stopPropagation();
window.openServiceEditModal(s);
};
btnRow.appendChild(optBtn);
}
// Add delete button for all services except Internet
if (s.id !== 'internet') {
const delBtn = el('button', 'delete-btn', '🗑️');
delBtn.title = 'Delete this service';
delBtn.onclick = (e) => {
e.stopPropagation();
window.deleteService(s.id, s.name);
};
btnRow.appendChild(delBtn);
}
const btn = el('button', null, 'Open');
btn.onclick = () => window.open(serviceUrl(s.id), '_blank', 'noopener');
btnRow.appendChild(btn);
card.appendChild(btnRow);
root.appendChild(card);
// Staggered loading animation
setTimeout(() => {
card.classList.add('loaded');
}, i * 100); // 100ms delay between each card
}
// Group recipe cards visually after grid is built
if (window.groupRecipeCards) setTimeout(window.groupRecipeCards, 50);
}
function setBadge(id, up, responseTime = null) {
const dot = document.getElementById('dot-' + id + '-grid');
const pill = document.getElementById('badge-' + id);
const timeEl = document.getElementById('time-' + id);
const card = document.querySelector(`[data-app="${id}"]`);
if (dot) {
dot.classList.toggle('ok', up);
dot.classList.toggle('bad', !up);
}
if (pill) {
pill.textContent = up ? 'ON' : 'OFF';
pill.classList.toggle('on', up);
pill.classList.toggle('off', !up);
}
if (timeEl && responseTime !== null) {
timeEl.textContent = up ? `${responseTime}ms` : 'timeout';
timeEl.className = `response-time ${getResponseTimeClass(responseTime, up)}`;
}
// Update card status for icon coloring
if (card) {
card.setAttribute('data-status', up ? 'on' : 'off');
}
}
async function refreshAll() {
// Check DNS servers dynamically (only those configured in SITE.dnsServers)
const dnsIds = Object.keys(SITE.dnsServers);
const topChecks = dnsIds.map(id => checkServiceWithTiming(id));
topChecks.push(checkServiceWithTiming('internet'));
const topResults = await Promise.all(topChecks);
dnsIds.forEach((id, i) => setQuick(id, topResults[i].isUp, topResults[i].responseTime));
const internetResult = topResults[topResults.length - 1];
setQuick('internet', internetResult.isUp, internetResult.responseTime);
// Check app services with timing
const appResults = await Promise.all(
window.APPS.map(async s => {
const result = await checkServiceWithTiming(s.id);
return { id: s.id, ...result };
})
);
appResults.forEach(result => {
setBadge(result.id, result.isUp, result.responseTime);
});
const stamp = document.getElementById('stamp');
if (stamp) stamp.textContent = 'last check: ' + new Date().toLocaleTimeString();
}
// DNS open buttons — use event delegation on .top container
document.querySelector('.top')?.addEventListener('click', (e) => {
const openBtn = e.target.closest('[id$="-open"]');
if (!openBtn) return;
const id = openBtn.id.replace('-open', '');
if (SITE.dnsServers[id]) window.open(serviceUrl(id), '_blank', 'noopener');
});
document.getElementById('ca-open')?.addEventListener('click', () => window.open(serviceUrl('ca'), '_blank', 'noopener'));
document.getElementById('creds-btn-ca')?.addEventListener('click', (e) => { e.stopPropagation(); const s = window.APPS.find(a => a.id === 'ca'); if (s && window.openServiceCredsModal) window.openServiceCredsModal(s); });
document.getElementById('options-btn-ca')?.addEventListener('click', (e) => { e.stopPropagation(); const s = window.APPS.find(a => a.id === 'ca'); if (s && window.openServiceEditModal) window.openServiceEditModal(s); });
document.getElementById('delete-btn-ca')?.addEventListener('click', (e) => { e.stopPropagation(); if (window.deleteService) window.deleteService('ca', 'DashCA'); });
// Window exports
window.loadServices = loadServices;
window.buildGrid = buildGrid;
window.refreshAll = refreshAll;
window.setQuick = setQuick;
window.setBadge = setBadge;
window.getResponseTimeClass = getResponseTimeClass;
window.checkServiceWithTiming = checkServiceWithTiming;
window.serviceUrl = serviceUrl;
window.el = el;
})();