+6
bun.lock
+6
bun.lock
···
5
5
"name": "plcbundle-watch",
6
6
"dependencies": {
7
7
"@tailwindcss/vite": "^4.1.16",
8
+
"date-fns": "^4.1.0",
9
+
"numeral": "^2.0.6",
8
10
"tailwindcss": "^4.1.16",
9
11
},
10
12
"devDependencies": {
···
277
279
278
280
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
279
281
282
+
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
283
+
280
284
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
281
285
282
286
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
···
334
338
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
335
339
336
340
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
341
+
342
+
"numeral": ["numeral@2.0.6", "", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="],
337
343
338
344
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
339
345
+2
package.json
+2
package.json
+62
-9
src/App.svelte
+62
-9
src/App.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
+
import { Progress } from '@skeletonlabs/skeleton-svelte';
3
4
import instancesData from './instances.json';
5
+
import { formatDistanceToNow, addSeconds } from 'date-fns';
6
+
import numeral from 'numeral';
7
+
4
8
5
9
type Instance = {
6
10
url: string,
···
12
16
let lastKnownBundle = $state({
13
17
number: 0,
14
18
hash: null,
19
+
mempool: null,
20
+
mempoolPercent: 0,
15
21
})
16
22
17
23
let instances = $state(instancesData)
18
24
let instancesSorted = $derived(instances.sort((a, b) => a.status?.responseTime > b.status?.responseTime ? 1 : -1))
19
25
26
+
function formatNumber(n: number) {
27
+
return numeral(n).format()
28
+
}
29
+
20
30
async function getStatus(instance: Instance) {
21
31
let statusResp: object | undefined;
22
32
let url: string = instance.url;
···
56
66
if (status?.bundles?.last_bundle > lastKnownBundle.number) {
57
67
lastKnownBundle.number = status?.bundles?.last_bundle
58
68
lastKnownBundle.hash = status?.bundles?.head_hash
69
+
lastKnownBundle.time = status?.bundles?.end_time
70
+
71
+
if (status?.mempool?.count > lastKnownBundle.mempool) {
72
+
lastKnownBundle.mempool = status?.mempool?.count
73
+
lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100
74
+
lastKnownBundle.etaNext = addSeconds(new Date(), status?.mempool?.eta_next_bundle_seconds)
75
+
}
59
76
}
60
77
instance.status = status
61
78
}))
···
69
86
<main class="w-full mt-10">
70
87
<div class="max-w-4xl mx-auto px-3">
71
88
72
-
<header>
73
-
<h1 class="text-3xl">plcbundle instances</h1>
89
+
<header class="flex items-center gap-2 flex-wrap">
90
+
<div class="grow">
91
+
<h1 class="text-3xl ">plcbundle instances</h1>
92
+
</div>
93
+
<div class="">
94
+
<button type="button" class="btn btn-sm preset-tonal-primary" onclick={() => doCheck()}>Refresh</button>
95
+
</div>
74
96
</header>
75
97
76
-
<div class="flex items-center gap-2 mt-10 flex-wrap">
77
-
<div class="grow flex items-center text-lg">
78
-
<div><span class="opacity-50">Last known bundle:</span> <span class="font-semibold">{lastKnownBundle.number}</span> [<span class="font-mono text-base">{lastKnownBundle?.hash?.slice(0, 7)}</span>]</div>
98
+
<div class="flex gap-10 mt-6 grid grid-cols-2">
99
+
<div>
100
+
<h2 class="opacity-75 text-sm">Last known bundle</h2>
101
+
<div>
102
+
<div class="flex items-center gap-5">
103
+
<div class="font-semibold text-3xl">{lastKnownBundle.number}</div>
104
+
<div class="mt-1 font-mono badge preset-outlined-primary-500 text-xs">{lastKnownBundle?.hash?.slice(0, 7)}</div>
105
+
</div>
106
+
<div>
107
+
<span class="opacity-50">{#if lastKnownBundle?.time} {formatDistanceToNow(lastKnownBundle.time, { addSuffix: true })}{/if}</span>
108
+
</div>
109
+
</div>
79
110
</div>
80
-
<div class="">
81
-
<button type="button" class="btn btn-sm preset-tonal-primary" onclick={() => doCheck()}>Refresh</button>
111
+
<div>
112
+
<div>
113
+
<h2 class="opacity-75 text-sm">Next bundle</h2>
114
+
</div>
115
+
<div class="flex gap-4">
116
+
<div class="mt-4">
117
+
<Progress value={lastKnownBundle.mempoolPercent} class="items-center">
118
+
<Progress.Circle style="--size: 48px; --thickness: 6px;">
119
+
<Progress.CircleTrack />
120
+
<Progress.CircleRange />
121
+
</Progress.Circle>
122
+
<!--Progress.ValueText class="text-xs opacity-50" /-->
123
+
</Progress>
124
+
</div>
125
+
{#if lastKnownBundle.number > 0}
126
+
<div>
127
+
<div class="font-semibold text-2xl animate-pulse">{lastKnownBundle.number + 1}</div>
128
+
<div>{formatNumber(lastKnownBundle.mempool)} / 10,000 <span class="opacity-50">({lastKnownBundle.mempoolPercent}%)</span></div>
129
+
{#if lastKnownBundle.etaNext}
130
+
<div class="mt-2 opacity-50">ETA: {formatDistanceToNow(lastKnownBundle.etaNext)}</div>
131
+
{/if}
132
+
</div>
133
+
{/if}
134
+
</div>
82
135
</div>
83
136
</div>
84
137
85
-
<table class="table mt-4">
138
+
<table class="table mt-10">
86
139
<thead>
87
140
<tr>
88
141
<th>endpoint</th>
···
101
154
<td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td>
102
155
<td>{#if instance.status?.bundles?.last_bundle === lastKnownBundle.number}✅{:else if instance.status}🔄{:else}⌛{/if}</td>
103
156
<td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td>
104
-
<td>{#if instance.status?.mempool && instance.status?.bundles?.last_bundle === lastKnownBundle.number}{instance.status?.mempool.count}{:else if instance.status}<span class="opacity-25">syncing</span>{/if}</td>
157
+
<td>{#if instance.status?.mempool && instance.status?.bundles?.last_bundle === lastKnownBundle.number}{formatNumber(instance.status?.mempool.count)}{:else if instance.status}<span class="opacity-25">syncing</span>{/if}</td>
105
158
<td><span class="font-mono text-xs">{#if instance.status?.bundles?.head_hash}{instance.status?.bundles?.head_hash.slice(0, 7)}{/if}</span></td>
106
159
<td><span class="font-mono text-xs">{#if instance.status?.bundles?.root_hash}{instance.status?.bundles?.root_hash.slice(0, 7)}{/if}</span></td>
107
160
<td>{#if instance.status?.server?.version}{instance.status?.server?.version}{/if}</td>