tangled
alpha
login
or
join now
tree.fail
/
plcbundle-watch
9
fork
atom
this repo has no description
plcbundle-watch.pages.dev
9
fork
atom
overview
issues
pulls
1
pipelines
types
tree.fail
4 months ago
0a7b6aa2
69ea997d
1/1
deploy.yml
success
10s
+83
-38
2 changed files
expand all
collapse all
unified
split
.gitignore
src
App.svelte
+1
.gitignore
···
22
*.njsproj
23
*.sln
24
*.sw?
0
···
22
*.njsproj
23
*.sln
24
*.sw?
25
+
.wrangler
+82
-38
src/App.svelte
···
5
import orderBy from "lodash/orderBy";
6
import { formatNumber, formatUptime } from './lib/utils';
7
import instancesData from './instances.json';
8
-
9
const APP_TITLE = 'plcbundle instances'
10
const PLC_DIRECTORY = 'plc.directory'
11
const ROOT = 'cbab6809a136d6a621906ee11199d3b0faf85b422fe0d0d2c346ce8e9dcd7485'
12
const AUTO_REFRESH_INTERVAL = 10 // in seconds
13
const BUNDLE_OPS = 10_000
14
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
15
type Instance = {
16
-
url: string,
17
-
cors?: boolean,
18
-
status?: object,
19
-
modern?: boolean,
0
20
}
21
22
-
let lastKnownBundle = $state({
0
0
0
0
0
0
0
0
0
23
number: 0,
24
hash: null,
25
mempool: null,
···
31
let isConflict = $state(false)
32
let lastUpdated = $state(new Date())
33
let autoRefreshEnabled = $state(true)
34
-
let instances = $state(instancesData.sort(() => Math.random() - 0.5))
35
36
const instanceOrderBy = [['_head', 'status.bundles.last_bundle', 'status.latency'], ['desc', 'asc']]
37
38
-
async function getStatus(instance: Instance) {
39
-
let statusResp: object | undefined;
40
let url: string = instance.url;
41
const start = performance.now();
42
try {
···
45
if (!statusResp) {
46
url = `https://keyoxide.org/api/3/get/http?url=${encodeURIComponent(url)}&format=text&time=${Date.now()}`
47
const indexResp = await (await fetch(url)).text()
48
-
const [ _, from, to ] = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/)
49
-
statusResp = {
50
-
bundles: {
51
-
last_bundle: Number(to),
52
-
root_hash: indexResp?.match(/Root: ([a-f0-9]{64})/)[1],
53
-
head_hash: indexResp?.match(/Head: ([a-f0-9]{64})/)[1],
54
-
},
55
-
server: {
56
-
uptime: 1,
0
0
0
0
0
0
57
}
58
}
59
}
···
66
67
function recalculateHead() {
68
isConflict = false
69
-
const headHashes = []
70
for (const instance of instances) {
71
instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number
72
-
if (instance._head) {
73
-
headHashes.push(instance.status?.bundles?.head_hash)
74
}
75
}
76
isConflict = [...new Set(headHashes)].length > 1
···
83
i.status = undefined
84
}
85
86
-
const statuses = []
87
-
88
await Promise.all(instances.map(async (instance) => {
89
const status = await getStatus(instance)
90
instance.status = status
91
-
if (status?.bundles?.last_bundle > lastKnownBundle.number) {
92
-
lastKnownBundle.number = status?.bundles?.last_bundle
93
-
lastKnownBundle.hash = status?.bundles?.head_hash
94
-
lastKnownBundle.time = status?.bundles?.end_time
95
96
-
if (status?.mempool?.count > lastKnownBundle.mempool) {
97
-
lastKnownBundle.mempool = status?.mempool?.count
98
lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100
99
-
lastKnownBundle.etaNext = addSeconds(new Date(), status?.mempool?.eta_next_bundle_seconds)
100
}
101
}
102
lastUpdated = new Date()
···
108
setTimeout(() => { canRefresh = true }, 500)
109
}
110
111
-
function updateTitle () {
112
-
const arr = []
113
-
if (lastUpdated > 0) {
114
const upCount = instances.filter(i => i._head)
115
arr.push(`${isConflict ? '⚠️' : '✅'} [${upCount.length}/${instances.length}]`)
116
}
···
118
return true
119
}
120
0
0
121
onMount(async () => {
122
await doCheck()
123
124
-
setTimeout(() => {
125
-
if (autoRefreshEnabled) {
126
-
doCheck()
0
0
0
0
0
0
0
0
0
0
0
127
}
128
-
}, AUTO_REFRESH_INTERVAL * 1000)
129
})
130
</script>
131
···
214
<td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td>
215
<td>{#if instance._head}{#if isConflict}⚠️{:else}✅{/if}{:else if instance.status}🔄{:else}⌛{/if}</td>
216
<td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td>
217
-
<td>{#if instance.status?.mempool && instance._head}{formatNumber(instance.status?.mempool.count)}{:else if instance.status}<span class="opacity-25">syncing</span>{/if}</td>
218
<td class="text-xs opacity-50">{#if instance.status?.mempool && instance._head}{instance.status?.mempool.last_op_age_seconds || 0}s{/if}</td>
219
<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>
220
<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>
···
5
import orderBy from "lodash/orderBy";
6
import { formatNumber, formatUptime } from './lib/utils';
7
import instancesData from './instances.json';
8
+
9
const APP_TITLE = 'plcbundle instances'
10
const PLC_DIRECTORY = 'plc.directory'
11
const ROOT = 'cbab6809a136d6a621906ee11199d3b0faf85b422fe0d0d2c346ce8e9dcd7485'
12
const AUTO_REFRESH_INTERVAL = 10 // in seconds
13
const BUNDLE_OPS = 10_000
14
15
+
type StatusResponse = {
16
+
bundles: {
17
+
last_bundle: number;
18
+
root_hash: string;
19
+
head_hash: string;
20
+
end_time?: string;
21
+
};
22
+
server: {
23
+
uptime: number;
24
+
};
25
+
mempool?: {
26
+
count: number;
27
+
eta_next_bundle_seconds: number;
28
+
};
29
+
latency?: number;
30
+
}
31
+
32
type Instance = {
33
+
url: string;
34
+
cors?: boolean;
35
+
status?: StatusResponse;
36
+
modern?: boolean;
37
+
_head?: boolean;
38
}
39
40
+
type LastKnownBundle = {
41
+
number: number;
42
+
hash: string | null;
43
+
mempool: number | null;
44
+
mempoolPercent: number;
45
+
time?: string;
46
+
etaNext?: Date;
47
+
}
48
+
49
+
let lastKnownBundle = $state<LastKnownBundle>({
50
number: 0,
51
hash: null,
52
mempool: null,
···
58
let isConflict = $state(false)
59
let lastUpdated = $state(new Date())
60
let autoRefreshEnabled = $state(true)
61
+
let instances = $state<Instance[]>(instancesData.sort(() => Math.random() - 0.5))
62
63
const instanceOrderBy = [['_head', 'status.bundles.last_bundle', 'status.latency'], ['desc', 'asc']]
64
65
+
async function getStatus(instance: Instance): Promise<StatusResponse | undefined> {
66
+
let statusResp: StatusResponse | undefined;
67
let url: string = instance.url;
68
const start = performance.now();
69
try {
···
72
if (!statusResp) {
73
url = `https://keyoxide.org/api/3/get/http?url=${encodeURIComponent(url)}&format=text&time=${Date.now()}`
74
const indexResp = await (await fetch(url)).text()
75
+
const match = indexResp?.match(/Range:\s+(\d{6}) - (\d{6})/)
76
+
if (match) {
77
+
const [, from, to] = match
78
+
const rootMatch = indexResp?.match(/Root: ([a-f0-9]{64})/)
79
+
const headMatch = indexResp?.match(/Head: ([a-f0-9]{64})/)
80
+
81
+
statusResp = {
82
+
bundles: {
83
+
last_bundle: Number(to),
84
+
root_hash: rootMatch ? rootMatch[1] : '',
85
+
head_hash: headMatch ? headMatch[1] : '',
86
+
},
87
+
server: {
88
+
uptime: 1,
89
+
}
90
}
91
}
92
}
···
99
100
function recalculateHead() {
101
isConflict = false
102
+
const headHashes: string[] = []
103
for (const instance of instances) {
104
instance._head = instance.status?.bundles?.last_bundle === lastKnownBundle.number
105
+
if (instance._head && instance.status?.bundles?.head_hash) {
106
+
headHashes.push(instance.status.bundles.head_hash)
107
}
108
}
109
isConflict = [...new Set(headHashes)].length > 1
···
116
i.status = undefined
117
}
118
0
0
119
await Promise.all(instances.map(async (instance) => {
120
const status = await getStatus(instance)
121
instance.status = status
122
+
if (status?.bundles?.last_bundle && status.bundles.last_bundle > lastKnownBundle.number) {
123
+
lastKnownBundle.number = status.bundles.last_bundle
124
+
lastKnownBundle.hash = status.bundles.head_hash
125
+
lastKnownBundle.time = status.bundles.end_time
126
127
+
if (status?.mempool?.count && (!lastKnownBundle.mempool || status.mempool.count > lastKnownBundle.mempool)) {
128
+
lastKnownBundle.mempool = status.mempool.count
129
lastKnownBundle.mempoolPercent = Math.round((lastKnownBundle.mempool/100)*100)/100
130
+
lastKnownBundle.etaNext = addSeconds(new Date(), status.mempool.eta_next_bundle_seconds)
131
}
132
}
133
lastUpdated = new Date()
···
139
setTimeout(() => { canRefresh = true }, 500)
140
}
141
142
+
function updateTitle() {
143
+
const arr: string[] = []
144
+
if (lastUpdated) {
145
const upCount = instances.filter(i => i._head)
146
arr.push(`${isConflict ? '⚠️' : '✅'} [${upCount.length}/${instances.length}]`)
147
}
···
149
return true
150
}
151
152
+
let autoRefreshTimer: ReturnType<typeof setTimeout> | null = null;
153
+
154
onMount(async () => {
155
await doCheck()
156
157
+
const scheduleRefresh = () => {
158
+
autoRefreshTimer = setTimeout(() => {
159
+
if (autoRefreshEnabled) {
160
+
doCheck()
161
+
}
162
+
scheduleRefresh()
163
+
}, AUTO_REFRESH_INTERVAL * 1000)
164
+
}
165
+
166
+
scheduleRefresh()
167
+
168
+
return () => {
169
+
if (autoRefreshTimer) {
170
+
clearTimeout(autoRefreshTimer)
171
}
172
+
}
173
})
174
</script>
175
···
258
<td><a href={instance.url} target="_blank" class="font-semibold">{instance.url.replace("https://", "")}</a></td>
259
<td>{#if instance._head}{#if isConflict}⚠️{:else}✅{/if}{:else if instance.status}🔄{:else}⌛{/if}</td>
260
<td>{#if instance.status?.bundles?.last_bundle}{instance.status?.bundles?.last_bundle}{/if}</td>
261
+
<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>
262
<td class="text-xs opacity-50">{#if instance.status?.mempool && instance._head}{instance.status?.mempool.last_op_age_seconds || 0}s{/if}</td>
263
<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>
264
<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>