1<script lang="ts"> 2 import { onMount } from 'svelte'; 3 import { formatDistanceToNow, addSeconds, formatDate, formatISO9075 } from 'date-fns'; 4 import { Progress, Switch } from '@skeletonlabs/skeleton-svelte'; 5 import orderBy from "lodash/orderBy"; 6 import instancesData from './instances.json'; 7 import numeral from 'numeral'; 8 9 const PLC_DIRECTORY = 'plc.directory' 10 const ROOT = 'cbab6809a136d6a621906ee11199d3b0faf85b422fe0d0d2c346ce8e9dcd7485' 11 const AUTO_REFRESH_INTERVAL = 15 // in seconds 12 const BUNDLE_OPS = 10_000 13 14 type Instance = { 15 url: string, 16 cors?: boolean, 17 status?: object, 18 modern?: boolean, 19 } 20 21 let lastKnownBundle = $state({ 22 number: 0, 23 hash: null, 24 mempool: null, 25 mempoolPercent: 0, 26 }) 27 28 let isUpdating = $state(false) 29 let canRefresh = $state(true) 30 let isConflict = $state(false) 31 let lastUpdated = $state(new Date()) 32 let autoRefreshEnabled = $state(true) 33 let instances = $state(instancesData.sort(() => Math.random() - 0.5)) 34 35 const instanceOrderBy = [['_head', 'status.latency'], ['desc', 'asc']] 36 37 function formatNumber(n: number) { 38 return numeral(n).format() 39 } 40 41 async function getStatus(instance: Instance) { 42 let statusResp: object | undefined; 43 let url: string = instance.url; 44 const start = performance.now(); 45 try { 46 statusResp = await (await fetch(`${url}/status`)).json() 47 } catch (e) {} 48 if (!statusResp) { 49 url = `https://keyoxide.org/api/3/get/http?url=${encodeURIComponent(url)}&format=text&time=${Date.now()}` 50 const indexResp = await (await fetch(url)).text() 51 const [ _, from, to ] = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/) 52 statusResp = { 53 bundles: { 54 last_bundle: Number(to), 55 root_hash: indexResp?.match(/Root: ([a-f0-9]{64})/)[1], 56 head_hash: indexResp?.match(/Head: ([a-f0-9]{64})/)[1], 57 }, 58 server: { 59 uptime: 1, 60 } 61 } 62 } 63 if (statusResp) { 64 statusResp.latency = performance.now() - start; 65 } 66 //if (instance.url === 'https://plc.j4ck.xyz') { statusResp.bundles.head_hash = 'f3ad3544452b2c078cba24990486bb9c277a1155'; } 67 return statusResp 68 } 69 70 function recalculateHead() { 71 isConflict = false 72 const headHashes = [] 73 for (const instance of instances) { 74 instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number 75 if (instance._head) { 76 headHashes.push(instance.status?.bundles?.head_hash) 77 } 78 } 79 isConflict = [...new Set(headHashes)].length > 1 80 } 81 82 async function doCheck() { 83 isUpdating = true 84 canRefresh = false 85 for (const i of instances) { 86 i.status = undefined 87 } 88 89 const statuses = [] 90 91 await Promise.all(instances.map(async (instance) => { 92 const status = await getStatus(instance) 93 instance.status = status 94 if (status?.bundles?.last_bundle > lastKnownBundle.number) { 95 lastKnownBundle.number = status?.bundles?.last_bundle 96 lastKnownBundle.hash = status?.bundles?.head_hash 97 lastKnownBundle.time = status?.bundles?.end_time 98 99 if (status?.mempool?.count > lastKnownBundle.mempool) { 100 lastKnownBundle.mempool = status?.mempool?.count 101 lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100 102 lastKnownBundle.etaNext = addSeconds(new Date(), status?.mempool?.eta_next_bundle_seconds) 103 } 104 } 105 lastUpdated = new Date() 106 107 recalculateHead() 108 })) 109 110 isUpdating = false 111 setTimeout(() => (canRefresh = false), 1000) 112 } 113 114 onMount(async () => { 115 await doCheck() 116 117 setTimeout(() => { 118 if (autoRefreshEnabled) { 119 doCheck() 120 } 121 }, AUTO_REFRESH_INTERVAL * 1000) 122 }) 123</script> 124 125<main class="w-full mt-10"> 126 <div class="max-w-4xl mx-auto px-3"> 127 128 <header class="flex items-center gap-10 flex-wrap"> 129 <div class="grow"> 130 <h1 class="text-3xl linear-text-gradient">plcbundle instances</h1> 131 </div> 132 <div class="flex items-center gap-6"> 133 <Switch class="opacity-75" checked={autoRefreshEnabled} onCheckedChange={(x) => autoRefreshEnabled = x.checked} disabled={isUpdating}> 134 <Switch.Control className="data-[state=checked]:preset-filled-success-500"> 135 <Switch.Thumb /> 136 </Switch.Control> 137 <Switch.Label>Auto-refresh ({AUTO_REFRESH_INTERVAL}s)</Switch.Label> 138 <Switch.HiddenInput /> 139 </Switch> 140 <button type="button" class="btn btn-sm preset-tonal-primary" onclick={() => doCheck()} disabled={canRefresh}>Refresh</button> 141 </div> 142 </header> 143 144 <div class="flex gap-10 mt-6 grid grid-cols-2"> 145 <div> 146 <h2 class="opacity-75 text-sm">Last known bundle</h2> 147 <div> 148 <div class="flex items-center gap-5"> 149 <div class="font-semibold text-3xl">{lastKnownBundle.number}</div> 150 {#if !isConflict} 151 <div class="mt-1 font-mono badge preset-outlined-primary-500 text-xs">{lastKnownBundle?.hash?.slice(0, 7)}</div> 152 {:else} 153 <div class="mt-1 badge preset-filled-error-500">⚠️ conflict!</div> 154 {/if} 155 </div> 156 <div> 157 <span class="opacity-50">{#if lastKnownBundle?.time} {formatDistanceToNow(lastKnownBundle.time, { addSuffix: true })}{/if}</span> 158 </div> 159 </div> 160 </div> 161 <div> 162 <div> 163 <h2 class="opacity-75 text-sm">Next bundle</h2> 164 </div> 165 <div class="flex gap-4"> 166 <div class="mt-4"> 167 <Progress value={lastKnownBundle.mempoolPercent} class="items-center"> 168 <Progress.Circle style="--size: 48px; --thickness: 6px;"> 169 <Progress.CircleTrack /> 170 <Progress.CircleRange /> 171 </Progress.Circle> 172 <!--Progress.ValueText class="text-xs opacity-50" /--> 173 </Progress> 174 </div> 175 {#if lastKnownBundle.number > 0} 176 <div> 177 <div class="font-semibold text-2xl animate-pulse">{lastKnownBundle.number + 1}</div> 178 <div>{formatNumber(lastKnownBundle.mempool)} / {formatNumber(BUNDLE_OPS)} <span class="opacity-50">({lastKnownBundle.mempoolPercent}%)</span></div> 179 {#if lastKnownBundle.etaNext} 180 <div class="mt-2 opacity-50">ETA: {formatDistanceToNow(lastKnownBundle.etaNext)}</div> 181 {/if} 182 </div> 183 {/if} 184 </div> 185 </div> 186 </div> 187 188 <table class="table mt-10"> 189 <thead> 190 <tr> 191 <th>endpoint</th> 192 <th>status</th> 193 <th>last bundle</th> 194 <th>mempool</th> 195 <th>head</th> 196 <th>root</th> 197 <th>version</th> 198 <th>latency</th> 199 </tr> 200 </thead> 201 <tbody> 202 {#each orderBy(instances, ...instanceOrderBy) as instance} 203 <tr> 204 <td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td> 205 <td>{#if instance._head}{#if isConflict}⚠️{:else}{/if}{:else if instance.status}🔄{:else}{/if}</td> 206 <td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td> 207 <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> 208 <td><span class="font-mono text-xs {instance._head ? (isConflict ? 'text-error-600' : 'text-success-600') : 'opacity-50'}">{#if instance.status?.bundles?.head_hash}{instance.status?.bundles?.head_hash.slice(0, 7)}{/if}</span></td> 209 <td><span class="font-mono text-xs {instance.status ? (instance.status?.bundles?.root_hash === ROOT ? 'text-success-600' : 'text-error-600') : ''}">{#if instance.status?.bundles?.root_hash}{instance.status?.bundles?.root_hash.slice(0, 7)}{/if}</span></td> 210 <td>{#if instance.status?.server?.version}{instance.status?.server?.version}{/if}</td> 211 <td class="opacity-50">{#if instance.status?.latency}{Math.round(instance.status?.latency)}ms{/if}</td> 212 </tr> 213 {/each} 214 </tbody> 215 </table> 216 217 218 <div class="mt-12"> 219 <div> 220 <span class="opacity-75">PLC Directory:</span> <a href="https://{PLC_DIRECTORY}">{PLC_DIRECTORY}</a> <span class="opacity-50">(origin)</span> 221 </div> 222 <div class="mt-2"> 223 <span class="opacity-75">Root:</span> <span class="font-mono text-xs">{ROOT.slice(0)}</span> 224 </div> 225 </div> 226 227 <hr class="hr mt-6" /> 228 <div class="mt-2 opacity-50"> 229 <div> 230 Last updated: {formatISO9075(lastUpdated)} 231 </div> 232 <div class="mt-4"> 233 Source: <a href="https://tangled.org/@tree.fail/plcbundle-watch">https://tangled.org/@tree.fail/plcbundle-watch</a> 234 </div> 235 </div> 236 237 238 239 </div> 240</main> 241