public uptime monitoring + (soon) observability with events saved to PDS
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