this repo has no description
plcbundle-watch.pages.dev
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 = 10 // 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?${Number(new Date())}`)).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 isUpdating = false
110 setTimeout(() => { canRefresh = true }, 500)
111 }
112
113 onMount(async () => {
114 await doCheck()
115
116 setTimeout(() => {
117 if (autoRefreshEnabled) {
118 doCheck()
119 }
120 }, AUTO_REFRESH_INTERVAL * 1000)
121 })
122</script>
123
124<main class="w-full mt-10">
125 <div class="max-w-4xl mx-auto px-3">
126
127 <header class="flex items-center gap-10 flex-wrap">
128 <div class="grow">
129 <h1 class="text-3xl linear-text-gradient">plcbundle instances</h1>
130 </div>
131 <div class="flex items-center gap-6">
132 <Switch class="opacity-75" checked={autoRefreshEnabled} onCheckedChange={(x) => autoRefreshEnabled = x.checked} disabled={isUpdating}>
133 <Switch.Control className="data-[state=checked]:preset-filled-success-500">
134 <Switch.Thumb />
135 </Switch.Control>
136 <Switch.Label>Auto-refresh ({AUTO_REFRESH_INTERVAL}s)</Switch.Label>
137 <Switch.HiddenInput />
138 </Switch>
139 <button type="button" class="btn btn-sm preset-tonal-primary" onclick={() => doCheck()} disabled={isUpdating || canRefresh === false}>Refresh</button>
140 </div>
141 </header>
142
143 <div class="flex gap-10 mt-6 grid grid-cols-2">
144 <div>
145 <h2 class="opacity-75 text-sm">Last known bundle</h2>
146 <div>
147 <div class="flex items-center gap-5">
148 <div class="font-semibold text-3xl">{lastKnownBundle.number}</div>
149 {#if !isConflict}
150 <div class="mt-1 font-mono badge preset-outlined-primary-500 text-xs">{lastKnownBundle?.hash?.slice(0, 7)}</div>
151 {:else}
152 <div class="mt-1 badge preset-filled-error-500">⚠️ conflict!</div>
153 {/if}
154 </div>
155 <div>
156 <span class="opacity-50">{#if lastKnownBundle?.time} {formatDistanceToNow(lastKnownBundle.time, { addSuffix: true })}{/if}</span>
157 </div>
158 </div>
159 </div>
160 <div>
161 <div>
162 <h2 class="opacity-75 text-sm">Next bundle</h2>
163 </div>
164 <div class="flex gap-4">
165 <div class="mt-4">
166 <Progress value={lastKnownBundle.mempoolPercent} class="items-center">
167 <Progress.Circle style="--size: 48px; --thickness: 6px;">
168 <Progress.CircleTrack />
169 <Progress.CircleRange />
170 </Progress.Circle>
171 <!--Progress.ValueText class="text-xs opacity-50" /-->
172 </Progress>
173 </div>
174 {#if lastKnownBundle.number > 0}
175 <div>
176 <div class="font-semibold text-2xl animate-pulse">{lastKnownBundle.number + 1}</div>
177 <div>{formatNumber(lastKnownBundle.mempool)} / {formatNumber(BUNDLE_OPS)} <span class="opacity-50">({lastKnownBundle.mempoolPercent}%)</span></div>
178 {#if lastKnownBundle.etaNext}
179 <div class="mt-2 opacity-50">ETA: {formatDistanceToNow(lastKnownBundle.etaNext)}</div>
180 {/if}
181 </div>
182 {/if}
183 </div>
184 </div>
185 </div>
186
187 <table class="table mt-10">
188 <thead>
189 <tr>
190 <th>endpoint</th>
191 <th>status</th>
192 <th>last bundle</th>
193 <th>mempool</th>
194 <th>head</th>
195 <th>root</th>
196 <th>version</th>
197 <th>latency</th>
198 </tr>
199 </thead>
200 <tbody>
201 {#each orderBy(instances, ...instanceOrderBy) as instance}
202 <tr>
203 <td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td>
204 <td>{#if instance._head}{#if isConflict}⚠️{:else}✅{/if}{:else if instance.status}🔄{:else}⌛{/if}</td>
205 <td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td>
206 <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>
207 <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>
208 <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>
209 <td>{#if instance.status?.server?.version}{instance.status?.server?.version}{/if}</td>
210 <td class="opacity-50">{#if instance.status?.latency}{Math.round(instance.status?.latency)}ms{/if}</td>
211 </tr>
212 {/each}
213 </tbody>
214 </table>
215
216
217 <div class="mt-12">
218 <div>
219 <span class="opacity-75">PLC Directory:</span> <a href="https://{PLC_DIRECTORY}">{PLC_DIRECTORY}</a> <span class="opacity-50">(origin)</span>
220 </div>
221 <div class="mt-2">
222 <span class="opacity-75">Root:</span> <span class="font-mono text-xs">{ROOT.slice(0)}</span>
223 </div>
224 </div>
225
226 <hr class="hr mt-6" />
227 <div class="mt-2 opacity-50">
228 <div>
229 Last updated: {formatISO9075(lastUpdated)}
230 </div>
231 <div class="mt-4">
232 Source: <a href="https://tangled.org/@tree.fail/plcbundle-watch">https://tangled.org/@tree.fail/plcbundle-watch</a>
233 </div>
234 </div>
235
236
237
238 </div>
239</main>
240