This commit is contained in:
2026-03-23 20:32:51 +01:00
parent bf0bf6b185
commit 4bb9d9b123
3 changed files with 295 additions and 73 deletions

View File

@@ -8,6 +8,30 @@
return res.json();
}
function buildSearchText(svc) {
const linkParts = (svc.links || []).flatMap(link => [link.label, link.url]);
return [
svc.name,
svc.description,
svc.sub,
...(svc.tags || []),
...linkParts
].filter(Boolean).join(' ');
}
function getServiceActions(svc) {
const actions = [{ label: 'Open', url: svc.url }, ...(svc.links || [])];
const seen = new Set();
return actions.filter(action => {
if (!action?.label || !action?.url) return false;
const key = `${action.label}::${action.url}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// ── Nav Renderer ──
function renderNav(config) {
const nav = document.getElementById('nav');
@@ -55,11 +79,11 @@
grid.className = 'card-grid';
services.forEach((svc, i) => {
const card = document.createElement('a');
card.href = svc.url;
const card = document.createElement('article');
card.className = 'service-card';
card.dataset.name = svc.name.toLowerCase();
card.dataset.category = cat.id;
card.dataset.searchText = buildSearchText(svc).toLowerCase();
card.style.animationDelay = `${(i + 1) * 0.02}s`;
// Status dot
@@ -67,20 +91,67 @@
dot.className = 'card-status-dot unknown';
card.appendChild(dot);
const primaryLink = document.createElement('a');
primaryLink.href = svc.url;
primaryLink.className = 'service-card-link';
const icon = document.createElement('i');
icon.className = svc.icon;
card.appendChild(icon);
primaryLink.appendChild(icon);
const heading = document.createElement('div');
heading.className = 'card-heading';
const name = document.createElement('span');
name.className = 'card-name';
name.textContent = svc.name;
card.appendChild(name);
heading.appendChild(name);
if (svc.sub) {
const sub = document.createElement('span');
sub.className = 'card-sub';
sub.textContent = svc.sub;
card.appendChild(sub);
heading.appendChild(sub);
}
primaryLink.appendChild(heading);
card.appendChild(primaryLink);
if (svc.description) {
const description = document.createElement('p');
description.className = 'card-description';
description.textContent = svc.description;
card.appendChild(description);
}
if (Array.isArray(svc.tags) && svc.tags.length > 0) {
const tags = document.createElement('div');
tags.className = 'card-tags';
svc.tags.forEach(tagText => {
const tag = document.createElement('span');
tag.className = 'card-tag';
tag.textContent = tagText;
tags.appendChild(tag);
});
card.appendChild(tags);
}
const actions = getServiceActions(svc);
if (actions.length > 0) {
const actionsWrap = document.createElement('div');
actionsWrap.className = 'card-actions';
actions.forEach(action => {
const link = document.createElement('a');
link.href = action.url;
link.className = 'service-action';
link.textContent = action.label;
actionsWrap.appendChild(link);
});
card.appendChild(actionsWrap);
}
grid.appendChild(card);
@@ -169,7 +240,6 @@
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(`[StatusMonitor] ${mon.label} raw:`, JSON.stringify(data).slice(0, 500));
const result = this.parsers[mon.parseType](data);
dot.className = 'status-dot ok';
value.textContent = result;
@@ -201,19 +271,23 @@
const dot = card.querySelector('.card-status-dot');
if (!dot) return;
const url = svc.healthCheckUrl;
const isRelative = url.startsWith('/');
const isHttps = url.startsWith('https://');
// HTTPS cross-origin can't be reliably checked from the browser
// (redirects, CORS headers, etc.) — leave as unknown
if (isHttps) return;
dot.className = 'card-status-dot loading';
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const url = svc.healthCheckUrl;
const isRelative = url.startsWith('/');
const isHttps = url.startsWith('https://');
// Relative URLs and HTTPS: normal fetch. HTTP cross-origin: no-cors.
// Relative URLs (/proxy/...): normal fetch. HTTP same-network: no-cors
const fetchOpts = { signal: controller.signal };
if (!isRelative && !isHttps) {
if (!isRelative) {
fetchOpts.mode = 'no-cors';
}
@@ -320,14 +394,44 @@
this.results.innerHTML = '';
if (!query) return;
const matches = this.services.filter(svc => this.fuzzyMatch(query, svc.name));
const matches = this.services.filter(svc => this.fuzzyMatch(query, buildSearchText(svc))).slice(0, 10);
matches.forEach((svc, i) => {
const item = document.createElement('div');
item.className = 'search-result-item' + (i === this.selectedIndex ? ' active' : '');
item.dataset.url = svc.url;
item.innerHTML = `<i class="${svc.icon}"></i><span>${svc.name}</span>`;
const icon = document.createElement('i');
icon.className = svc.icon;
const body = document.createElement('div');
body.className = 'search-result-body';
const title = document.createElement('span');
title.className = 'search-result-title';
title.textContent = svc.name;
body.appendChild(title);
if (svc.description) {
const desc = document.createElement('span');
desc.className = 'search-result-desc';
desc.textContent = svc.description;
body.appendChild(desc);
}
const metaParts = [];
if (svc.sub) metaParts.push(svc.sub);
if (Array.isArray(svc.tags) && svc.tags.length > 0) metaParts.push(svc.tags.join(' · '));
if (metaParts.length > 0) {
const meta = document.createElement('span');
meta.className = 'search-result-meta';
meta.textContent = metaParts.join(' | ');
body.appendChild(meta);
}
item.appendChild(icon);
item.appendChild(body);
item.addEventListener('click', () => {
window.location.href = svc.url;
@@ -368,8 +472,8 @@
}
allCards.forEach(card => {
const name = card.dataset.name || '';
const match = this.fuzzyMatch(query, name);
const searchText = card.dataset.searchText || card.dataset.name || '';
const match = this.fuzzyMatch(query, searchText);
card.classList.toggle('search-hidden', !match);
card.classList.toggle('search-highlight', match);
});