<main class="grid" id="grid">
{% for s in services %}
- <a class="card{% if s.lan %} is-lan{% endif %}"
- href="{{ s.href }}"
- rel="noopener noreferrer">
+ <a
+ class="card"
+ href="{{ s.href }}"
+ rel="noopener noreferrer"
+ {% if s.lan %} data-lan="true"{% endif %}
+ >
{% if s.tag %}<span class="chip">{{ s.tag }}</span>{% endif %}
<h3>{{ s.name }}</h3>
<p>{{ s.desc }}</p>
{% endfor %}
</main>
+
<script>
(function () {
- const TIMEOUT_MS = 3500;
-
- // Find all cards that have a URL to test
- const cards = Array.from(document.querySelectorAll('.card[data-url]'));
+ const PING_URL = "https://lan.zndr.dk/ping"; // your HTTPS LAN endpoint
+ const TIMEOUT_MS = 1500;
- function markOnline(card) {
- // If it was previously flagged offline (e.g. navigation back), clear it
- card.classList.remove('is-offline');
- card.removeAttribute('title');
- }
+ const lanCards = Array.from(document.querySelectorAll('.card[data-lan="true"]'));
+ if (!lanCards.length) return;
- function markOffline(card, reason) {
- card.classList.add('is-offline');
- card.title = reason || 'This service did not respond from your current network.';
+ function stripe() {
+ lanCards.forEach(c => c.classList.add('is-lan'));
}
- cards.forEach(card => {
- const base = card.getAttribute('data-url') || '';
- const custom = card.getAttribute('data-probe'); // optional custom path like "/api/health"
- let probeUrl;
-
- try {
- const u = new URL(base);
-
- // Don’t try to fetch http:// from an https:// page (mixed-content would block)
- if (location.protocol === 'https:' && u.protocol !== 'https:') {
- markOffline(card, 'Blocked mixed-content (HTTP) from HTTPS page.');
- return;
- }
-
- // Build probe URL: custom path (same origin) or /favicon.ico
- probeUrl = (custom ? (u.origin + custom) : (u.origin + '/favicon.ico')) + '?t=' + Date.now();
-
- const img = new Image();
- const timer = setTimeout(() => {
- img.src = ''; // cancel
- markOffline(card, 'Timed out.');
- }, TIMEOUT_MS);
-
- img.onload = () => { clearTimeout(timer); markOnline(card); };
- img.onerror = () => { clearTimeout(timer); markOffline(card, 'Probe failed.'); };
-
- img.referrerPolicy = 'no-referrer';
- img.decoding = 'async';
- img.src = probeUrl;
-
- } catch (e) {
- markOffline(card, 'Invalid URL.');
- }
- });
+ // Try a fast CORS fetch: only a readable 200 at home should count as "LAN"
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
+
+ fetch(PING_URL, {
+ signal: ctrl.signal,
+ credentials: "omit",
+ cache: "no-store"
+ })
+ .then(res => {
+ clearTimeout(timer);
+ if (!res.ok) stripe(); // away → stripe
+ // home (res.ok) → do nothing
+ })
+ .catch(() => { clearTimeout(timer); stripe(); }); // error/timeout → stripe
})();
</script>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
+ <!-- Full-bleed gradient background -->
+ <defs>
+ <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0" stop-color="#10a37f"/>
+ <stop offset="1" stop-color="#ef4444"/>
+ </linearGradient>
+ </defs>
+ <rect width="512" height="512" fill="url(#grad)"/>
+
+ <!-- White stacked text, slightly smaller (safer margins) -->
+ <g font-family="system-ui, sans-serif" font-weight="700" fill="#fff" text-anchor="middle">
+ <text x="256" y="220" font-size="180">ZN</text>
+ <text x="256" y="420" font-size="180">DR</text>
+ </g>
+</svg>
+