@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/docs/client.md +2 -1
  3. package/docs/storage.md +90 -39
  4. package/docs/wallet.md +2 -1
  5. package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
  6. package/out/src/CWIStyleWalletManager.js +3 -2
  7. package/out/src/CWIStyleWalletManager.js.map +1 -1
  8. package/out/src/monitor/Monitor.d.ts.map +1 -1
  9. package/out/src/monitor/Monitor.js +5 -1
  10. package/out/src/monitor/Monitor.js.map +1 -1
  11. package/out/src/storage/StorageKnex.d.ts +1 -0
  12. package/out/src/storage/StorageKnex.d.ts.map +1 -1
  13. package/out/src/storage/StorageKnex.js +14 -7
  14. package/out/src/storage/StorageKnex.js.map +1 -1
  15. package/out/src/storage/StorageProvider.d.ts +2 -0
  16. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  17. package/out/src/storage/StorageProvider.js +23 -0
  18. package/out/src/storage/StorageProvider.js.map +1 -1
  19. package/out/src/storage/adminServer/adminServer.d.ts +26 -0
  20. package/out/src/storage/adminServer/adminServer.d.ts.map +1 -0
  21. package/out/src/storage/adminServer/adminServer.js +547 -0
  22. package/out/src/storage/adminServer/adminServer.js.map +1 -0
  23. package/out/src/storage/adminServer/adminUi.d.ts +2 -0
  24. package/out/src/storage/adminServer/adminUi.d.ts.map +1 -0
  25. package/out/src/storage/adminServer/adminUi.js +1024 -0
  26. package/out/src/storage/adminServer/adminUi.js.map +1 -0
  27. package/out/src/storage/adminServer/index.all.d.ts +3 -0
  28. package/out/src/storage/adminServer/index.all.d.ts.map +1 -0
  29. package/out/src/storage/adminServer/index.all.js +19 -0
  30. package/out/src/storage/adminServer/index.all.js.map +1 -0
  31. package/out/src/storage/index.all.d.ts +1 -0
  32. package/out/src/storage/index.all.d.ts.map +1 -1
  33. package/out/src/storage/index.all.js +1 -0
  34. package/out/src/storage/index.all.js.map +1 -1
  35. 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('&', '&amp;')
521
+ .replaceAll('<', '&lt;')
522
+ .replaceAll('>', '&gt;')
523
+ .replaceAll('"', '&quot;')
524
+ .replaceAll("'", '&#39;')
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