public uptime monitoring + (soon) observability with events saved to PDS
at main 4.4 kB view raw
1<script lang="ts"> 2 import { onMount } from 'svelte'; 3 import { fetchUptimeChecks, fetchInitialUptimeChecks } from './lib/atproto.ts'; 4 import UptimeDisplay from './lib/uptime-display.svelte'; 5 import type { UptimeCheckRecord } from './lib/types.ts'; 6 import { config } from './lib/config.ts'; 7 8 let checks = $state<UptimeCheckRecord[]>([]); 9 let loading = $state(false); 10 let error = $state(''); 11 let lastUpdate = $state<Date | null>(null); 12 let hasRecentEvents = $state(true); 13 let initialLoadComplete = $state(false); 14 let isLoadingMore = $state(false); 15 16 /** 17 * checks if there are any events in the configured threshold period 18 */ 19 function checkForRecentEvents(records: UptimeCheckRecord[]): boolean { 20 const thresholdMinutes = config.inactivityThresholdMinutes || 5; 21 const thresholdTimeAgo = new Date(Date.now() - thresholdMinutes * 60 * 1000); 22 return records.some(record => record.indexedAt > thresholdTimeAgo); 23 } 24 25 async function loadChecks() { 26 if (!initialLoadComplete) { 27 isLoadingMore = true; 28 } else { 29 loading = true; 30 } 31 error = ''; 32 33 try { 34 if (!initialLoadComplete) { 35 // Initial load: fetch records progressively and show in real-time 36 await fetchInitialUptimeChecks(config.pds, config.did, (progressRecords) => { 37 checks = progressRecords; 38 lastUpdate = new Date(); 39 hasRecentEvents = checkForRecentEvents(progressRecords); 40 }); 41 initialLoadComplete = true; 42 } else { 43 // Refresh: fetch only new records and append them 44 const result = await fetchUptimeChecks(config.pds, config.did); 45 if (result.records.length > 0) { 46 // Check if we actually have new records by comparing with existing ones 47 const existingUris = new Set(checks.map(c => c.uri)); 48 const newRecords = result.records.filter(r => !existingUris.has(r.uri)); 49 50 if (newRecords.length > 0) { 51 checks = newRecords.concat(checks); // prepend new records 52 } 53 } 54 lastUpdate = new Date(); 55 hasRecentEvents = checkForRecentEvents(checks); 56 } 57 } catch (err) { 58 error = (err as Error).message || 'failed to fetch uptime checks'; 59 checks = []; 60 } finally { 61 loading = false; 62 isLoadingMore = false; 63 } 64 } 65 66 onMount(() => { 67 // load checks immediately 68 loadChecks(); 69 70 // refresh every 10 seconds 71 const interval = setInterval(loadChecks, 10 * 1000); 72 return () => clearInterval(interval); 73 }); 74</script> 75 76<main class="max-w-6xl mx-auto p-8"> 77 <header class="text-center mb-8"> 78 <h1 class="text-5xl font-bold text-accent mb-2">{config.title}</h1> 79 <p class="text-muted-foreground">{config.subtitle}</p> 80 </header> 81 82 <div class="bg-card rounded-lg shadow-sm p-4 mb-8 flex justify-between items-center"> 83 <div class="flex items-center gap-4"> 84 {#if lastUpdate} 85 <span class="text-sm text-muted-foreground"> 86 last updated: {lastUpdate.toLocaleTimeString()} 87 </span> 88 {/if} 89 {#if isLoadingMore} 90 <span class="text-sm text-primary animate-pulse"> 91 fetching more data... 92 </span> 93 {/if} 94 </div> 95 <button 96 class="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50 transition-opacity" 97 onclick={loadChecks} 98 disabled={loading || isLoadingMore} 99 > 100 {loading || isLoadingMore ? 'loading...' : 'refresh'} 101 </button> 102 </div> 103 104 {#if error} 105 <div class="bg-destructive/10 text-destructive rounded-lg p-4 mb-4"> 106 {error} 107 </div> 108 {/if} 109 110 {#if config.showInactivityWarning && !hasRecentEvents && checks.length > 0} 111 <div class="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-lg p-4 mb-4"> 112 <strong>⚠️ Warning:</strong> No uptime pings received in the last {config.inactivityThresholdMinutes || 5} minutes. 113 The uptime monitor may be offline or experiencing issues. 114 </div> 115 {/if} 116 117 {#if loading && checks.length === 0} 118 <div class="text-center py-12 bg-card rounded-lg shadow-sm text-muted-foreground"> 119 fetching uptime data... 120 </div> 121 {:else} 122 <UptimeDisplay {checks} /> 123 {/if} 124 125 <footer class="mt-12 pt-8 border-t border-border text-center text-sm text-muted-foreground"> 126 <p> 127 built by <a href="https://bsky.app/profile/nekomimi.pet" target="_blank" rel="noopener noreferrer" class="text-accent hover:underline">@nekomimi.pet</a> 128 · <a href="https://tangled.org/@nekomimi.pet/cute-monitor" target="_blank" rel="noopener noreferrer" class="text-accent hover:underline">source</a> 129 </p> 130 </footer> 131</main> 132