@bsv/wallet-toolbox 2.1.16 → 2.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/docs/client.md +2 -1
- package/docs/storage.md +90 -39
- package/docs/wallet.md +2 -1
- package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
- package/out/src/CWIStyleWalletManager.js +3 -2
- package/out/src/CWIStyleWalletManager.js.map +1 -1
- package/out/src/monitor/Monitor.d.ts.map +1 -1
- package/out/src/monitor/Monitor.js +5 -1
- package/out/src/monitor/Monitor.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +1 -0
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +14 -7
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +2 -0
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +23 -0
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/adminServer/adminServer.d.ts +26 -0
- package/out/src/storage/adminServer/adminServer.d.ts.map +1 -0
- package/out/src/storage/adminServer/adminServer.js +547 -0
- package/out/src/storage/adminServer/adminServer.js.map +1 -0
- package/out/src/storage/adminServer/adminUi.d.ts +2 -0
- package/out/src/storage/adminServer/adminUi.d.ts.map +1 -0
- package/out/src/storage/adminServer/adminUi.js +1024 -0
- package/out/src/storage/adminServer/adminUi.js.map +1 -0
- package/out/src/storage/adminServer/index.all.d.ts +3 -0
- package/out/src/storage/adminServer/index.all.d.ts.map +1 -0
- package/out/src/storage/adminServer/index.all.js +19 -0
- package/out/src/storage/adminServer/index.all.js.map +1 -0
- package/out/src/storage/index.all.d.ts +1 -0
- package/out/src/storage/index.all.d.ts.map +1 -1
- package/out/src/storage/index.all.js +1 -0
- package/out/src/storage/index.all.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderAdminPage = renderAdminPage;
|
|
4
|
+
function renderAdminPage() {
|
|
5
|
+
return `<!DOCTYPE html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
|
+
<title>Monitor Admin</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #f4efe6;
|
|
14
|
+
--panel: #fffaf0;
|
|
15
|
+
--ink: #1f1a14;
|
|
16
|
+
--muted: #6b6257;
|
|
17
|
+
--line: #d9ccb7;
|
|
18
|
+
--accent: #a54b1a;
|
|
19
|
+
--accent-2: #0f766e;
|
|
20
|
+
--warn: #7c2d12;
|
|
21
|
+
--mono: "SFMono-Regular", Menlo, Consolas, monospace;
|
|
22
|
+
--sans: Georgia, "Iowan Old Style", serif;
|
|
23
|
+
}
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
background:
|
|
28
|
+
radial-gradient(circle at top left, rgba(165, 75, 26, 0.12), transparent 28rem),
|
|
29
|
+
linear-gradient(180deg, #f8f2e9 0%, var(--bg) 100%);
|
|
30
|
+
color: var(--ink);
|
|
31
|
+
font-family: var(--sans);
|
|
32
|
+
}
|
|
33
|
+
main { width: 100%; padding: 18px; }
|
|
34
|
+
h1, h2, h3 { margin: 0 0 8px; font-weight: 700; }
|
|
35
|
+
h1 { font-size: 1.7rem; line-height: 1.1; }
|
|
36
|
+
h2 { font-size: 1.05rem; }
|
|
37
|
+
p, label, button, input, select, table { font-size: 0.88rem; }
|
|
38
|
+
.subtle { color: var(--muted); }
|
|
39
|
+
.panel {
|
|
40
|
+
background: rgba(255, 250, 240, 0.92);
|
|
41
|
+
border: 1px solid var(--line);
|
|
42
|
+
border-radius: 14px;
|
|
43
|
+
padding: 14px;
|
|
44
|
+
box-shadow: 0 10px 28px rgba(59, 36, 14, 0.08);
|
|
45
|
+
backdrop-filter: blur(12px);
|
|
46
|
+
}
|
|
47
|
+
.stack { display: grid; gap: 12px; margin-top: 14px; }
|
|
48
|
+
details.panel {
|
|
49
|
+
padding: 0;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
}
|
|
52
|
+
details.panel[open] summary { border-bottom: 1px solid var(--line); }
|
|
53
|
+
summary {
|
|
54
|
+
list-style: none;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
padding: 14px 16px;
|
|
57
|
+
font-size: 1.1rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
}
|
|
63
|
+
summary::-webkit-details-marker { display: none; }
|
|
64
|
+
summary::after {
|
|
65
|
+
content: '+';
|
|
66
|
+
color: var(--accent);
|
|
67
|
+
font-size: 1.1rem;
|
|
68
|
+
}
|
|
69
|
+
details[open] summary::after { content: '−'; }
|
|
70
|
+
.panel-body { padding: 14px; }
|
|
71
|
+
.toolbar {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-wrap: wrap;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
align-items: end;
|
|
76
|
+
margin: 8px 0;
|
|
77
|
+
}
|
|
78
|
+
.toolbar label {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 3px;
|
|
82
|
+
min-width: 108px;
|
|
83
|
+
}
|
|
84
|
+
input, select, button, textarea {
|
|
85
|
+
border: 1px solid var(--line);
|
|
86
|
+
border-radius: 9px;
|
|
87
|
+
padding: 6px 8px;
|
|
88
|
+
background: #fffdfa;
|
|
89
|
+
color: var(--ink);
|
|
90
|
+
font-family: inherit;
|
|
91
|
+
}
|
|
92
|
+
button {
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
background: linear-gradient(180deg, #fff7ef, #f2dfca);
|
|
95
|
+
min-height: 31px;
|
|
96
|
+
}
|
|
97
|
+
button.primary {
|
|
98
|
+
background: linear-gradient(180deg, #c5622b, var(--accent));
|
|
99
|
+
color: #fff8f2;
|
|
100
|
+
border-color: #8b3f16;
|
|
101
|
+
}
|
|
102
|
+
table {
|
|
103
|
+
width: 100%;
|
|
104
|
+
border-collapse: collapse;
|
|
105
|
+
margin-top: 6px;
|
|
106
|
+
font-family: var(--mono);
|
|
107
|
+
font-size: 0.75rem;
|
|
108
|
+
}
|
|
109
|
+
th, td {
|
|
110
|
+
border-bottom: 1px solid var(--line);
|
|
111
|
+
text-align: left;
|
|
112
|
+
padding: 6px 5px;
|
|
113
|
+
vertical-align: top;
|
|
114
|
+
}
|
|
115
|
+
th { color: var(--muted); font-weight: 600; }
|
|
116
|
+
pre {
|
|
117
|
+
overflow: auto;
|
|
118
|
+
background: #fff;
|
|
119
|
+
border: 1px solid var(--line);
|
|
120
|
+
border-radius: 10px;
|
|
121
|
+
padding: 8px;
|
|
122
|
+
font-family: var(--mono);
|
|
123
|
+
font-size: 0.75rem;
|
|
124
|
+
max-height: 320px;
|
|
125
|
+
}
|
|
126
|
+
details { margin-top: 6px; }
|
|
127
|
+
.event-list {
|
|
128
|
+
display: grid;
|
|
129
|
+
gap: 8px;
|
|
130
|
+
margin-top: 8px;
|
|
131
|
+
}
|
|
132
|
+
.event {
|
|
133
|
+
border-left: 4px solid var(--accent-2);
|
|
134
|
+
padding-left: 8px;
|
|
135
|
+
}
|
|
136
|
+
.row-actions {
|
|
137
|
+
display: flex;
|
|
138
|
+
gap: 4px;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
}
|
|
141
|
+
.notice {
|
|
142
|
+
margin-top: 8px;
|
|
143
|
+
color: var(--warn);
|
|
144
|
+
}
|
|
145
|
+
.stats-table {
|
|
146
|
+
font-family: var(--mono);
|
|
147
|
+
font-size: 0.78rem;
|
|
148
|
+
table-layout: fixed;
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
width: auto;
|
|
151
|
+
min-width: 0;
|
|
152
|
+
}
|
|
153
|
+
.stats-table th:first-child,
|
|
154
|
+
.stats-table td:first-child {
|
|
155
|
+
position: sticky;
|
|
156
|
+
left: 0;
|
|
157
|
+
background: var(--panel);
|
|
158
|
+
}
|
|
159
|
+
.stats-table th,
|
|
160
|
+
.stats-table td {
|
|
161
|
+
padding-top: 4px;
|
|
162
|
+
padding-bottom: 4px;
|
|
163
|
+
}
|
|
164
|
+
.stats-table thead th {
|
|
165
|
+
color: var(--ink);
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
border-bottom: 2px solid #bda98c;
|
|
168
|
+
background: rgba(242, 223, 202, 0.72);
|
|
169
|
+
box-shadow: inset 0 -1px 0 rgba(139, 63, 22, 0.08);
|
|
170
|
+
}
|
|
171
|
+
.stats-table .stats-head-label,
|
|
172
|
+
.stats-table .stats-label {
|
|
173
|
+
text-align: left;
|
|
174
|
+
}
|
|
175
|
+
.stats-table .stats-head-num,
|
|
176
|
+
.stats-table .stats-num {
|
|
177
|
+
text-align: right;
|
|
178
|
+
}
|
|
179
|
+
.stats-table .stats-subrow {
|
|
180
|
+
color: var(--muted);
|
|
181
|
+
}
|
|
182
|
+
.stats-wrap {
|
|
183
|
+
display: inline-block;
|
|
184
|
+
max-width: 100%;
|
|
185
|
+
overflow-x: auto;
|
|
186
|
+
border: 1px solid rgba(217, 204, 183, 0.75);
|
|
187
|
+
border-radius: 12px;
|
|
188
|
+
background: rgba(255, 253, 250, 0.78);
|
|
189
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
|
190
|
+
}
|
|
191
|
+
.stats-table .stats-group td {
|
|
192
|
+
padding-top: 6px;
|
|
193
|
+
border-top: 1px solid rgba(165, 75, 26, 0.18);
|
|
194
|
+
}
|
|
195
|
+
.stats-table .stats-group .stats-label {
|
|
196
|
+
font-weight: 700;
|
|
197
|
+
color: var(--ink);
|
|
198
|
+
}
|
|
199
|
+
.stats-table .stats-child .stats-label {
|
|
200
|
+
padding-left: 1.8ch;
|
|
201
|
+
position: relative;
|
|
202
|
+
}
|
|
203
|
+
.stats-table .stats-child .stats-label::before {
|
|
204
|
+
content: '↳';
|
|
205
|
+
position: absolute;
|
|
206
|
+
left: 0.2ch;
|
|
207
|
+
color: #9a8166;
|
|
208
|
+
}
|
|
209
|
+
.pill {
|
|
210
|
+
display: inline-block;
|
|
211
|
+
border: 1px solid var(--line);
|
|
212
|
+
border-radius: 999px;
|
|
213
|
+
padding: 4px 10px;
|
|
214
|
+
color: var(--muted);
|
|
215
|
+
font-size: 0.78rem;
|
|
216
|
+
background: rgba(255,255,255,0.55);
|
|
217
|
+
}
|
|
218
|
+
.compact-select {
|
|
219
|
+
min-width: 18rem;
|
|
220
|
+
max-width: 100%;
|
|
221
|
+
}
|
|
222
|
+
.wide-input {
|
|
223
|
+
min-width: min(34rem, 100%);
|
|
224
|
+
width: min(34rem, 100%);
|
|
225
|
+
max-width: 100%;
|
|
226
|
+
}
|
|
227
|
+
.mono { font-family: var(--mono); }
|
|
228
|
+
.section-head {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: space-between;
|
|
232
|
+
gap: 8px;
|
|
233
|
+
margin-bottom: 8px;
|
|
234
|
+
flex-wrap: wrap;
|
|
235
|
+
}
|
|
236
|
+
.service-groups {
|
|
237
|
+
display: grid;
|
|
238
|
+
gap: 12px;
|
|
239
|
+
}
|
|
240
|
+
.service-group {
|
|
241
|
+
border: 1px solid rgba(217, 204, 183, 0.8);
|
|
242
|
+
border-radius: 12px;
|
|
243
|
+
background: rgba(255, 253, 250, 0.72);
|
|
244
|
+
padding: 10px;
|
|
245
|
+
}
|
|
246
|
+
.service-group h3 {
|
|
247
|
+
font-size: 0.96rem;
|
|
248
|
+
margin-bottom: 6px;
|
|
249
|
+
}
|
|
250
|
+
.service-table {
|
|
251
|
+
width: auto;
|
|
252
|
+
min-width: 100%;
|
|
253
|
+
margin-top: 0;
|
|
254
|
+
font-size: 0.73rem;
|
|
255
|
+
}
|
|
256
|
+
.service-table th {
|
|
257
|
+
color: var(--ink);
|
|
258
|
+
font-weight: 700;
|
|
259
|
+
background: rgba(242, 223, 202, 0.62);
|
|
260
|
+
border-bottom: 2px solid #bda98c;
|
|
261
|
+
}
|
|
262
|
+
.service-table th,
|
|
263
|
+
.service-table td {
|
|
264
|
+
padding: 4px 5px;
|
|
265
|
+
vertical-align: top;
|
|
266
|
+
}
|
|
267
|
+
.service-num {
|
|
268
|
+
text-align: right;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
}
|
|
271
|
+
.service-list {
|
|
272
|
+
display: flex;
|
|
273
|
+
flex-wrap: wrap;
|
|
274
|
+
gap: 4px;
|
|
275
|
+
max-width: 54ch;
|
|
276
|
+
}
|
|
277
|
+
.call-chip {
|
|
278
|
+
display: inline-flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 4px;
|
|
281
|
+
border-radius: 999px;
|
|
282
|
+
padding: 2px 6px;
|
|
283
|
+
border: 1px solid rgba(217, 204, 183, 0.8);
|
|
284
|
+
background: rgba(255,255,255,0.86);
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
}
|
|
287
|
+
.call-chip.ok {
|
|
288
|
+
border-color: rgba(15, 118, 110, 0.35);
|
|
289
|
+
background: rgba(15, 118, 110, 0.08);
|
|
290
|
+
color: #0f5e58;
|
|
291
|
+
}
|
|
292
|
+
.call-chip.fail {
|
|
293
|
+
border-color: rgba(165, 75, 26, 0.35);
|
|
294
|
+
background: rgba(165, 75, 26, 0.08);
|
|
295
|
+
color: #8b3f16;
|
|
296
|
+
}
|
|
297
|
+
.service-empty {
|
|
298
|
+
color: var(--muted);
|
|
299
|
+
font-size: 0.8rem;
|
|
300
|
+
}
|
|
301
|
+
.loading-text {
|
|
302
|
+
color: var(--muted);
|
|
303
|
+
font-style: italic;
|
|
304
|
+
}
|
|
305
|
+
.history-nav {
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-wrap: wrap;
|
|
308
|
+
gap: 6px;
|
|
309
|
+
align-items: center;
|
|
310
|
+
margin-bottom: 8px;
|
|
311
|
+
}
|
|
312
|
+
.history-buttons {
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-wrap: wrap;
|
|
315
|
+
gap: 4px;
|
|
316
|
+
align-items: center;
|
|
317
|
+
}
|
|
318
|
+
.history-buttons button {
|
|
319
|
+
min-height: 28px;
|
|
320
|
+
padding: 4px 7px;
|
|
321
|
+
font-size: 0.74rem;
|
|
322
|
+
}
|
|
323
|
+
.history-buttons button.active {
|
|
324
|
+
background: linear-gradient(180deg, #c5622b, var(--accent));
|
|
325
|
+
color: #fff8f2;
|
|
326
|
+
border-color: #8b3f16;
|
|
327
|
+
}
|
|
328
|
+
</style>
|
|
329
|
+
</head>
|
|
330
|
+
<body>
|
|
331
|
+
<main>
|
|
332
|
+
<h1>Monitor Admin</h1>
|
|
333
|
+
<p class="subtle">Authenticated operator console for monitor status, event review, req investigation, and targeted task execution.</p>
|
|
334
|
+
<p id="authNotice" class="notice"></p>
|
|
335
|
+
<div class="stack">
|
|
336
|
+
<details class="panel" open>
|
|
337
|
+
<summary>Stats</summary>
|
|
338
|
+
<div class="panel-body">
|
|
339
|
+
<div class="toolbar">
|
|
340
|
+
<button id="refreshStats" class="primary">Refresh Stats</button>
|
|
341
|
+
<span id="statsWhen" class="pill"></span>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="stats-wrap">
|
|
344
|
+
<table id="statsTable" class="stats-table">
|
|
345
|
+
<thead>
|
|
346
|
+
<tr>
|
|
347
|
+
<th>Metric</th>
|
|
348
|
+
<th>Day</th>
|
|
349
|
+
<th>Week</th>
|
|
350
|
+
<th>Month</th>
|
|
351
|
+
<th>Total</th>
|
|
352
|
+
</tr>
|
|
353
|
+
</thead>
|
|
354
|
+
<tbody></tbody>
|
|
355
|
+
</table>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</details>
|
|
359
|
+
<details class="panel" open>
|
|
360
|
+
<summary>Formatted Admin Log</summary>
|
|
361
|
+
<div class="panel-body">
|
|
362
|
+
<div class="section-head">
|
|
363
|
+
<div class="subtle">Provider-by-provider service history for the aggregated monitor and storage service layers.</div>
|
|
364
|
+
<button id="openAdminLogJson">Open JSON</button>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="history-nav">
|
|
367
|
+
<button id="callHistoryNextDay">Next Day</button>
|
|
368
|
+
<button id="callHistoryPrevDay">Previous Day</button>
|
|
369
|
+
<span id="callHistorySummary" class="pill"></span>
|
|
370
|
+
</div>
|
|
371
|
+
<div id="callHistoryButtons" class="history-buttons"></div>
|
|
372
|
+
<div id="adminLogTables" class="service-groups"></div>
|
|
373
|
+
</div>
|
|
374
|
+
</details>
|
|
375
|
+
<details class="panel" open>
|
|
376
|
+
<summary>Tasks</summary>
|
|
377
|
+
<div class="panel-body">
|
|
378
|
+
<div class="toolbar">
|
|
379
|
+
<button id="refreshTasks">Refresh Tasks</button>
|
|
380
|
+
</div>
|
|
381
|
+
<div id="tasks"></div>
|
|
382
|
+
<pre id="taskLog"></pre>
|
|
383
|
+
</div>
|
|
384
|
+
</details>
|
|
385
|
+
<details class="panel" open>
|
|
386
|
+
<summary>User UTXO Review</summary>
|
|
387
|
+
<div class="panel-body">
|
|
388
|
+
<div class="toolbar">
|
|
389
|
+
<label>Identity Key / User ID<input id="utxoIdentityKey" class="wide-input mono" type="text" placeholder="enter identityKey or numeric userId" /></label>
|
|
390
|
+
<label>Mode
|
|
391
|
+
<select id="utxoMode">
|
|
392
|
+
<option value="all">all invalid UTXOs</option>
|
|
393
|
+
<option value="change">change only</option>
|
|
394
|
+
</select>
|
|
395
|
+
</label>
|
|
396
|
+
<button id="runUtxoReview" class="primary">Run Review</button>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="toolbar">
|
|
399
|
+
<button id="loadUtxoUsers">Load Recently Active Users</button>
|
|
400
|
+
<label>Recently Active Users
|
|
401
|
+
<select id="utxoUserSelect" class="compact-select mono">
|
|
402
|
+
<option value="">select a recently active user</option>
|
|
403
|
+
</select>
|
|
404
|
+
</label>
|
|
405
|
+
<span id="utxoUserSummary" class="pill"></span>
|
|
406
|
+
</div>
|
|
407
|
+
<pre id="utxoReviewLog"></pre>
|
|
408
|
+
</div>
|
|
409
|
+
</details>
|
|
410
|
+
<details class="panel" open>
|
|
411
|
+
<summary>Monitor Events</summary>
|
|
412
|
+
<div class="panel-body">
|
|
413
|
+
<div class="toolbar">
|
|
414
|
+
<label>Limit<input id="eventsLimit" type="number" value="20" min="1" max="200" /></label>
|
|
415
|
+
<label>Event<input id="eventsName" type="text" placeholder="optional event name" /></label>
|
|
416
|
+
<button id="refreshEvents">Load Events</button>
|
|
417
|
+
</div>
|
|
418
|
+
<div id="events" class="event-list"></div>
|
|
419
|
+
</div>
|
|
420
|
+
</details>
|
|
421
|
+
<details class="panel" open>
|
|
422
|
+
<summary>Req Review</summary>
|
|
423
|
+
<div class="panel-body">
|
|
424
|
+
<div class="toolbar">
|
|
425
|
+
<label>Status<input id="reqStatus" type="text" placeholder="doubleSpend" /></label>
|
|
426
|
+
<label>Txid<input id="reqTxid" type="text" placeholder="exact txid" /></label>
|
|
427
|
+
<label>Min Tx Id<input id="reqMinTransactionId" type="number" value="150000" min="0" /></label>
|
|
428
|
+
<label>Limit<input id="reqLimit" type="number" value="50" min="1" max="200" /></label>
|
|
429
|
+
<button id="refreshReqs" class="primary">Load Review Rows</button>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="subtle" id="reqSummary"></div>
|
|
432
|
+
<div style="overflow:auto">
|
|
433
|
+
<table id="reqTable">
|
|
434
|
+
<thead>
|
|
435
|
+
<tr>
|
|
436
|
+
<th>Req</th>
|
|
437
|
+
<th>Tx</th>
|
|
438
|
+
<th>User</th>
|
|
439
|
+
<th>Req Status</th>
|
|
440
|
+
<th>Tx Status</th>
|
|
441
|
+
<th>Age</th>
|
|
442
|
+
<th>Txid</th>
|
|
443
|
+
<th>Actions</th>
|
|
444
|
+
</tr>
|
|
445
|
+
</thead>
|
|
446
|
+
<tbody></tbody>
|
|
447
|
+
</table>
|
|
448
|
+
</div>
|
|
449
|
+
<pre id="reqDetail"></pre>
|
|
450
|
+
</div>
|
|
451
|
+
</details>
|
|
452
|
+
</div>
|
|
453
|
+
</main>
|
|
454
|
+
<script src="/admin/assets/bsv-sdk.js"></script>
|
|
455
|
+
<script>
|
|
456
|
+
const byId = id => document.getElementById(id)
|
|
457
|
+
let authFetch
|
|
458
|
+
let identityKey = ''
|
|
459
|
+
let callHistoryState = { offset: 0, limit: 120, selected: null, total: 0, events: [], selectedByService: {} }
|
|
460
|
+
const serviceOrder = [
|
|
461
|
+
'getMerklePath',
|
|
462
|
+
'getRawTx',
|
|
463
|
+
'postBeef',
|
|
464
|
+
'getUtxoStatus',
|
|
465
|
+
'getStatusForTxids',
|
|
466
|
+
'getScriptHashHistory',
|
|
467
|
+
'updateFiatExchangeRates'
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
async function ensureAuthFetch() {
|
|
471
|
+
if (authFetch) return authFetch
|
|
472
|
+
const sdk = window.bsv
|
|
473
|
+
if (!sdk || !sdk.AuthFetch || !sdk.WalletClient) {
|
|
474
|
+
throw new Error('BSV SDK bundle failed to load.')
|
|
475
|
+
}
|
|
476
|
+
const wallet = new sdk.WalletClient('auto', window.location.host)
|
|
477
|
+
identityKey = (await wallet.getPublicKey({ identityKey: true })).publicKey
|
|
478
|
+
authFetch = new sdk.AuthFetch(wallet)
|
|
479
|
+
return authFetch
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function api(path, options) {
|
|
483
|
+
const client = await ensureAuthFetch()
|
|
484
|
+
const response = await client.fetch(window.location.origin + path, options)
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
const text = await response.text()
|
|
487
|
+
throw new Error(text || response.statusText)
|
|
488
|
+
}
|
|
489
|
+
return response.json()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function setNotice(text) {
|
|
493
|
+
byId('authNotice').textContent = text || ''
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function pretty(value) {
|
|
497
|
+
return JSON.stringify(value, null, 2)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function setButtonPending(id, pending, pendingText) {
|
|
501
|
+
const button = byId(id)
|
|
502
|
+
if (!button) return
|
|
503
|
+
if (pending) {
|
|
504
|
+
if (!button.dataset.originalText) button.dataset.originalText = button.textContent
|
|
505
|
+
button.textContent = pendingText
|
|
506
|
+
button.disabled = true
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
button.textContent = button.dataset.originalText || button.textContent
|
|
510
|
+
button.disabled = false
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function statText(value) {
|
|
514
|
+
if (value === null || value === undefined || value === '') return '-'
|
|
515
|
+
return String(value)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function escapeHtml(value) {
|
|
519
|
+
return String(value ?? '')
|
|
520
|
+
.replaceAll('&', '&')
|
|
521
|
+
.replaceAll('<', '<')
|
|
522
|
+
.replaceAll('>', '>')
|
|
523
|
+
.replaceAll('"', '"')
|
|
524
|
+
.replaceAll("'", ''')
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function formatWhen(value) {
|
|
528
|
+
if (!value) return ''
|
|
529
|
+
const date = new Date(value)
|
|
530
|
+
if (Number.isNaN(date.getTime())) return String(value)
|
|
531
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function toTimeValue(value) {
|
|
535
|
+
if (!value) return undefined
|
|
536
|
+
const date = new Date(value)
|
|
537
|
+
const time = date.getTime()
|
|
538
|
+
return Number.isNaN(time) ? undefined : time
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function formatCallChip(call) {
|
|
542
|
+
const kind = call.success ? 'ok' : 'fail'
|
|
543
|
+
const status = call.success ? 'S' : 'F'
|
|
544
|
+
const detail = call.result || call.error?.code || call.error?.message || ''
|
|
545
|
+
const titleParts = [call.when, detail].filter(Boolean)
|
|
546
|
+
return (
|
|
547
|
+
'<span class="call-chip ' + kind + '" title="' + escapeHtml(titleParts.join(' | ')) + '">' +
|
|
548
|
+
'<strong>' + status + '</strong>' +
|
|
549
|
+
'<span>' + escapeHtml(call.msecs + 'ms') + '</span>' +
|
|
550
|
+
(detail ? '<span>' + escapeHtml(detail) + '</span>' : '') +
|
|
551
|
+
'</span>'
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function intervalCountsForService(serviceHistory) {
|
|
556
|
+
const providers = Object.values(serviceHistory?.historyByProvider || {})
|
|
557
|
+
return providers.reduce(
|
|
558
|
+
(totals, provider) => {
|
|
559
|
+
const interval = provider.resetCounts && provider.resetCounts.length ? provider.resetCounts[0] : null
|
|
560
|
+
totals.success += interval?.success ?? 0
|
|
561
|
+
totals.failure += interval?.failure ?? 0
|
|
562
|
+
totals.error += interval?.error ?? 0
|
|
563
|
+
return totals
|
|
564
|
+
},
|
|
565
|
+
{ success: 0, failure: 0, error: 0 }
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function totalCountsForService(serviceHistory) {
|
|
570
|
+
const providers = Object.values(serviceHistory?.historyByProvider || {})
|
|
571
|
+
return providers.reduce(
|
|
572
|
+
(totals, provider) => {
|
|
573
|
+
totals.success += provider.totalCounts?.success ?? 0
|
|
574
|
+
totals.failure += provider.totalCounts?.failure ?? 0
|
|
575
|
+
totals.error += provider.totalCounts?.error ?? 0
|
|
576
|
+
return totals
|
|
577
|
+
},
|
|
578
|
+
{ success: 0, failure: 0, error: 0 }
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function visibleEventsForService(serviceName) {
|
|
583
|
+
return (callHistoryState.events || []).filter(event => {
|
|
584
|
+
const counts = intervalCountsForService(event.detailsJson?.[serviceName])
|
|
585
|
+
return counts.success + counts.failure + counts.error > 0
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function renderServiceEventButtons(serviceName) {
|
|
590
|
+
const events = visibleEventsForService(serviceName)
|
|
591
|
+
if (!events.length) {
|
|
592
|
+
return '<div class="service-empty">No interval activity for this service in the current day window.</div>'
|
|
593
|
+
}
|
|
594
|
+
const selectedId = callHistoryState.selectedByService?.[serviceName]
|
|
595
|
+
return (
|
|
596
|
+
'<div class="history-buttons">' +
|
|
597
|
+
events
|
|
598
|
+
.map(event => {
|
|
599
|
+
const active = event.id === selectedId ? ' active' : ''
|
|
600
|
+
return (
|
|
601
|
+
'<button class="service-event-button' + active + '" data-service-name="' + escapeHtml(serviceName) + '" data-event-id="' + escapeHtml(event.id) + '">' +
|
|
602
|
+
'#' + escapeHtml(event.id) + ' ' + escapeHtml(formatWhen(event.created_at)) +
|
|
603
|
+
'</button>'
|
|
604
|
+
)
|
|
605
|
+
})
|
|
606
|
+
.join('') +
|
|
607
|
+
'</div>'
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function renderServiceGroup(serviceName) {
|
|
612
|
+
const selectedId = callHistoryState.selectedByService?.[serviceName]
|
|
613
|
+
const selectedEvent = (callHistoryState.events || []).find(event => event.id === selectedId) || null
|
|
614
|
+
const serviceHistory = selectedEvent?.detailsJson?.[serviceName]
|
|
615
|
+
const intervalTotals = intervalCountsForService(serviceHistory)
|
|
616
|
+
const totalCounts = totalCountsForService(serviceHistory)
|
|
617
|
+
const providers = Object.values(serviceHistory?.historyByProvider || {})
|
|
618
|
+
|
|
619
|
+
const rows = providers.length
|
|
620
|
+
? providers
|
|
621
|
+
.map(provider => {
|
|
622
|
+
const interval = provider.resetCounts && provider.resetCounts.length ? provider.resetCounts[0] : null
|
|
623
|
+
const since = toTimeValue(interval?.since)
|
|
624
|
+
const until = toTimeValue(interval?.until)
|
|
625
|
+
const recent = (provider.calls || [])
|
|
626
|
+
.filter(call => {
|
|
627
|
+
const when = toTimeValue(call.when)
|
|
628
|
+
if (when === undefined) return false
|
|
629
|
+
if (since !== undefined && when < since) return false
|
|
630
|
+
if (until !== undefined && when > until) return false
|
|
631
|
+
return true
|
|
632
|
+
})
|
|
633
|
+
.slice(0, 5)
|
|
634
|
+
return (
|
|
635
|
+
'<tr>' +
|
|
636
|
+
'<td>' + escapeHtml(provider.providerName) + '</td>' +
|
|
637
|
+
'<td class="service-num">' + escapeHtml((interval?.success ?? 0) + '/' + (interval?.failure ?? 0) + '/' + (interval?.error ?? 0)) + '</td>' +
|
|
638
|
+
'<td class="service-num">' + escapeHtml((provider.totalCounts?.success ?? 0) + '/' + (provider.totalCounts?.failure ?? 0) + '/' + (provider.totalCounts?.error ?? 0)) + '</td>' +
|
|
639
|
+
'<td>' + escapeHtml(formatWhen(interval?.since)) + '</td>' +
|
|
640
|
+
'<td><div class="service-list">' + (recent.length ? recent.map(formatCallChip).join('') : '<span class="service-empty">No recent calls</span>') + '</div></td>' +
|
|
641
|
+
'</tr>'
|
|
642
|
+
)
|
|
643
|
+
})
|
|
644
|
+
.join('')
|
|
645
|
+
: '<tr><td colspan="5" class="service-empty">No provider data for selected event.</td></tr>'
|
|
646
|
+
|
|
647
|
+
const selectedSummary = selectedEvent
|
|
648
|
+
? '<span class="pill">event #' + escapeHtml(selectedEvent.id) + ' interval ' + escapeHtml(intervalTotals.success + '/' + intervalTotals.failure + '/' + intervalTotals.error) + '</span>'
|
|
649
|
+
: '<span class="pill">no event selected</span>'
|
|
650
|
+
|
|
651
|
+
return (
|
|
652
|
+
'<div class="service-group">' +
|
|
653
|
+
'<div class="section-head"><h3>' + escapeHtml(serviceName) + '</h3>' + selectedSummary + '</div>' +
|
|
654
|
+
'<div class="history-nav"><span class="pill">totals ' + escapeHtml(totalCounts.success + '/' + totalCounts.failure + '/' + totalCounts.error) + '</span></div>' +
|
|
655
|
+
renderServiceEventButtons(serviceName) +
|
|
656
|
+
'<table class="service-table">' +
|
|
657
|
+
'<thead><tr><th>Provider</th><th class="service-num">Int S/F/E</th><th class="service-num">Tot S/F/E</th><th>Since</th><th>Recent</th></tr></thead>' +
|
|
658
|
+
'<tbody>' + rows + '</tbody>' +
|
|
659
|
+
'</table>' +
|
|
660
|
+
'</div>'
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function renderAdminLogTables() {
|
|
665
|
+
const container = byId('adminLogTables')
|
|
666
|
+
container.innerHTML = serviceOrder.map(renderServiceGroup).join('')
|
|
667
|
+
container.querySelectorAll('.service-event-button').forEach(button => {
|
|
668
|
+
button.onclick = () => {
|
|
669
|
+
const serviceName = button.getAttribute('data-service-name')
|
|
670
|
+
const eventId = Number(button.getAttribute('data-event-id'))
|
|
671
|
+
if (!serviceName || !eventId) return
|
|
672
|
+
callHistoryState.selectedByService[serviceName] = eventId
|
|
673
|
+
renderAdminLogTables()
|
|
674
|
+
}
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function openAdminLogJsonPopup() {
|
|
679
|
+
const selected = serviceOrder.reduce((acc, serviceName) => {
|
|
680
|
+
const eventId = callHistoryState.selectedByService?.[serviceName]
|
|
681
|
+
const event = (callHistoryState.events || []).find(item => item.id === eventId) || null
|
|
682
|
+
acc[serviceName] = event
|
|
683
|
+
return acc
|
|
684
|
+
}, {})
|
|
685
|
+
const popup = window.open('', 'monitor-admin-log-json', 'popup=yes,width=1100,height=760')
|
|
686
|
+
if (!popup) {
|
|
687
|
+
setNotice('Popup was blocked by the browser.')
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
popup.document.title = 'Monitor Admin Log JSON'
|
|
691
|
+
popup.document.body.innerHTML =
|
|
692
|
+
'<pre style="margin:0;padding:16px;font:12px/1.4 SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;">' +
|
|
693
|
+
escapeHtml(JSON.stringify(selected, null, 2)) +
|
|
694
|
+
'</pre>'
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function loadCallHistory(offset = 0) {
|
|
698
|
+
const query = new URLSearchParams()
|
|
699
|
+
query.set('offset', String(Math.max(0, offset)))
|
|
700
|
+
query.set('limit', String(callHistoryState.limit || 10))
|
|
701
|
+
const result = await api('/admin/api/call-history?' + query.toString())
|
|
702
|
+
const nextSelectedByService = { ...(callHistoryState.selectedByService || {}) }
|
|
703
|
+
serviceOrder.forEach(serviceName => {
|
|
704
|
+
const visible = (result.events || []).filter(event => {
|
|
705
|
+
const counts = intervalCountsForService(event.detailsJson?.[serviceName])
|
|
706
|
+
return counts.success + counts.failure + counts.error > 0
|
|
707
|
+
})
|
|
708
|
+
if (!visible.length) {
|
|
709
|
+
delete nextSelectedByService[serviceName]
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
const existing = nextSelectedByService[serviceName]
|
|
713
|
+
if (!visible.some(event => event.id === existing)) {
|
|
714
|
+
nextSelectedByService[serviceName] = visible[0].id
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
callHistoryState = {
|
|
718
|
+
...callHistoryState,
|
|
719
|
+
offset: result.offset || 0,
|
|
720
|
+
limit: result.limit || 10,
|
|
721
|
+
selected: result.selected || null,
|
|
722
|
+
total: result.total || 0,
|
|
723
|
+
events: result.events || [],
|
|
724
|
+
selectedByService: nextSelectedByService,
|
|
725
|
+
hasNewer: !!result.hasNewer,
|
|
726
|
+
hasOlder: !!result.hasOlder
|
|
727
|
+
}
|
|
728
|
+
byId('callHistoryButtons').innerHTML = ''
|
|
729
|
+
byId('callHistorySummary').textContent = result.events && result.events.length
|
|
730
|
+
? 'day window ' + (result.offset + 1) + '-' + (result.offset + result.events.length) + ' of ' + result.total
|
|
731
|
+
: 'no MonitorCallHistory events'
|
|
732
|
+
byId('callHistoryNextDay').disabled = !result.hasNewer
|
|
733
|
+
byId('callHistoryPrevDay').disabled = !result.hasOlder
|
|
734
|
+
renderAdminLogTables()
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function renderStatsTable(stats) {
|
|
738
|
+
const rows = [
|
|
739
|
+
{ label: 'users', values: [stats.usersDay, stats.usersWeek, stats.usersMonth, stats.usersTotal] },
|
|
740
|
+
[
|
|
741
|
+
'change BSV',
|
|
742
|
+
stats.satoshisDefaultDayFormatted ?? stats.satoshisDefaultDay,
|
|
743
|
+
stats.satoshisDefaultWeekFormatted ?? stats.satoshisDefaultWeek,
|
|
744
|
+
stats.satoshisDefaultMonthFormatted ?? stats.satoshisDefaultMonth,
|
|
745
|
+
stats.satoshisDefaultTotalFormatted ?? stats.satoshisDefaultTotal
|
|
746
|
+
],
|
|
747
|
+
[
|
|
748
|
+
'other BSV',
|
|
749
|
+
stats.satoshisOtherDayFormatted ?? stats.satoshisOtherDay,
|
|
750
|
+
stats.satoshisOtherWeekFormatted ?? stats.satoshisOtherWeek,
|
|
751
|
+
stats.satoshisOtherMonthFormatted ?? stats.satoshisOtherMonth,
|
|
752
|
+
stats.satoshisOtherTotalFormatted ?? stats.satoshisOtherTotal
|
|
753
|
+
],
|
|
754
|
+
['labels', stats.labelsDay, stats.labelsWeek, stats.labelsMonth, stats.labelsTotal],
|
|
755
|
+
['tags', stats.tagsDay, stats.tagsWeek, stats.tagsMonth, stats.tagsTotal],
|
|
756
|
+
['baskets', stats.basketsDay, stats.basketsWeek, stats.basketsMonth, stats.basketsTotal],
|
|
757
|
+
['transactions', stats.transactionsDay, stats.transactionsWeek, stats.transactionsMonth, stats.transactionsTotal],
|
|
758
|
+
['completed', stats.txCompletedDay, stats.txCompletedWeek, stats.txCompletedMonth, stats.txCompletedTotal],
|
|
759
|
+
['failed', stats.txFailedDay, stats.txFailedWeek, stats.txFailedMonth, stats.txFailedTotal],
|
|
760
|
+
['abandoned', stats.txAbandonedDay, stats.txAbandonedWeek, stats.txAbandonedMonth, stats.txAbandonedTotal],
|
|
761
|
+
['nosend', stats.txNosendDay, stats.txNosendWeek, stats.txNosendMonth, stats.txNosendTotal],
|
|
762
|
+
['unproven', stats.txUnprovenDay, stats.txUnprovenWeek, stats.txUnprovenMonth, stats.txUnprovenTotal],
|
|
763
|
+
['sending', stats.txSendingDay, stats.txSendingWeek, stats.txSendingMonth, stats.txSendingTotal],
|
|
764
|
+
['unprocessed', stats.txUnprocessedDay, stats.txUnprocessedWeek, stats.txUnprocessedMonth, stats.txUnprocessedTotal],
|
|
765
|
+
['unsigned', stats.txUnsignedDay, stats.txUnsignedWeek, stats.txUnsignedMonth, stats.txUnsignedTotal],
|
|
766
|
+
['nonfinal', stats.txNonfinalDay, stats.txNonfinalWeek, stats.txNonfinalMonth, stats.txNonfinalTotal],
|
|
767
|
+
['unfail', stats.txUnfailDay, stats.txUnfailWeek, stats.txUnfailMonth, stats.txUnfailTotal]
|
|
768
|
+
].map(row =>
|
|
769
|
+
Array.isArray(row)
|
|
770
|
+
? {
|
|
771
|
+
label: row[0],
|
|
772
|
+
values: [row[1], row[2], row[3], row[4]],
|
|
773
|
+
group: row[0] === 'transactions',
|
|
774
|
+
child:
|
|
775
|
+
row[0] === 'completed' ||
|
|
776
|
+
row[0] === 'failed' ||
|
|
777
|
+
row[0] === 'abandoned' ||
|
|
778
|
+
row[0] === 'nosend' ||
|
|
779
|
+
row[0] === 'unproven' ||
|
|
780
|
+
row[0] === 'sending' ||
|
|
781
|
+
row[0] === 'unprocessed' ||
|
|
782
|
+
row[0] === 'unsigned' ||
|
|
783
|
+
row[0] === 'nonfinal' ||
|
|
784
|
+
row[0] === 'unfail'
|
|
785
|
+
}
|
|
786
|
+
: row
|
|
787
|
+
)
|
|
788
|
+
const headers = ['Metric', 'Day', 'Week', 'Month', 'Total']
|
|
789
|
+
const colWidths = headers.map((header, index) =>
|
|
790
|
+
rows.reduce((max, row) => Math.max(max, statText(index === 0 ? row.label : row.values[index - 1]).length), header.length)
|
|
791
|
+
)
|
|
792
|
+
const table = byId('statsTable')
|
|
793
|
+
const thead = table.querySelector('thead')
|
|
794
|
+
const body = table.querySelector('tbody')
|
|
795
|
+
table.querySelector('colgroup')?.remove()
|
|
796
|
+
const colgroup = document.createElement('colgroup')
|
|
797
|
+
colWidths.forEach((width, index) => {
|
|
798
|
+
const col = document.createElement('col')
|
|
799
|
+
const extra = index === 0 ? 2 : 1
|
|
800
|
+
col.style.width = (width + extra) + 'ch'
|
|
801
|
+
colgroup.appendChild(col)
|
|
802
|
+
})
|
|
803
|
+
table.insertBefore(colgroup, thead)
|
|
804
|
+
thead.innerHTML =
|
|
805
|
+
'<tr>' +
|
|
806
|
+
'<th class="stats-head-label">Metric</th>' +
|
|
807
|
+
'<th class="stats-head-num">Day</th>' +
|
|
808
|
+
'<th class="stats-head-num">Week</th>' +
|
|
809
|
+
'<th class="stats-head-num">Month</th>' +
|
|
810
|
+
'<th class="stats-head-num">Total</th>' +
|
|
811
|
+
'</tr>'
|
|
812
|
+
body.innerHTML = ''
|
|
813
|
+
rows.forEach(row => {
|
|
814
|
+
const tr = document.createElement('tr')
|
|
815
|
+
const label = statText(row.label)
|
|
816
|
+
const classes = []
|
|
817
|
+
if (row.group) classes.push('stats-group')
|
|
818
|
+
if (row.child) classes.push('stats-subrow', 'stats-child')
|
|
819
|
+
tr.className = classes.join(' ')
|
|
820
|
+
tr.innerHTML =
|
|
821
|
+
'<td class="stats-label">' + label + '</td>' +
|
|
822
|
+
'<td class="stats-num">' + statText(row.values[0]) + '</td>' +
|
|
823
|
+
'<td class="stats-num">' + statText(row.values[1]) + '</td>' +
|
|
824
|
+
'<td class="stats-num">' + statText(row.values[2]) + '</td>' +
|
|
825
|
+
'<td class="stats-num">' + statText(row.values[3]) + '</td>'
|
|
826
|
+
body.appendChild(tr)
|
|
827
|
+
})
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function renderUtxoUsers(users, total) {
|
|
831
|
+
const select = byId('utxoUserSelect')
|
|
832
|
+
select.innerHTML = '<option value="">select a recent user</option>'
|
|
833
|
+
users.forEach(user => {
|
|
834
|
+
const option = document.createElement('option')
|
|
835
|
+
option.value = user.identityKey
|
|
836
|
+
option.textContent = (user.userId ?? '?') + ' | ' + user.identityKey
|
|
837
|
+
select.appendChild(option)
|
|
838
|
+
})
|
|
839
|
+
byId('utxoUserSummary').textContent = total ? total + ' user' + (total === 1 ? '' : 's') : 'no users loaded'
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function loadStats() {
|
|
843
|
+
setButtonPending('refreshStats', true, 'Loading...')
|
|
844
|
+
byId('statsTable').querySelector('tbody').innerHTML =
|
|
845
|
+
'<tr><td colspan="5" class="loading-text">Loading stats...</td></tr>'
|
|
846
|
+
try {
|
|
847
|
+
const result = await api('/admin/api/stats')
|
|
848
|
+
const stats = result.stats || {}
|
|
849
|
+
renderStatsTable(stats)
|
|
850
|
+
byId('statsWhen').textContent = (stats.when || '').toString()
|
|
851
|
+
setNotice('Authenticated as ' + result.requestedBy)
|
|
852
|
+
} finally {
|
|
853
|
+
setButtonPending('refreshStats', false)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function loadTasks() {
|
|
858
|
+
const result = await api('/admin/api/tasks')
|
|
859
|
+
const container = byId('tasks')
|
|
860
|
+
container.innerHTML = ''
|
|
861
|
+
result.tasks.forEach(task => {
|
|
862
|
+
const row = document.createElement('div')
|
|
863
|
+
row.className = 'toolbar'
|
|
864
|
+
const button = document.createElement('button')
|
|
865
|
+
button.textContent = 'Run'
|
|
866
|
+
button.onclick = async () => {
|
|
867
|
+
const run = await api('/admin/api/tasks/' + encodeURIComponent(task.name) + '/run', {
|
|
868
|
+
method: 'POST',
|
|
869
|
+
headers: { 'Content-Type': 'application/json' },
|
|
870
|
+
body: JSON.stringify({})
|
|
871
|
+
})
|
|
872
|
+
byId('taskLog').textContent = pretty(run)
|
|
873
|
+
}
|
|
874
|
+
row.innerHTML = '<div><strong>' + task.name + '</strong><div class="subtle">' + task.kind + '</div></div>'
|
|
875
|
+
row.appendChild(button)
|
|
876
|
+
container.appendChild(row)
|
|
877
|
+
})
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function loadUtxoUsers() {
|
|
881
|
+
setButtonPending('loadUtxoUsers', true, 'Loading...')
|
|
882
|
+
byId('utxoUserSummary').textContent = 'Loading...'
|
|
883
|
+
try {
|
|
884
|
+
const result = await api('/admin/api/users?limit=50')
|
|
885
|
+
renderUtxoUsers(result.users || [], result.total || 0)
|
|
886
|
+
} finally {
|
|
887
|
+
setButtonPending('loadUtxoUsers', false)
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function runUtxoReview() {
|
|
892
|
+
const identityKeyValue = byId('utxoIdentityKey').value.trim()
|
|
893
|
+
const identityKey = identityKeyValue || byId('utxoUserSelect').value
|
|
894
|
+
if (!identityKey) {
|
|
895
|
+
throw new Error('Enter or select an identityKey first.')
|
|
896
|
+
}
|
|
897
|
+
byId('utxoIdentityKey').value = identityKey
|
|
898
|
+
const mode = byId('utxoMode').value === 'change' ? 'change' : 'all'
|
|
899
|
+
setButtonPending('runUtxoReview', true, 'Running...')
|
|
900
|
+
byId('utxoReviewLog').textContent = 'Running review...'
|
|
901
|
+
try {
|
|
902
|
+
const result = await api('/admin/api/review-utxos', {
|
|
903
|
+
method: 'POST',
|
|
904
|
+
headers: { 'Content-Type': 'application/json' },
|
|
905
|
+
body: JSON.stringify({ identityKey, mode })
|
|
906
|
+
})
|
|
907
|
+
byId('utxoReviewLog').textContent = result.log || ''
|
|
908
|
+
setNotice('Authenticated as ' + result.requestedBy)
|
|
909
|
+
} finally {
|
|
910
|
+
setButtonPending('runUtxoReview', false)
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function loadEvents() {
|
|
915
|
+
const query = new URLSearchParams()
|
|
916
|
+
query.set('limit', byId('eventsLimit').value || '20')
|
|
917
|
+
const name = byId('eventsName').value.trim()
|
|
918
|
+
if (name) query.set('event', name)
|
|
919
|
+
const result = await api('/admin/api/events?' + query.toString())
|
|
920
|
+
const container = byId('events')
|
|
921
|
+
container.innerHTML = ''
|
|
922
|
+
result.events.forEach(event => {
|
|
923
|
+
const el = document.createElement('div')
|
|
924
|
+
el.className = 'event'
|
|
925
|
+
const details = event.detailsPretty || event.details || ''
|
|
926
|
+
el.innerHTML = '<strong>' + event.event + '</strong><div class="subtle">' + event.created_at + '</div><pre>' + details + '</pre>'
|
|
927
|
+
container.appendChild(el)
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function loadReqs() {
|
|
932
|
+
const query = new URLSearchParams()
|
|
933
|
+
const status = byId('reqStatus').value.trim()
|
|
934
|
+
const txid = byId('reqTxid').value.trim()
|
|
935
|
+
const minTxId = byId('reqMinTransactionId').value.trim()
|
|
936
|
+
const limit = byId('reqLimit').value.trim()
|
|
937
|
+
if (status) query.set('status', status)
|
|
938
|
+
if (txid) query.set('txid', txid)
|
|
939
|
+
if (minTxId) query.set('minTransactionId', minTxId)
|
|
940
|
+
if (limit) query.set('limit', limit)
|
|
941
|
+
const result = await api('/admin/api/proven-tx-reqs/review?' + query.toString())
|
|
942
|
+
byId('reqSummary').textContent = result.total + ' review rows'
|
|
943
|
+
const body = byId('reqTable').querySelector('tbody')
|
|
944
|
+
body.innerHTML = ''
|
|
945
|
+
result.rows.forEach(row => {
|
|
946
|
+
const tr = document.createElement('tr')
|
|
947
|
+
tr.innerHTML =
|
|
948
|
+
'<td>' + row.provenTxReqId + '</td>' +
|
|
949
|
+
'<td>' + (row.transactionId ?? '') + '</td>' +
|
|
950
|
+
'<td>' + (row.userId ?? '') + '</td>' +
|
|
951
|
+
'<td>' + row.reqStatus + '</td>' +
|
|
952
|
+
'<td>' + (row.txStatus ?? '') + '</td>' +
|
|
953
|
+
'<td>' + row.hoursOld + 'h</td>' +
|
|
954
|
+
'<td>' + row.txid + '</td>'
|
|
955
|
+
const actions = document.createElement('td')
|
|
956
|
+
actions.className = 'row-actions'
|
|
957
|
+
const inspect = document.createElement('button')
|
|
958
|
+
inspect.textContent = 'Inspect'
|
|
959
|
+
inspect.onclick = async () => {
|
|
960
|
+
const detail = await api('/admin/api/proven-tx-reqs/' + row.provenTxReqId)
|
|
961
|
+
byId('reqDetail').textContent = pretty(detail)
|
|
962
|
+
}
|
|
963
|
+
const decode = document.createElement('button')
|
|
964
|
+
decode.textContent = 'Decode'
|
|
965
|
+
decode.onclick = async () => {
|
|
966
|
+
const detail = await api('/admin/api/proven-tx-reqs/' + row.provenTxReqId + '/decode')
|
|
967
|
+
byId('reqDetail').textContent = pretty(detail)
|
|
968
|
+
}
|
|
969
|
+
const rebroadcast = document.createElement('button')
|
|
970
|
+
rebroadcast.textContent = 'Rebroadcast'
|
|
971
|
+
rebroadcast.onclick = async () => {
|
|
972
|
+
const provider = prompt('Provider? Use "taal" or "woc".', 'taal')
|
|
973
|
+
if (!provider) return
|
|
974
|
+
const result = await api('/admin/api/proven-tx-reqs/' + row.provenTxReqId + '/rebroadcast', {
|
|
975
|
+
method: 'POST',
|
|
976
|
+
headers: { 'Content-Type': 'application/json' },
|
|
977
|
+
body: JSON.stringify({ provider })
|
|
978
|
+
})
|
|
979
|
+
byId('reqDetail').textContent = pretty(result)
|
|
980
|
+
}
|
|
981
|
+
actions.appendChild(inspect)
|
|
982
|
+
actions.appendChild(decode)
|
|
983
|
+
actions.appendChild(rebroadcast)
|
|
984
|
+
tr.appendChild(actions)
|
|
985
|
+
body.appendChild(tr)
|
|
986
|
+
})
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function init() {
|
|
990
|
+
try {
|
|
991
|
+
await ensureAuthFetch()
|
|
992
|
+
setNotice('Authenticated as ' + identityKey)
|
|
993
|
+
await Promise.all([loadStats(), loadCallHistory(), loadTasks(), loadUtxoUsers(), loadEvents(), loadReqs()])
|
|
994
|
+
} catch (error) {
|
|
995
|
+
setNotice(error.message || String(error))
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
byId('refreshStats').onclick = () => loadStats().catch(error => setNotice(error.message || String(error)))
|
|
1000
|
+
byId('openAdminLogJson').onclick = () => openAdminLogJsonPopup()
|
|
1001
|
+
byId('callHistoryNextDay').onclick = () =>
|
|
1002
|
+
loadCallHistory(Math.max(0, callHistoryState.offset - callHistoryState.limit)).catch(error =>
|
|
1003
|
+
setNotice(error.message || String(error))
|
|
1004
|
+
)
|
|
1005
|
+
byId('callHistoryPrevDay').onclick = () =>
|
|
1006
|
+
loadCallHistory(callHistoryState.offset + callHistoryState.limit).catch(error =>
|
|
1007
|
+
setNotice(error.message || String(error))
|
|
1008
|
+
)
|
|
1009
|
+
byId('refreshTasks').onclick = () => loadTasks().catch(error => setNotice(error.message || String(error)))
|
|
1010
|
+
byId('loadUtxoUsers').onclick = () => loadUtxoUsers().catch(error => setNotice(error.message || String(error)))
|
|
1011
|
+
byId('utxoUserSelect').onchange = event => {
|
|
1012
|
+
const value = event.target && event.target.value ? event.target.value : ''
|
|
1013
|
+
if (value) byId('utxoIdentityKey').value = value
|
|
1014
|
+
}
|
|
1015
|
+
byId('runUtxoReview').onclick = () => runUtxoReview().catch(error => setNotice(error.message || String(error)))
|
|
1016
|
+
byId('refreshEvents').onclick = () => loadEvents().catch(error => setNotice(error.message || String(error)))
|
|
1017
|
+
byId('refreshReqs').onclick = () => loadReqs().catch(error => setNotice(error.message || String(error)))
|
|
1018
|
+
|
|
1019
|
+
init()
|
|
1020
|
+
</script>
|
|
1021
|
+
</body>
|
|
1022
|
+
</html>`;
|
|
1023
|
+
}
|
|
1024
|
+
//# sourceMappingURL=adminUi.js.map
|