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

@@ -235,14 +235,15 @@ a {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 28px 16px 24px;
align-items: stretch;
text-align: left;
gap: 12px;
min-height: 248px;
padding: 22px 18px 18px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
cursor: pointer;
animation: fadeInUp 0.5s ease both;
}
@@ -253,32 +254,104 @@ a {
background: var(--bg-card-hover);
}
.service-card i {
font-size: 32px;
.service-card-link {
display: flex;
align-items: flex-start;
gap: 14px;
}
.service-card-link i {
font-size: 28px;
color: var(--accent);
margin-bottom: 12px;
flex-shrink: 0;
transition: transform 0.25s ease;
}
.service-card:hover i {
transform: scale(1.1);
.service-card:hover .service-card-link i {
transform: scale(1.08);
}
.card-heading {
display: flex;
flex-direction: column;
min-width: 0;
}
.card-name {
font-size: 14px;
font-size: 15px;
font-weight: 600;
color: var(--text);
line-height: 1.3;
color: var(--text-heading);
line-height: 1.35;
}
.card-sub {
font-size: 11px;
color: var(--text-muted);
margin-top: 6px;
margin-top: 4px;
font-family: 'Inter', monospace;
opacity: 0.7;
}
.card-description {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.card-tag {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border: 1px solid rgba(255, 196, 81, 0.16);
border-radius: 999px;
background: rgba(255, 196, 81, 0.08);
font-size: 10px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: auto;
padding-top: 8px;
}
.service-action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
padding: 0 10px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(255, 255, 255, 0.03);
color: var(--text);
font-size: 12px;
font-weight: 600;
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.service-action:hover {
border-color: var(--border-hover);
background: rgba(255, 196, 81, 0.1);
color: var(--text-heading);
}
/* Staggered animation */
.service-card:nth-child(1) { animation-delay: 0.02s; }
.service-card:nth-child(2) { animation-delay: 0.04s; }
@@ -522,7 +595,7 @@ a {
.search-result-item {
display: flex;
align-items: center;
align-items: flex-start;
gap: 12px;
padding: 12px 20px;
cursor: pointer;
@@ -538,14 +611,31 @@ a {
font-size: 18px;
color: var(--accent);
flex-shrink: 0;
margin-top: 2px;
}
.search-result-item span {
.search-result-body {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.search-result-title {
font-size: 14px;
font-weight: 500;
font-weight: 600;
color: var(--text);
}
.search-result-desc,
.search-result-meta {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-result-empty {
padding: 24px 20px;
text-align: center;
@@ -634,6 +724,14 @@ a {
/* ── Mobile Adjustments ── */
@media (max-width: 768px) {
.card-grid {
grid-template-columns: 1fr;
}
.service-card {
min-height: 0;
}
.search-overlay {
padding: 8vh 16px 0;
}

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);
});