update

Changed files
+82 -47
src
+3
bun.lock
··· 16 16 "@skeletonlabs/skeleton-svelte": "^4.2.2", 17 17 "@sveltejs/vite-plugin-svelte": "^6.2.1", 18 18 "@tsconfig/svelte": "^5.0.5", 19 + "@types/lodash": "^4.17.20", 19 20 "@types/node": "^24.6.0", 20 21 "svelte": "^5.39.6", 21 22 "svelte-check": "^4.3.2", ··· 186 187 "@tsconfig/svelte": ["@tsconfig/svelte@5.0.5", "", {}, "sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ=="], 187 188 188 189 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 190 + 191 + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], 189 192 190 193 "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], 191 194
+1
package.json
··· 16 16 "@sveltejs/vite-plugin-svelte": "^6.2.1", 17 17 "@tsconfig/svelte": "^5.0.5", 18 18 "@types/node": "^24.6.0", 19 + "@types/lodash": "^4.17.20", 19 20 "svelte": "^5.39.6", 20 21 "svelte-check": "^4.3.2", 21 22 "typescript": "~5.9.3",
+77 -47
src/App.svelte
··· 1 1 <script lang="ts"> 2 + 2 3 import { onMount } from 'svelte'; 3 4 import { filesize } from 'filesize'; 4 5 import { formatDistanceToNow, addSeconds, subSeconds, formatDate, formatISO9075 } from 'date-fns'; 5 6 import { Progress, Switch } from '@skeletonlabs/skeleton-svelte'; 6 - import orderBy from "lodash/orderBy"; 7 + import orderBy from 'lodash/orderBy'; 7 8 import BundleDownloader from './BundleDownloader.svelte'; 8 9 import { formatNumber, formatUptime } from './lib/utils'; 9 10 import instancesData from './instances.json'; ··· 21 22 root_hash: string; 22 23 head_hash: string; 23 24 end_time?: string; 24 - total_size: number; 25 - uncompressed_size: number; 25 + total_size?: number; 26 + uncompressed_size?: number; 26 27 }; 27 28 server: { 28 29 uptime: number; ··· 34 35 latency?: number; 35 36 } 36 37 38 + type StatusResponseError = { 39 + error: string; 40 + } 41 + 37 42 type Instance = { 38 43 url: string; 39 44 cors?: boolean; 40 - status?: StatusResponse; 45 + status?: StatusResponse | StatusResponseError; 41 46 modern?: boolean; 42 47 _head?: boolean; 43 48 } ··· 50 55 mempoolBundle: number; 51 56 time?: string; 52 57 etaNext?: Date | null; 53 - totalSize: number; 54 - totalSizeUncompressed: number; 58 + totalSize?: number | null; 59 + totalSizeUncompressed?: number | null; 55 60 } 56 61 57 62 let lastKnownBundle = $state<LastKnownBundle>({ 58 63 number: 0, 59 64 hash: null, 60 65 mempool: null, 66 + mempoolBundle: 0, 61 67 mempoolPercent: 0, 62 68 }) 63 69 ··· 68 74 let autoRefreshEnabled = $state(true) 69 75 let instances = $state<Instance[]>(instancesData.sort(() => Math.random() - 0.5)) 70 76 71 - const instanceOrderBy = [['_head', 'status.bundles.last_bundle', 'status.latency'], ['desc', 'desc', 'asc']] 77 + const instanceOrderBy = [['status.error', '_head', 'status.bundles.last_bundle', 'status.latency'], ['desc', 'desc', 'desc', 'asc']] 72 78 73 - async function getStatus(instance: Instance): Promise<StatusResponse | undefined> { 79 + async function getStatus(instance: Instance): Promise<StatusResponse | StatusResponseError> { 74 80 let statusResp: StatusResponse | undefined; 75 81 let url: string = instance.url; 82 + let lastError: string | undefined; 76 83 const start = performance.now(); 77 84 try { 78 85 statusResp = await (await fetch(`${url}/status?${Number(new Date())}`)).json() 79 - } catch (e) {} 86 + } catch (e: any) { 87 + lastError = e.message; 88 + } 80 89 if (!statusResp) { 81 90 url = `https://keyoxide.org/api/3/get/http?url=${encodeURIComponent(url)}&format=text&time=${Date.now()}` 82 - const indexResp = await (await fetch(url)).text() 91 + 92 + let indexResp: string | undefined; 93 + try { 94 + indexResp = await (await fetch(url)).text() 95 + } catch(e: any) { 96 + lastError = e.message; 97 + } 83 98 const match = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/) 84 99 if (match) { 85 100 const [, from, to] = match ··· 87 102 const headMatch = indexResp?.match(/Head: ([a-f0-9]{64})/) 88 103 89 104 statusResp = { 105 + ok: true, 90 106 bundles: { 91 107 last_bundle: Number(to), 92 108 root_hash: rootMatch ? rootMatch[1] : '', ··· 99 115 } 100 116 } 101 117 if (statusResp) { 118 + statusResp.ok = true 102 119 statusResp.latency = performance.now() - start; 103 120 } 104 121 //if (instance.url === 'https://plc.j4ck.xyz') { statusResp.bundles.head_hash = 'f3ad3544452b2c078cba24990486bb9c277a1155'; } 105 - return statusResp 122 + return statusResp ?? { error: lastError || 'unknown error' } 106 123 } 107 124 108 125 function recalculateHead() { 109 126 isConflict = false 110 127 const headHashes: string[] = [] 111 128 for (const instance of instances) { 129 + if (instance.status && 'error' in instance.status) { 130 + continue 131 + } 112 132 instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number 113 133 if (instance._head && instance.status?.bundles?.head_hash) { 114 134 headHashes.push(instance.status.bundles.head_hash) ··· 121 141 isUpdating = true 122 142 canRefresh = false 123 143 for (const i of instances) { 124 - if (i.status) { 144 + if (i.status && 'ok' in i.status) { 125 145 i.status.ok = false 126 146 } 127 147 } 128 148 129 149 await Promise.all(instances.map(async (instance) => { 130 150 const status = await getStatus(instance) 131 - instance.status = status 132 - if (instance.status) { 133 - instance.status.ok = true 151 + if (!status) { 152 + return false 134 153 } 135 154 136 - if (status?.bundles?.last_bundle && status.bundles.last_bundle >= lastKnownBundle.number) { 137 - lastKnownBundle.number = status.bundles.last_bundle 138 - lastKnownBundle.hash = status.bundles.head_hash 139 - lastKnownBundle.time = status.bundles.end_time 155 + instance.status = status 156 + if ('ok' in status && status.ok) { 140 157 141 - if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool || status.bundles.last_bundle > lastKnownBundle.mempoolBundle)) { 142 - lastKnownBundle.mempoolBundle = status.bundles.last_bundle 143 - lastKnownBundle.mempool = status.mempool.count 144 - lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100 145 - lastKnownBundle.etaNext = status.mempool.eta_next_bundle_seconds ? addSeconds(new Date(), status.mempool.eta_next_bundle_seconds) : null 146 - lastKnownBundle.totalSize = status.bundles.total_size 147 - lastKnownBundle.totalSizeUncompressed = status.bundles.uncompressed_size 158 + if (status?.bundles?.last_bundle && status.bundles.last_bundle >= lastKnownBundle.number) { 159 + lastKnownBundle.number = status.bundles.last_bundle 160 + lastKnownBundle.hash = status.bundles.head_hash 161 + lastKnownBundle.time = status.bundles.end_time 162 + 163 + if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool || status.bundles.last_bundle > lastKnownBundle.mempoolBundle)) { 164 + lastKnownBundle.mempoolBundle = status.bundles.last_bundle 165 + lastKnownBundle.mempool = status.mempool.count 166 + lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100 167 + lastKnownBundle.etaNext = status.mempool.eta_next_bundle_seconds ? addSeconds(new Date(), status.mempool.eta_next_bundle_seconds) : null 168 + lastKnownBundle.totalSize = status.bundles.total_size 169 + lastKnownBundle.totalSizeUncompressed = status.bundles.uncompressed_size 170 + } 148 171 } 149 172 } 173 + 150 174 lastUpdated = new Date() 151 175 152 176 recalculateHead() ··· 168 192 169 193 let autoRefreshTimer: ReturnType<typeof setTimeout> | null = null; 170 194 171 - onMount(async () => { 172 - await doCheck() 195 + onMount(() => { 196 + doCheck().then(() => { 197 + const scheduleRefresh = () => { 198 + autoRefreshTimer = setTimeout(() => { 199 + if (autoRefreshEnabled) { 200 + doCheck() 201 + } 202 + scheduleRefresh() 203 + }, AUTO_REFRESH_INTERVAL * 1000) 204 + } 205 + 206 + scheduleRefresh() 173 207 174 - const scheduleRefresh = () => { 175 - autoRefreshTimer = setTimeout(() => { 176 - if (autoRefreshEnabled) { 177 - doCheck() 178 - } 179 - scheduleRefresh() 180 - }, AUTO_REFRESH_INTERVAL * 1000) 181 - } 182 - 183 - scheduleRefresh() 208 + }) 184 209 185 210 return () => { 186 211 if (autoRefreshTimer) { ··· 199 224 </div> 200 225 <div class="flex items-center gap-6"> 201 226 <Switch class="opacity-75" checked={autoRefreshEnabled} onCheckedChange={(x) => autoRefreshEnabled = x.checked} disabled={isUpdating}> 202 - <Switch.Control className="data-[state=checked]:preset-filled-success-500"> 227 + <Switch.Control class="data-[state=checked]:preset-filled-success-500"> 203 228 <Switch.Thumb /> 204 229 </Switch.Control> 205 230 <Switch.Label>Auto-refresh ({AUTO_REFRESH_INTERVAL}s)</Switch.Label> ··· 259 284 <div class="mt-2 grid grid-cols-1 gap-1"> 260 285 <div><span class="opacity-50">Instances:</span> {instances.filter(i => i._head).length} latest / {instances.length} total</div> 261 286 <div><span class="opacity-50">PLC Operations:</span> {formatNumber((lastKnownBundle.number * BUNDLE_OPS) + (lastKnownBundle.mempool || 0))}</div> 262 - <div><span class="opacity-50">Bundles Size:</span> {filesize(lastKnownBundle.totalSize)}</div> 263 - <div><span class="opacity-50">Uncompressed:</span> {filesize(lastKnownBundle.totalSizeUncompressed)}</div> 287 + <div><span class="opacity-50">Bundles Size:</span> {#if lastKnownBundle.totalSize}{filesize(lastKnownBundle.totalSize)}{/if}</div> 288 + <div><span class="opacity-50">Uncompressed:</span> {#if lastKnownBundle.totalSizeUncompressed}{filesize(lastKnownBundle.totalSizeUncompressed)}{/if}</div> 264 289 </div> 265 290 </div> 266 291 {/if} ··· 286 311 {#each orderBy(instances, ...instanceOrderBy) as instance} 287 312 <tr> 288 313 <td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td> 289 - <td>{#if instance._head && instance.status?.ok}{#if isConflict}⚠️{:else}✅{/if}{:else if instance.status && instance.status?.ok}🔄{:else}⌛{/if}</td> 290 - <td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td> 291 - <td>{#if instance.status?.mempool && instance._head}{formatNumber(instance.status?.mempool.count)}{:else if instance.status}<span class="opacity-25 text-xs">syncing</span>{/if}</td> 292 - <td class="text-xs opacity-50">{#if instance.status?.mempool && instance._head}{instance.status?.mempool.last_op_age_seconds || 0}s{/if}</td> 293 - <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> 294 - <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> 314 + <td>{#if instance._head && instance.status?.ok}{#if isConflict}⚠️{:else}✅{/if}{:else if instance.status && instance.status?.ok}🔄{:else if instance.status?.error}❌{:else}⌛{/if}</td> 315 + {#if instance.status?.error} 316 + <td colspan="5" class="opacity-50 text-xs">Error: {instance.status?.error}</td> 317 + {:else} 318 + <td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td> 319 + <td>{#if instance.status?.mempool && instance._head}{formatNumber(instance.status?.mempool.count)}{:else if instance.status?.error}<span class="opacity-25 text-xs">error</span>{:else if instance.status}<span class="opacity-25 text-xs">syncing</span>{/if}</td> 320 + <td class="text-xs opacity-50">{#if instance.status?.mempool && instance._head}{instance.status?.mempool.last_op_age_seconds || 0}s{/if}</td> 321 + <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> 322 + <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> 323 + {/if} 324 + 295 325 <td class="text-xs">{#if instance.status?.server?.version}{instance.status?.server?.version}{/if}</td> 296 326 <td class="text-xs">{#if instance.status?.server?.websocket_enabled}✔︎{:else if instance.status}<span class="opacity-25">-</span>{/if}</td> 297 327 <td class="text-xs">{#if instance.status?.server?.uptime_seconds}{formatUptime(instance.status?.server?.uptime_seconds)}{/if}</td>
+1
src/types/lodash.d.ts
··· 1 + declare module 'lodash/orderBy';