+3
-1
web/config.example.json
+3
-1
web/config.example.json
···
2
2
"pds": "https://bsky.social",
3
3
"did": "did:plc:your-did-here",
4
4
"title": "cuteuptime",
5
-
"subtitle": "cute uptime monitoring using your PDS to store events"
5
+
"subtitle": "cute uptime monitoring using your PDS to store events",
6
+
"showInactivityWarning": true,
7
+
"inactivityThresholdMinutes": 5
6
8
}
7
9
+59
-13
web/src/app.svelte
+59
-13
web/src/app.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
-
import { fetchUptimeChecks } from './lib/atproto.ts';
3
+
import { fetchUptimeChecks, fetchInitialUptimeChecks } from './lib/atproto.ts';
4
4
import UptimeDisplay from './lib/uptime-display.svelte';
5
5
import type { UptimeCheckRecord } from './lib/types.ts';
6
6
import { config } from './lib/config.ts';
7
7
8
8
let checks = $state<UptimeCheckRecord[]>([]);
9
-
let loading = $state(true);
9
+
let loading = $state(false);
10
10
let error = $state('');
11
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
+
}
12
24
13
25
async function loadChecks() {
14
-
loading = true;
26
+
if (!initialLoadComplete) {
27
+
isLoadingMore = true;
28
+
} else {
29
+
loading = true;
30
+
}
15
31
error = '';
16
32
17
33
try {
18
-
checks = await fetchUptimeChecks(config.pds, config.did);
19
-
lastUpdate = new Date();
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
+
}
20
57
} catch (err) {
21
58
error = (err as Error).message || 'failed to fetch uptime checks';
22
59
checks = [];
23
60
} finally {
24
61
loading = false;
62
+
isLoadingMore = false;
25
63
}
26
64
}
27
65
···
48
86
last updated: {lastUpdate.toLocaleTimeString()}
49
87
</span>
50
88
{/if}
89
+
{#if isLoadingMore}
90
+
<span class="text-sm text-primary animate-pulse">
91
+
fetching more data...
92
+
</span>
93
+
{/if}
51
94
</div>
52
95
<button
53
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"
54
97
onclick={loadChecks}
55
-
disabled={loading}
98
+
disabled={loading || isLoadingMore}
56
99
>
57
-
{loading ? 'refreshing...' : 'refresh'}
100
+
{loading || isLoadingMore ? 'loading...' : 'refresh'}
58
101
</button>
59
102
</div>
60
103
···
64
107
</div>
65
108
{/if}
66
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
+
67
117
{#if loading && checks.length === 0}
68
118
<div class="text-center py-12 bg-card rounded-lg shadow-sm text-muted-foreground">
69
-
loading uptime data...
119
+
fetching uptime data...
70
120
</div>
71
-
{:else if checks.length > 0}
121
+
{:else}
72
122
<UptimeDisplay {checks} />
73
-
{:else if !loading}
74
-
<div class="text-center py-12 bg-card rounded-lg shadow-sm text-muted-foreground">
75
-
no uptime data available
76
-
</div>
77
123
{/if}
78
124
79
125
<footer class="mt-12 pt-8 border-t border-border text-center text-sm text-muted-foreground">
+48
-5
web/src/lib/atproto.ts
+48
-5
web/src/lib/atproto.ts
···
4
4
import type { UptimeCheck, UptimeCheckRecord } from './types.ts';
5
5
6
6
/**
7
-
* fetches uptime check records from a PDS for a given DID
7
+
* fetches uptime check records from a PDS for a given DID with cursor support
8
8
*
9
9
* @param pds the PDS URL
10
10
* @param did the DID or handle to fetch records for
11
-
* @returns array of uptime check records
11
+
* @param cursor optional cursor for pagination
12
+
* @returns object with records array and optional next cursor
12
13
*/
13
14
export async function fetchUptimeChecks(
14
15
pds: string,
15
16
did: ActorIdentifier,
16
-
): Promise<UptimeCheckRecord[]> {
17
+
cursor?: string,
18
+
): Promise<{ records: UptimeCheckRecord[]; cursor?: string }> {
17
19
const handler = simpleFetchHandler({ service: pds });
18
20
const rpc = new Client({ handler });
19
21
···
30
32
resolvedDid = did as Did;
31
33
}
32
34
33
-
// fetch uptime check records
35
+
// fetch uptime check records with cursor
34
36
const response = await ok(
35
37
rpc.get('com.atproto.repo.listRecords', {
36
38
params: {
37
39
repo: resolvedDid,
38
40
collection: 'pet.nkp.uptime.check',
39
41
limit: 100,
42
+
cursor,
40
43
},
41
44
}),
42
45
);
43
46
44
47
// transform records into a more usable format
45
-
return response.records.map((record) => ({
48
+
const records = response.records.map((record) => ({
46
49
uri: record.uri,
47
50
cid: record.cid,
48
51
value: record.value as unknown as UptimeCheck,
49
52
indexedAt: new Date((record.value as unknown as UptimeCheck).checkedAt),
50
53
}));
54
+
55
+
return {
56
+
records,
57
+
cursor: response.cursor,
58
+
};
59
+
}
60
+
61
+
/**
62
+
* fetches up to 2000 uptime check records using cursor pagination with progress callback
63
+
*
64
+
* @param pds the PDS URL
65
+
* @param did the DID or handle to fetch records for
66
+
* @param onProgress optional callback called with each batch of records
67
+
* @returns array of uptime check records (max 2000)
68
+
*/
69
+
export async function fetchInitialUptimeChecks(
70
+
pds: string,
71
+
did: ActorIdentifier,
72
+
onProgress?: (records: UptimeCheckRecord[]) => void,
73
+
): Promise<UptimeCheckRecord[]> {
74
+
let allRecords: UptimeCheckRecord[] = [];
75
+
let cursor: string | undefined;
76
+
const maxRecords = 2000;
77
+
const batchSize = 100;
78
+
let totalFetched = 0;
79
+
80
+
do {
81
+
const result = await fetchUptimeChecks(pds, did, cursor);
82
+
const newRecords = result.records;
83
+
allRecords = allRecords.concat(newRecords);
84
+
totalFetched += newRecords.length;
85
+
cursor = result.cursor;
86
+
87
+
// Call progress callback with the accumulated records
88
+
if (onProgress) {
89
+
onProgress(allRecords);
90
+
}
91
+
} while (cursor && totalFetched < maxRecords);
92
+
93
+
return allRecords;
51
94
}
+8
-1
web/src/lib/config.ts
+8
-1
web/src/lib/config.ts
···
9
9
did: ActorIdentifier;
10
10
title: string;
11
11
subtitle: string;
12
+
/** whether to show the 5-minute inactivity warning */
13
+
showInactivityWarning?: boolean;
14
+
/** minutes to check for recent activity (default: 5) */
15
+
inactivityThresholdMinutes?: number;
12
16
};
13
17
14
-
export const config = __CONFIG__;
18
+
export const config = __CONFIG__ as typeof __CONFIG__ & {
19
+
showInactivityWarning: boolean;
20
+
inactivityThresholdMinutes: number;
21
+
};
15
22