better stats

Changed files
+70 -9
src
+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
··· 23 23 }, 24 24 "dependencies": { 25 25 "@tailwindcss/vite": "^4.1.16", 26 + "date-fns": "^4.1.0", 27 + "numeral": "^2.0.6", 26 28 "tailwindcss": "^4.1.16" 27 29 } 28 30 }
+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>