|
179 | 179 | } |
180 | 180 |
|
181 | 181 | .dot.amber { background: var(--amber); box-shadow: 0 0 6px var(--amber); } |
182 | | - .dot.red { background: #ff3b3b; box-shadow: 0 0 6px #ff3b3b; } |
183 | 182 |
|
184 | 183 | @keyframes dot-pulse { |
185 | 184 | 0%,100% { opacity: 1; } |
|
523 | 522 | <div class="status-strip"> |
524 | 523 | <div class="status-item"><div class="dot"></div> 17 BOTS ACTIVE</div> |
525 | 524 | <div class="status-item"><div class="dot"></div> RASPBERRY PI 5 ONLINE</div> |
526 | | - <div class="status-item" id="wd-item" title="Live cluster watchdog"><div class="dot amber" id="wd-dot"></div> <span id="wd-text">CLUSTER: CHECKING…</span></div> |
527 | 525 | <div class="status-item"><div class="dot amber"></div> WAITING FOR CARDS</div> |
528 | 526 | </div> |
529 | 527 | </header> |
|
708 | 706 | loading.style.animation = 'none'; |
709 | 707 | } |
710 | 708 | })(); |
711 | | - |
712 | | -// --- Live cluster watchdog (reads status/watchdog.json, refreshed by Pi cron) --- |
713 | | -(function watchdog() { |
714 | | - const dot = document.getElementById('wd-dot'); |
715 | | - const text = document.getElementById('wd-text'); |
716 | | - if (!dot || !text) return; |
717 | | - const fmtAge = (iso) => { |
718 | | - const s = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000); |
719 | | - if (s < 90) return Math.round(s) + 's ago'; |
720 | | - if (s < 5400) return Math.round(s / 60) + 'm ago'; |
721 | | - return Math.round(s / 3600) + 'h ago'; |
722 | | - }; |
723 | | - async function tick() { |
724 | | - try { |
725 | | - const r = await fetch('status/watchdog.json?cb=' + Date.now(), { cache: 'no-store' }); |
726 | | - const s = await r.json(); |
727 | | - const stale = (Date.now() - new Date(s.ts).getTime()) > 30 * 60 * 1000; // >30m = stale |
728 | | - const healed = Array.isArray(s.healed) && s.healed.length > 0; |
729 | | - let cls, label; |
730 | | - if (stale) { cls = 'amber'; label = 'CLUSTER: STALE'; } |
731 | | - else if (!s.ok) { cls = 'red'; label = 'CLUSTER: ' + (s.failures ? s.failures.length : 0) + ' ISSUE(S)'; } |
732 | | - else if (healed) { cls = 'amber'; label = 'CLUSTER: SELF-HEALED'; } |
733 | | - else { cls = ''; label = 'CLUSTER: HEALTHY'; } |
734 | | - dot.className = 'dot' + (cls ? ' ' + cls : ''); |
735 | | - text.textContent = label + ' · ' + fmtAge(s.ts); |
736 | | - document.getElementById('wd-item').title = |
737 | | - 'Watchdog ' + s.ts + (s.failures && s.failures.length ? ' — ' + s.failures.join('; ') : ' — all checks passing'); |
738 | | - } catch { |
739 | | - dot.className = 'dot red'; |
740 | | - text.textContent = 'CLUSTER: NO DATA'; |
741 | | - } |
742 | | - } |
743 | | - tick(); |
744 | | - setInterval(tick, 60000); |
745 | | -})(); |
746 | 709 | </script> |
747 | 710 | <div style="position:fixed;bottom:0;left:0;right:0;z-index:999;background:linear-gradient(90deg,#060910,#0b1124,#060910);border-top:1px solid #1e3060;padding:0.6rem 1.5rem;display:flex;align-items:center;justify-content:center;gap:1.2rem;font-family:monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;"><span style="width:7px;height:7px;border-radius:50%;background:#f59e0b;box-shadow:0 0 8px #f59e0b;display:inline-block;"></span><span style="color:#3d5a78;">Try Claude free for 2 weeks —</span><a href="https://claude.ai/referral/4fAMYN9Ing" target="_blank" rel="noopener" style="color:#00b4d8;text-decoration:none;">Start your free trial →</a><span style="color:#3d5a78;">· AI behind this ecosystem</span></div> |
748 | 711 | </body> |
|
0 commit comments