UPDATE
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user