diff --git a/index.html b/index.html index 1f9eb19..5a50c78 100644 --- a/index.html +++ b/index.html @@ -266,9 +266,9 @@

Nextcloud

Private cloud storage, files, and collaboration

-
+
- Online + Checking...
@@ -302,9 +302,9 @@

Netdata

Real-time system monitoring and metrics

-
+
- Online + Checking...
@@ -313,50 +313,53 @@
- +

Immich

Self-hosted photo and video backup solution

-
- + + + + + + + + diff --git a/script.js b/script.js index fbd1126..912aae9 100644 --- a/script.js +++ b/script.js @@ -465,3 +465,142 @@ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.documentElement.style.setProperty('--transition-normal', '0s'); document.documentElement.style.setProperty('--transition-slow', '0s'); } + + +/** + * Service Status Checker - Real-time health checks + */ +function initServiceStatusChecker() { + const statusElements = document.querySelectorAll('.service-status[data-status-url]'); + + statusElements.forEach(element => { + const url = element.getAttribute('data-status-url'); + checkServiceHealth(element, url); + + // Check every 60 seconds + setInterval(() => checkServiceHealth(element, url), 60000); + }); +} + +async function checkServiceHealth(element, url) { + const statusText = element.querySelector('.status-text'); + const originalText = statusText.textContent; + + try { + // Try to fetch with a short timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(url, { + method: 'HEAD', + mode: 'no-cors', + signal: controller.signal + }); + + clearTimeout(timeoutId); + + // If we get here, service is likely online + element.className = 'service-status online'; + statusText.textContent = 'Online'; + + } catch (error) { + // For no-cors requests, we can't read the response + // Try an alternative approach - image load test for same-origin + testViaImage(element, url, statusText); + } +} + +function testViaImage(element, url, statusText) { + // Extract domain from URL + const urlObj = new URL(url); + const testUrl = `${urlObj.protocol}//${urlObj.hostname}/favicon.ico`; + + const img = new Image(); + img.onload = () => { + element.className = 'service-status online'; + statusText.textContent = 'Online'; + }; + img.onerror = () => { + // Could be offline or just no favicon + // Check if it's a private/internal service + if (element.closest('.private-service')) { + element.className = 'service-status online'; + statusText.textContent = 'Restricted'; + } else { + element.className = 'service-status offline'; + statusText.textContent = 'Offline'; + } + }; + img.src = testUrl + '?t=' + Date.now(); + + // Timeout after 3 seconds + setTimeout(() => { + if (!img.complete) { + img.src = ''; + if (element.closest('.private-service')) { + element.className = 'service-status online'; + statusText.textContent = 'Restricted'; + } else { + element.className = 'service-status maintenance'; + statusText.textContent = 'Unknown'; + } + } + }, 3000); +} + +// Initialize status checker +document.addEventListener('DOMContentLoaded', initServiceStatusChecker); + +/** + * Service Info Modals + */ +function initServiceModals() { + const privateServices = document.querySelectorAll('.private-service'); + const modals = document.querySelectorAll('.service-modal'); + const closeButtons = document.querySelectorAll('.modal-close'); + + // Open modal when clicking private service + privateServices.forEach(service => { + service.addEventListener('click', (e) => { + e.preventDefault(); + const serviceName = service.getAttribute('data-service'); + const modal = document.getElementById(`modal-${serviceName}`); + if (modal) { + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + }); + }); + + // Close modal when clicking close button + closeButtons.forEach(button => { + button.addEventListener('click', () => { + const modal = button.closest('.service-modal'); + modal.classList.remove('active'); + document.body.style.overflow = ''; + }); + }); + + // Close modal when clicking outside + modals.forEach(modal => { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.classList.remove('active'); + document.body.style.overflow = ''; + } + }); + }); + + // Close on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + modals.forEach(modal => { + modal.classList.remove('active'); + }); + document.body.style.overflow = ''; + } + }); +} + +// Initialize modals +document.addEventListener('DOMContentLoaded', initServiceModals); diff --git a/styles.css b/styles.css index 67dc8c8..cab65ca 100644 --- a/styles.css +++ b/styles.css @@ -1512,3 +1512,187 @@ body { background: rgba(234, 179, 8, 0.3); color: var(--color-text); } +/* Selection */ +::selection { + background: rgba(234, 179, 8, 0.3); + color: var(--color-text); +} + +/* ======================================== + Service Status Checking + ======================================== */ +.service-status.checking { + background: rgba(107, 114, 128, 0.1); + border: 1px solid rgba(107, 114, 128, 0.3); + color: #6b7280; +} + +.service-status.checking .status-dot { + background: #6b7280; + animation: pulse-dot 1s infinite; +} + +.service-status.online { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + color: #22c55e; +} + +.service-status.online .status-dot { + background: #22c55e; + animation: pulse-dot 2s infinite; +} + +.service-status.offline { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +.service-status.offline .status-dot { + background: #ef4444; + animation: none; +} + +.service-status.maintenance { + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + color: #f59e0b; +} + +.service-status.maintenance .status-dot { + background: #f59e0b; + animation: pulse-dot 1.5s infinite; +} + +/* Private Services */ +.private-service { + cursor: pointer; +} + +.private-service .service-arrow svg { + width: 20px; + height: 20px; +} + +/* ======================================== + Service Info Modals + ======================================== */ +.service-modal { + display: none; + position: fixed; + inset: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + align-items: center; + justify-content: center; + padding: 2rem; + opacity: 0; + transition: opacity 0.3s ease; +} + +.service-modal.active { + display: flex; + opacity: 1; +} + +.modal-content { + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + padding: 2.5rem; + max-width: 500px; + width: 100%; + position: relative; + transform: translateY(20px); + transition: transform 0.3s ease; + box-shadow: var(--shadow-card); +} + +.service-modal.active .modal-content { + transform: translateY(0); +} + +.modal-close { + position: absolute; + top: 1rem; + right: 1rem; + width: 36px; + height: 36px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text); + font-size: 1.5rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: var(--transition-fast); +} + +.modal-close:hover { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.5); + color: #ef4444; +} + +.modal-content h3 { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1rem; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.modal-content p { + color: var(--color-text-muted); + margin-bottom: 1.5rem; +} + +.modal-details h4 { + font-family: 'Space Grotesk', sans-serif; + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + margin: 1.5rem 0 0.75rem; +} + +.modal-details ul { + list-style: none; + padding: 0; +} + +.modal-details li { + padding: 0.35rem 0; + padding-left: 1.5rem; + position: relative; + color: var(--color-text-muted); +} + +.modal-details li::before { + content: '✓'; + position: absolute; + left: 0; + color: var(--color-primary); + font-weight: bold; +} + +.modal-details strong { + color: var(--color-primary); +} + +@media (max-width: 768px) { + .service-modal { + padding: 1rem; + } + + .modal-content { + padding: 1.5rem; + } +}