Update: 2026 copyright, private service modals, real-time status checking
This commit is contained in:
parent
0771b37ee2
commit
0ae90b8b85
95
index.html
95
index.html
|
|
@ -266,9 +266,9 @@
|
|||
</div>
|
||||
<h3 class="service-name">Nextcloud</h3>
|
||||
<p class="service-desc">Private cloud storage, files, and collaboration</p>
|
||||
<div class="service-status online">
|
||||
<div class="service-status checking" data-status-url="https://cloud.lemonlink.eu/status.php">
|
||||
<span class="status-dot"></span>
|
||||
<span>Online</span>
|
||||
<span class="status-text">Checking...</span>
|
||||
</div>
|
||||
<div class="service-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -302,9 +302,9 @@
|
|||
</div>
|
||||
<h3 class="service-name">Netdata</h3>
|
||||
<p class="service-desc">Real-time system monitoring and metrics</p>
|
||||
<div class="service-status online">
|
||||
<div class="service-status checking" data-status-url="https://stats.lemonlink.eu/api/v1/info">
|
||||
<span class="status-dot"></span>
|
||||
<span>Online</span>
|
||||
<span class="status-text">Checking...</span>
|
||||
</div>
|
||||
<div class="service-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -313,50 +313,53 @@
|
|||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://photos.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
|
||||
<div class="service-card private-service" data-service="immich">
|
||||
<div class="service-glow"></div>
|
||||
<div class="service-icon" style="--icon-color: #ad5c5c;">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
|
||||
</div>
|
||||
<h3 class="service-name">Immich</h3>
|
||||
<p class="service-desc">Self-hosted photo and video backup solution</p>
|
||||
<div class="service-status online">
|
||||
<div class="service-status checking" data-status-url="https://photos.lemonlink.eu/api/server-info/ping">
|
||||
<span class="status-dot"></span>
|
||||
<span>Online</span>
|
||||
<span class="status-text">Checking...</span>
|
||||
</div>
|
||||
<div class="service-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M7 17L17 7M17 7H7M17 7V17"/>
|
||||
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="https://dash.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
|
||||
<div class="service-card private-service" data-service="homarr">
|
||||
<div class="service-glow"></div>
|
||||
<div class="service-icon" style="--icon-color: #ff5c5c;">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
|
||||
</div>
|
||||
<h3 class="service-name">Homarr</h3>
|
||||
<p class="service-desc">Customizable dashboard for all your services</p>
|
||||
<div class="service-status online">
|
||||
<div class="service-status checking" data-status-url="https://dash.lemonlink.eu">
|
||||
<span class="status-dot"></span>
|
||||
<span>Online</span>
|
||||
<span class="status-text">Checking...</span>
|
||||
</div>
|
||||
<div class="service-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M7 17L17 7M17 7H7M17 7V17"/>
|
||||
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="https://vpn.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
|
||||
<div class="service-card private-service" data-service="tailscale">
|
||||
<div class="service-glow"></div>
|
||||
<div class="service-icon" style="--icon-color: #7b68ee;">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
|
||||
</div>
|
||||
<h3 class="service-name">Tailscale</h3>
|
||||
<p class="service-desc">Zero-config VPN for secure remote access</p>
|
||||
<div class="service-status online">
|
||||
<div class="service-status checking" data-status-url="https://login.tailscale.com">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">Checking...</span>
|
||||
</div>
|
||||
<span class="status-dot"></span>
|
||||
<span>Online</span>
|
||||
</div>
|
||||
|
|
@ -623,11 +626,69 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2024 LemonLink. All rights reserved.</p>
|
||||
<p>© 2026 LemonLink. All rights reserved.</p>
|
||||
<p class="footer-made">Powered by 2x Xeon E5645 | 96GB RAM | TrueNAS SCALE</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Service Info Modals -->
|
||||
<div id="modal-immich" class="service-modal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close">×</button>
|
||||
<h3>📸 Immich Photo Server</h3>
|
||||
<p>Self-hosted photo and video backup solution - a Google Photos alternative.</p>
|
||||
<div class="modal-details">
|
||||
<h4>Features:</h4>
|
||||
<ul>
|
||||
<li>Automatic mobile photo backup</li>
|
||||
<li>AI-powered face recognition</li>
|
||||
<li>Albums and sharing</li>
|
||||
<li>RAW file support</li>
|
||||
</ul>
|
||||
<h4>Access:</h4>
|
||||
<p>This is a <strong>private service</strong>. Contact me for access.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-homarr" class="service-modal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close">×</button>
|
||||
<h3>🎛️ Homarr Dashboard</h3>
|
||||
<p>Personal dashboard aggregating all services in one place.</p>
|
||||
<div class="modal-details">
|
||||
<h4>Features:</h4>
|
||||
<ul>
|
||||
<li>Service status monitoring</li>
|
||||
<li>Quick bookmarks</li>
|
||||
<li>Weather widget</li>
|
||||
<li>Custom integrations</li>
|
||||
</ul>
|
||||
<h4>Access:</h4>
|
||||
<p>This is a <strong>private service</strong>. VPN access required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-tailscale" class="service-modal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close">×</button>
|
||||
<h3>🔒 Tailscale VPN</h3>
|
||||
<p>Zero-configuration mesh VPN for secure remote access to the homelab.</p>
|
||||
<div class="modal-details">
|
||||
<h4>Features:</h4>
|
||||
<ul>
|
||||
<li>Point-to-point encrypted connections</li>
|
||||
<li>No open ports required</li>
|
||||
<li>Multi-device support</li>
|
||||
<li>Exit nodes for secure browsing</li>
|
||||
</ul>
|
||||
<h4>Access:</h4>
|
||||
<p>Tailscale network invitation required. Contact me to join the tailnet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
139
script.js
139
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);
|
||||
|
|
|
|||
184
styles.css
184
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue