/**
* PercoHub — Frontend JS
* Charge les services depuis api.php, rend les catégories/cards, auto-refresh.
*/
// ── Config ─────────────────────────────────────────────────────────────────
const REFRESH_INTERVAL = 30; // secondes
const API_BASE = '/api.php';
// ── State ──────────────────────────────────────────────────────────────────
let refreshTimer = null;
let countdown = REFRESH_INTERVAL;
let activeFilter = 'all';
let lastData = null;
// ── Init ───────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
loadAll();
startCountdown();
});
// ── Load ───────────────────────────────────────────────────────────────────
async function loadAll(forceShow = false) {
if (forceShow) {
showLoader();
}
try {
const res = await fetch(`${API_BASE}?action=all`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
lastData = data;
renderDashboard(data);
renderGlobalStats(data);
renderFilterBar(data);
hideLoader();
resetCountdown();
} catch (err) {
showError(err.message);
}
}
// ── Render ─────────────────────────────────────────────────────────────────
function renderDashboard(data) {
const dash = document.getElementById('dashboard');
dash.innerHTML = '';
data.categories.forEach(cat => {
if (activeFilter !== 'all' && cat.id !== activeFilter) return;
const section = document.createElement('section');
section.dataset.cat = cat.id;
// Header
const onlineCount = cat.services.filter(s => s.status?.online).length;
section.innerHTML = `
`;
dash.appendChild(section);
const grid = section.querySelector(`#grid-${cat.id}`);
cat.services.forEach(svc => {
grid.appendChild(buildCard(svc, cat.color));
});
});
dash.classList.remove('hidden');
}
function buildCard(svc, catColor) {
const status = svc.status || {};
const isOnline = status.online;
const statusText = status.status_text || 'inconnu';
const hasUrl = !!svc.url;
const hasNote = !!svc.note;
// Status dot class
let dotClass = 'loading';
let badgeClass = 'badge-gray';
let badgeText = statusText;
if (statusText === 'running' || statusText === 'online') {
dotClass = 'online'; badgeClass = 'badge-green'; badgeText = 'running';
} else if (statusText === 'healthy') {
dotClass = 'online'; badgeClass = 'badge-green'; badgeText = 'healthy';
} else if (statusText === 'offline' || statusText === 'absent' || statusText === 'exited') {
dotClass = 'offline'; badgeClass = 'badge-red';
} else if (statusText === 'restarting') {
dotClass = 'warn'; badgeClass = 'badge-yellow';
} else if (statusText === 'inconnu') {
dotClass = 'loading'; badgeClass = 'badge-gray';
}
// Response time badge
let timeBadge = '';
if (status.http?.ms && status.http.ms > 0) {
timeBadge = `${status.http.ms}ms`;
}
// Docker badge
let dockerBadge = '';
if (svc.docker) {
dockerBadge = `🐳`;
}
// Note
let noteBadge = hasNote ? `${svc.note}` : '';
// Tags
const tagsHtml = (svc.tags || []).map(t =>
`#${t}`
).join('');
const card = document.createElement('div');
card.className = `service-card cat-${catColor} ${hasUrl ? 'has-link' : ''}`;
if (hasUrl) {
card.setAttribute('onclick', `openService('${svc.url}', '${svc.name}')`);
card.setAttribute('title', `Ouvrir ${svc.name}`);
}
card.innerHTML = `
${svc.name}
${dockerBadge}
${badgeText}
${timeBadge}
${svc.desc}
${tagsHtml}
${noteBadge}
${hasUrl ? `
${formatUrl(svc.url)}` : ''}
`;
return card;
}
// ── Global stats ───────────────────────────────────────────────────────────
function renderGlobalStats(data) {
const el = document.getElementById('global-stats');
const total = data.total || 0;
const online = data.online || 0;
const offline = total - online;
el.innerHTML = `
${online}
${offline}
${total} services
`;
}
// ── Filter bar ─────────────────────────────────────────────────────────────
function renderFilterBar(data) {
const bar = document.getElementById('filter-bar');
bar.innerHTML = `
`;
data.categories.forEach(cat => {
const btn = document.createElement('button');
btn.className = `filter-btn ${activeFilter === cat.id ? 'active' : ''}`;
btn.textContent = `${cat.icon} ${cat.name}`;
btn.onclick = () => setFilter(cat.id);
bar.appendChild(btn);
});
}
function setFilter(id) {
activeFilter = id;
if (lastData) {
renderDashboard(lastData);
renderFilterBar(lastData);
}
}
// ── Countdown & refresh ────────────────────────────────────────────────────
function startCountdown() {
refreshTimer = setInterval(() => {
countdown--;
const el = document.getElementById('refresh-timer');
if (el) el.textContent = `↻ ${countdown}s`;
if (countdown <= 0) {
loadAll();
}
}, 1000);
}
function resetCountdown() {
countdown = REFRESH_INTERVAL;
}
// ── Helpers ────────────────────────────────────────────────────────────────
function openService(url, name) {
window.open(url, '_blank', 'noopener');
}
function formatUrl(url) {
try {
const u = new URL(url);
return u.host + (u.port ? '' : '') + (u.pathname !== '/' ? u.pathname : '');
} catch {
return url;
}
}
function showLoader() {
document.getElementById('loader').classList.remove('hidden');
document.getElementById('dashboard').classList.add('hidden');
document.getElementById('error-panel').classList.add('hidden');
}
function hideLoader() {
document.getElementById('loader').classList.add('hidden');
}
function showError(msg) {
document.getElementById('loader').classList.add('hidden');
document.getElementById('error-panel').classList.remove('hidden');
document.getElementById('error-msg').textContent = msg;
}
function closeModal(e) {
if (e.target === document.getElementById('modal')) {
document.getElementById('modal').classList.add('hidden');
}
}