+37
-9
src/App.svelte
+37
-9
src/App.svelte
···
31
mempool?: {
32
count: number;
33
eta_next_bundle_seconds: number;
34
};
35
latency?: number;
36
}
···
45
status?: StatusResponse | StatusResponseError;
46
modern?: boolean;
47
_head?: boolean;
48
}
49
50
type LastKnownBundle = {
···
53
mempool: number | null;
54
mempoolPercent: number;
55
mempoolBundle: number;
56
time?: string;
57
etaNext?: Date | null;
58
totalSize?: number | null;
···
69
70
let isUpdating = $state(false)
71
let canRefresh = $state(true)
72
-
let isConflict = $state(false)
73
let lastUpdated = $state(new Date())
74
let autoRefreshEnabled = $state(true)
75
let instances = $state<Instance[]>(instancesData.sort(() => Math.random() - 0.5))
···
124
125
function recalculateHead() {
126
isConflict = false
127
-
const headHashes: string[] = []
128
for (const instance of instances) {
129
if (instance.status && 'error' in instance.status) {
130
continue
131
}
132
instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number
133
if (instance._head && instance.status?.bundles?.head_hash) {
134
-
headHashes.push(instance.status.bundles.head_hash)
135
}
136
}
137
-
isConflict = [...new Set(headHashes)].length > 1
138
}
139
140
async function doCheck() {
···
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
···
249
<div>
250
<span class="opacity-50">{#if lastKnownBundle?.time} {formatDistanceToNow(lastKnownBundle.time, { addSuffix: true })}{/if}</span>
251
</div>
252
</div>
253
</div>
254
<div>
···
311
{#each orderBy(instances, ...instanceOrderBy) as instance}
312
<tr>
313
<td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></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
···
31
mempool?: {
32
count: number;
33
eta_next_bundle_seconds: number;
34
+
last_time: Date;
35
};
36
latency?: number;
37
}
···
46
status?: StatusResponse | StatusResponseError;
47
modern?: boolean;
48
_head?: boolean;
49
+
_conflict?: boolean;
50
}
51
52
type LastKnownBundle = {
···
55
mempool: number | null;
56
mempoolPercent: number;
57
mempoolBundle: number;
58
+
lastTime?: Date;
59
time?: string;
60
etaNext?: Date | null;
61
totalSize?: number | null;
···
72
73
let isUpdating = $state(false)
74
let canRefresh = $state(true)
75
+
let consensus = $state({})
76
+
let isConflict = $state(consensus)
77
+
let instancesInConflict = $state<string[]>([])
78
let lastUpdated = $state(new Date())
79
let autoRefreshEnabled = $state(true)
80
let instances = $state<Instance[]>(instancesData.sort(() => Math.random() - 0.5))
···
129
130
function recalculateHead() {
131
isConflict = false
132
+
instancesInConflict = []
133
+
const headHashes: any = {}
134
for (const instance of instances) {
135
if (instance.status && 'error' in instance.status) {
136
continue
137
}
138
instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number
139
if (instance._head && instance.status?.bundles?.head_hash) {
140
+
if (!headHashes[instance.status.bundles.head_hash]) {
141
+
headHashes[instance.status.bundles.head_hash] = []
142
+
}
143
+
headHashes[instance.status.bundles.head_hash].push(instance.url)
144
}
145
}
146
+
// second pass
147
+
const sorted: any = Object.fromEntries(
148
+
Object.entries(headHashes).sort(([, a]: any, [, b]: any) => b.length - a.length)
149
+
)
150
+
for (const instance of instances) {
151
+
if (Object.keys(sorted).length > 1 && Object.keys(sorted)[1] && sorted[Object.keys(sorted)[1]].includes(instance.url)) {
152
+
instance._conflict = true
153
+
instancesInConflict.push(instance.url)
154
+
}
155
+
}
156
+
//const uniq = [...new Set(headHashes)]
157
+
isConflict = instancesInConflict.length > Math.ceil(instances.length/2)
158
}
159
160
async function doCheck() {
···
183
if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool || status.bundles.last_bundle > lastKnownBundle.mempoolBundle)) {
184
lastKnownBundle.mempoolBundle = status.bundles.last_bundle
185
lastKnownBundle.mempool = status.mempool.count
186
+
lastKnownBundle.lastTime = status.mempool.last_time
187
lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100
188
lastKnownBundle.etaNext = status.mempool.eta_next_bundle_seconds ? addSeconds(new Date(), status.mempool.eta_next_bundle_seconds) : null
189
lastKnownBundle.totalSize = status.bundles.total_size
···
270
<div>
271
<span class="opacity-50">{#if lastKnownBundle?.time} {formatDistanceToNow(lastKnownBundle.time, { addSuffix: true })}{/if}</span>
272
</div>
273
+
<div class="mt-1">
274
+
{#if instancesInConflict.length > 0}
275
+
⚠️ Fork alert on {instancesInConflict.length} instances!
276
+
{:else if !isConflict}
277
+
✅ Everything fine!
278
+
{/if}
279
+
</div>
280
</div>
281
</div>
282
<div>
···
339
{#each orderBy(instances, ...instanceOrderBy) as instance}
340
<tr>
341
<td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td>
342
+
<td>{#if instance._head && instance.status?.ok}{#if instance._conflict}⚠️{:else}✅{/if}{:else if instance.status && instance.status?.ok}🔄{:else if instance.status?.error}❌{:else}⌛{/if}</td>
343
{#if instance.status?.error}
344
<td colspan="5" class="opacity-50 text-xs">Error: {instance.status?.error}</td>
345
{:else}
346
+
<td>{#if instance.status?.bundles?.last_bundle}<span class="{instance._conflict ? 'text-error-600' : ''}">{instance.status?.bundles?.last_bundle}</span>{/if}</td>
347
+
<td>{#if instance.status?.mempool && instance._head}<span class="{instance._conflict ? 'text-error-600' : ''}">{formatNumber(instance.status?.mempool.count)}</span>{: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>
348
+
<td>{#if instance.status?.mempool && instance._head}<span class="text-xs opacity-50 {instance._conflict ? 'text-error-600' : ''}">{instance.status?.mempool.last_op_age_seconds || 0}s</span>{/if}</td>
349
+
<td><span class="font-mono text-xs {instance._head ? (instance._conflict ? '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>
350
<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>
351
{/if}
352