Add SSO admin console for dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s

This commit is contained in:
Kroonk
2026-05-21 15:25:51 +02:00
parent 470220b8f4
commit 2125601e56
11 changed files with 816 additions and 207 deletions

144
app.js
View File

@@ -1,15 +1,114 @@
const cards = [...document.querySelectorAll('.service-card')];
const sections = [...document.querySelectorAll('.section')];
const CONFIG_URL = '/services.json?v=1';
const LOCAL_CONFIG_KEY = 'mischlabs.dashboard.config';
const icons = {
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><path d="M9 12l2 2 4-4"></path>',
folder: '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>',
lock: '<rect x="3" y="11" width="18" height="11" rx="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path><circle cx="12" cy="16" r="1"></circle>',
film: '<rect x="2" y="2" width="20" height="20" rx="2.2"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line>',
headphones: '<path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path>',
book: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>',
git: '<circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line><path d="M12 1.05V7"></path><path d="M12 17.01v5.95"></path>',
chess: '<path d="M8 16l-1 4h10l-1-4"></path><path d="M6.5 16h11"></path><path d="M12 2a2 2 0 0 1 2 2c0 1.1-.9 2-2 3-1.1-1-2-1.9-2-3a2 2 0 0 1 2-2z"></path><path d="M9 7 6 10.5c-.6.7-.2 1.5.7 1.5h10.6c.9 0 1.3-.8.7-1.5L15 7"></path><path d="M9 12v4"></path><path d="M15 12v4"></path>',
user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>'
};
let cards = [];
let sections = [];
let config = null;
let activeFilter = 'all';
const searchInput = document.querySelector('#serviceSearch');
const segments = [...document.querySelectorAll('.segment')];
const emptyState = document.querySelector('#emptyState');
const onlineCount = document.querySelector('#onlineCount');
const serviceCount = document.querySelector('#serviceCount');
const lastChecked = document.querySelector('#lastChecked');
let activeFilter = 'all';
const dashboardSections = document.querySelector('#dashboardSections');
function normalize(value) {
return value.toLowerCase().trim();
return String(value || '').toLowerCase().trim();
}
function getLocalConfig() {
try {
const raw = window.localStorage.getItem(LOCAL_CONFIG_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
async function loadConfig() {
const response = await fetch(CONFIG_URL, { cache: 'no-store' });
const baseConfig = await response.json();
return getLocalConfig() || baseConfig;
}
function serviceSearchText(service) {
return [
service.name,
service.description,
service.domain,
service.category,
service.keywords
].join(' ');
}
function createServiceCard(service) {
const card = document.createElement('a');
card.href = service.url;
card.target = '_blank';
card.rel = 'noopener';
card.className = 'card service-card';
card.dataset.category = service.category;
card.dataset.url = service.url;
card.dataset.name = serviceSearchText(service);
card.innerHTML = `
<span class="status-dot" aria-label="Status wird geprueft"></span>
<div class="card-icon" style="--accent: ${service.accent};">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
${icons[service.icon] || icons.folder}
</svg>
</div>
<div class="card-content">
<h3>${service.name}</h3>
<p>${service.description}</p>
</div>
<span class="card-domain">${service.domain}</span>
`;
return card;
}
function renderDashboard(nextConfig) {
dashboardSections.innerHTML = '';
serviceCount.textContent = String(nextConfig.services.length);
nextConfig.categories.forEach((category) => {
const services = nextConfig.services.filter((service) => service.category === category.id);
if (!services.length) return;
const section = document.createElement('section');
section.className = 'section';
section.dataset.category = category.id;
section.innerHTML = `
<div class="section-heading">
<div>
<p class="section-kicker">${category.label}</p>
<h2>${category.title}</h2>
</div>
<span class="section-count">${services.length} ${services.length === 1 ? 'Dienst' : 'Dienste'}</span>
</div>
<div class="grid"></div>
`;
const grid = section.querySelector('.grid');
services.forEach((service) => grid.appendChild(createServiceCard(service)));
dashboardSections.appendChild(section);
});
cards = [...document.querySelectorAll('.service-card')];
sections = [...document.querySelectorAll('.section')];
}
function updateVisibility() {
@@ -77,6 +176,19 @@ async function refreshStatus() {
})}`;
}
function scheduleStatusRefresh() {
const run = () => refreshStatus().catch(() => {
onlineCount.textContent = '--';
lastChecked.textContent = 'Status aktuell nicht verfuegbar.';
});
if ('requestIdleCallback' in window) {
window.requestIdleCallback(run, { timeout: 2000 });
} else {
window.setTimeout(run, 600);
}
}
segments.forEach((segment) => {
segment.addEventListener('click', () => setActiveFilter(segment.dataset.filter));
});
@@ -89,18 +201,14 @@ if ('serviceWorker' in navigator) {
});
}
window.addEventListener('load', () => {
const run = () => refreshStatus().catch(() => {
loadConfig()
.then((nextConfig) => {
config = nextConfig;
renderDashboard(config);
updateVisibility();
scheduleStatusRefresh();
})
.catch(() => {
dashboardSections.innerHTML = '<p class="empty-state">Dashboard-Konfiguration konnte nicht geladen werden.</p>';
onlineCount.textContent = '--';
lastChecked.textContent = 'Status aktuell nicht verfuegbar.';
});
if ('requestIdleCallback' in window) {
window.requestIdleCallback(run, { timeout: 2000 });
} else {
window.setTimeout(run, 600);
}
});
updateVisibility();