Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam
1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width"/> 6 <title>atproto PDS & account debugger</title> 7 <meta name="description" content="Quick diagnostics for PDS hosts, handles, relay connections, handles, DIDs, ..." /> 8 9 <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script> 10 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 11 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/> 12 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> 13 14 <script type="module"> 15 import { 16 Client, 17 ClientResponseError, 18 ok, 19 simpleFetchHandler, 20 } from 'https://esm.sh/@atcute/client@4.1.1'; 21 import { 22 DohJsonHandleResolver, 23 WellKnownHandleResolver, 24 } from 'https://esm.sh/@atcute/identity-resolver@1.2.1'; 25 26 window.SimpleQuery = service => { 27 const client = new Client({ handler: simpleFetchHandler({ service }) }); 28 return (...args) => ok(client.get(...args)); 29 }; 30 window.SimpleProc = service => { 31 const client = new Client({ handler: simpleFetchHandler({ service }) }); 32 return (...args) => ok(client.post(...args)); 33 }; 34 window.isXrpcErr = e => e instanceof ClientResponseError; 35 36 window.dnsResolver = new DohJsonHandleResolver({ 37 dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query', 38 }); 39 window.httpResolver = new WellKnownHandleResolver(); 40 41 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue'); 42 window.relays = [ 43 { 44 name: 'Bluesky', 45 icon: './icons/bsky-favicon.png', 46 hostname: 'bsky.network', 47 note: 'current', 48 missingApis: { 49 ['com.atproto.sync.getHostStatus']: 'missing API (old relay code)', 50 ['com.atproto.sync.getRepoStatus']: 'missing API (old relay code)', 51 }, 52 }, 53 { 54 name: 'Microcosm Montreal', 55 icon: './icons/microcosm-favicon.png', 56 hostname: 'relay.fire.hose.cam', 57 }, 58 { 59 name: 'Microcosm France', 60 icon: './icons/microcosm-favicon.png', 61 hostname: 'relay3.fr.hose.cam', 62 }, 63 { 64 name: 'Upcloud', 65 icon: 'https://upcloud.com/media/android-chrome-512x512-2-150x150.png', 66 hostname: 'relay.upcloud.world', 67 }, 68 { 69 name: 'Blacksky', 70 icon: 'https://blacksky.community/static/favicon-32x32.png', 71 hostname: 'atproto.africa', 72 missingApis: { 73 ['com.atproto.sync.getHostStatus']: 'API not yet deployed', 74 ['com.atproto.sync.getRepoStatus']: 'API not implemented', 75 }, 76 }, 77 { 78 name: 'Bluesky East', 79 icon: './icons/bsky-favicon.png', 80 note: 'future', 81 hostname: 'relay1.us-east.bsky.network', 82 }, 83 { 84 name: 'Bluesky West', 85 icon: './icons/bsky-favicon.png', 86 note: 'future', 87 hostname: 'relay1.us-west.bsky.network', 88 }, 89 ]; 90 </script> 91 92 <script> 93 document.addEventListener('alpine:init', () => { 94 Alpine.data('debug', () => ({ 95 // form input 96 identifier: '', 97 98 // state 99 identifierLoading: false, 100 identifierError: null, 101 102 // stuff to check 103 pds: null, 104 did: null, 105 handle: null, 106 107 async goto(identifier) { 108 this.identifier = identifier; 109 await this.diagnose(); 110 }, 111 112 async diagnose() { 113 this.identifierLoading = true; 114 this.identifierError = null; 115 this.pds = null; 116 this.did = null; 117 this.handle = null; 118 this.identifier = this.identifier.trim(); 119 if (this.identifier === '') { 120 // do nothing 121 } else if (this.identifier.startsWith('https://')) { 122 this.pds = this.identifier; 123 } else { 124 if (this.identifier.startsWith('at://')) { 125 this.identifier = this.identifier.slice('at://'.length); 126 } 127 if (this.identifier.startsWith('did:')) { 128 this.did = this.identifier; 129 let data; 130 try { 131 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 132 params: { identifier: this.identifier }, 133 }); 134 this.pds = data.pds; 135 this.handle = data.handle; 136 } catch (e) { 137 if (window.isXrpcErr(e)) { 138 this.identifierError = e.error; 139 if (e.message) this.description += ` ${e.description}`; 140 } else { 141 this.identifierError = 'Failed to resolve identifier, see console for error.'; 142 console.error(e); 143 } 144 } 145 } else { 146 this.handle = this.identifier; 147 let data; 148 try { 149 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 150 params: { identifier: this.identifier }, 151 }); 152 this.did = data.did; 153 this.pds = data.pds; 154 } catch (e) { 155 if (window.isXrpcErr(e)) { 156 this.identifierError = e.error; 157 if (e.message) this.description += ` ${e.description}`; 158 } else { 159 this.identifierError = 'Failed to resolve identifier, see console for error.'; 160 console.error(e); 161 } 162 } 163 } 164 } 165 this.identifierLoading = false; 166 }, 167 })); 168 169 Alpine.data('pdsCheck', pds => ({ 170 loadingDesc: false, 171 error: null, 172 description: null, 173 accounts: [], 174 accountsComplete: false, 175 version: null, 176 177 async init() { 178 await this.update(pds); 179 }, 180 181 async update(pds) { 182 this.loadingDesc = true; 183 this.error = null; 184 this.description = null; 185 this.accounts = []; 186 this.accountsComplete = false; 187 this.version = null; 188 189 if (!pds) { 190 this.loadingDesc = false; 191 return; 192 } 193 194 let query = window.SimpleQuery(pds); 195 try { 196 this.description = await query('com.atproto.server.describeServer'); 197 } catch (e) { 198 if (window.isXrpcErr(e)) { 199 this.error = e.error; 200 } else { 201 this.error = 'Failed to reach (see console)'; 202 console.error(e); 203 } 204 } 205 let health 206 try { 207 health = await query('_health'); 208 this.version = health.version; 209 } catch (e) { 210 if (window.isXrpcErr(e)) { 211 this.error = e.error; 212 } else { 213 this.error = 'Failed to reach (see console)'; 214 console.error(e); 215 } 216 } 217 let accountsRes; 218 try { 219 accountsRes = await query('com.atproto.sync.listRepos', { 220 params: { limit: 100 }, 221 }); 222 this.accounts = accountsRes.repos; 223 this.accountsComplete == !accountsRes.cursor; 224 } catch (e) { 225 if (window.isXrpcErr(e)) { 226 this.error = e.error; 227 } else { 228 this.error = 'Failed to reach (see console)'; 229 console.error(e); 230 } 231 } 232 this.loadingDesc = false; 233 }, 234 })); 235 236 Alpine.data('relayCheckHost', (pds, relay) => ({ 237 loading: false, 238 error: null, 239 status: null, 240 expectedErrorInfo: null, 241 reqCrawlStatus: null, 242 reqCrawlError: null, 243 244 async init() { 245 await this.check(pds, relay); 246 }, 247 248 async check(pds, relay) { 249 this.loading = true; 250 this.error = null; 251 this.status = null; 252 this.expectedError = false; 253 const query = window.SimpleQuery(`https://${relay.hostname}`); 254 const hostname = pds.split('://')[1]; 255 let data; 256 try { 257 data = await query('com.atproto.sync.getHostStatus', { 258 params: { hostname }, 259 }); 260 this.status = data.status; 261 } catch(e) { 262 if (relay.missingApis['com.atproto.sync.getHostStatus']) { 263 this.error = 'Can\'t check'; 264 this.expectedErrorInfo = relay.missingApis['com.atproto.sync.getHostStatus']; 265 } else if (window.isXrpcErr(e)) { 266 this.error = e.error; 267 } else { 268 this.error = 'Failed to check (see console)'; 269 console.error(e); 270 } 271 } 272 this.loading = false; 273 this.reqCrawlStatus = null; 274 this.reqCrawlError = null; 275 }, 276 277 async requestCrawl(pds, relay) { 278 this.reqCrawlStatus = "loading"; 279 const proc = window.SimpleProc(`https://${relay.hostname}`); 280 const hostname = pds.split('://')[1]; 281 let data; 282 try { 283 data = await proc('com.atproto.sync.requestCrawl', { 284 input: { hostname }, 285 }); 286 } catch (e) { 287 if (window.isXrpcErr(e)) { 288 this.reqCrawlError = e.error; 289 } else { 290 this.reqCrawlError = 'failed (see console)'; 291 console.error(e); 292 } 293 } 294 this.reqCrawlStatus = "done"; 295 }, 296 })); 297 298 Alpine.data('checkHandle', handle => ({ 299 loading: false, 300 dnsDid: null, 301 dnsErr: null, 302 httpDid: null, 303 httpErr: null, 304 305 async init() { 306 await this.updateHandle(handle); 307 }, 308 async updateHandle(handle) { 309 this.loading = true; 310 this.dnsDid = null; 311 this.dnsErr = null; 312 this.httpDid = null; 313 this.httpErr = null; 314 try { 315 this.dnsDid = await window.dnsResolver.resolve(handle); 316 } catch (e) { 317 this.dnsErr = e.name; 318 } 319 try { 320 this.httpDid = await window.httpResolver.resolve(handle); 321 } catch (e) { 322 this.httpErr = e.name; 323 } 324 this.loading = false; 325 }, 326 })); 327 328 Alpine.data('didToHandle', did => ({ 329 loading: false, 330 error: null, 331 handle: null, 332 async load() { 333 loading = true; 334 error = null; 335 handle = null; 336 let data; 337 try { 338 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 339 params: { identifier: did }, 340 }); 341 this.handle = data.handle; 342 } catch (e) { 343 if (window.isXrpcErr(e)) { 344 this.error = e.error; 345 } else { 346 this.error = 'failed (see console)'; 347 console.error(e); 348 } 349 } 350 loading = false; 351 } 352 })); 353 354 Alpine.data('didRepoState', (did, pds) => ({ 355 loading: false, 356 error: null, 357 state: null, 358 359 async init() { 360 await this.checkRepoState(did, pds); 361 }, 362 async checkRepoState(did, pds) { 363 this.loading = true; 364 this.error = null; 365 this.state = null; 366 367 if (!did || !pds) { 368 this.loading = false; 369 return; 370 } 371 const query = window.SimpleQuery(pds); 372 try { 373 this.state = await query('com.atproto.sync.getRepoStatus', { 374 params: { did }, 375 }); 376 } catch (e) { 377 if (window.isXrpcErr(e)) { 378 this.error = e.error; 379 } else { 380 this.error = 'failed (see console)'; 381 console.error(e); 382 } 383 } 384 this.loading = false; 385 }, 386 })); 387 388 Alpine.data('repoSize', () => ({ 389 lading: false, 390 error: null, 391 size: null, 392 393 async loadRepoForSize(did, pds) { 394 this.loading = true; 395 396 if (!did || !pds) { 397 this.loading = false; 398 return; 399 } 400 const query = window.SimpleQuery(pds); 401 try { 402 const res = await query('com.atproto.sync.getRepo', { 403 params: { did }, 404 as: 'blob', 405 }); 406 let bytes = res.size; 407 let mbs = bytes / Math.pow(2, 20); 408 this.size = mbs.toFixed(1); 409 } catch (e) { 410 if (window.isXrpcErr(e)) { 411 this.error = e.error; 412 } else { 413 this.error = 'failed (see console)'; 414 console.error(e); 415 } 416 } 417 this.loading = false; 418 }, 419 })); 420 421 Alpine.data('relayCheckRepo', (did, relay) => ({ 422 loading: false, 423 error: null, 424 status: null, 425 expectedErrorInfo: null, 426 427 async init() { 428 await this.check(did, relay); 429 }, 430 431 async check(did, relay) { 432 this.loading = true; 433 this.error = null; 434 this.status = null; 435 this.expectedErrorInfo = null; 436 437 const query = window.SimpleQuery(`https://${relay.hostname}`); 438 try { 439 this.status = await query('com.atproto.sync.getRepoStatus', { 440 params: { did }, 441 }); 442 } catch(e) { 443 if (relay.missingApis['com.atproto.sync.getRepoStatus']) { 444 this.error = 'Can\'t check'; 445 this.expectedErrorInfo = relay.missingApis['com.atproto.sync.getRepoStatus']; 446 } else if (window.isXrpcErr(e)) { 447 this.error = e.error; 448 } else { 449 this.error = 'Failed to check (see console)'; 450 console.error(e); 451 } 452 } 453 454 this.loading = false; 455 }, 456 457 revStatus(repoRev) { 458 if ( 459 !repoRev || 460 !(this.status && this.status.rev) 461 ) return null; 462 463 if (this.status.rev < repoRev) return 'behind'; 464 if (this.status.rev === repoRev) return 'current'; 465 if (this.status.rev > repoRev) return 'ahead'; 466 } 467 })); 468 469 Alpine.data('pdsHistory', (did, currentPds) => ({ 470 loading: false, 471 error: null, 472 history: [], 473 474 async init() { 475 this.loading = true; 476 this.error = null; 477 this.history = []; 478 try { 479 const res = await fetch(`https://plc.directory/${did}/log/audit`); 480 if (res.ok) { 481 const log = await res.json(); 482 let prev = null; 483 for (op of log) { 484 let opPds = null; 485 const services = op.operation.services; 486 if (services) { 487 const app = services.atproto_pds; 488 if (app) { 489 opPds = app.endpoint; 490 } 491 } 492 if (opPds === prev) continue; 493 prev = opPds; 494 this.history.push({ 495 pds: opPds, 496 date: op.createdAt, 497 }); 498 } 499 this.history.reverse(); 500 if (this.history[0]) this.history[0].current = true; 501 } else { 502 this.error = `${res.status}: ${await res.text()}`; 503 } 504 } catch (e) { 505 this.error = 'failed to get history'; 506 console.error(e); 507 } 508 this.loading = false; 509 }, 510 })); 511 512 Alpine.data('handleHistory', (did, currentHandle) => ({ 513 loading: false, 514 error: null, 515 history: [], 516 517 async init() { 518 this.loading = true; 519 this.error = null; 520 this.history = []; 521 try { 522 const res = await fetch(`https://plc.directory/${did}/log/audit`); 523 if (res.ok) { 524 const log = await res.json(); 525 let prev = null; 526 for (op of log) { 527 let opHandle = null; 528 if (op.operation.alsoKnownAs) { 529 for (aka of op.operation.alsoKnownAs) { 530 if (aka.startsWith("at://")) { 531 opHandle = aka.slice("at://".length); 532 break; 533 } 534 } 535 } 536 if (opHandle === prev) continue; 537 prev = opHandle; 538 this.history.push({ 539 handle: opHandle, 540 date: op.createdAt, 541 }); 542 } 543 this.history.reverse(); 544 } else { 545 this.error = `${res.status}: ${await res.text()}`; 546 } 547 } catch (e) { 548 this.error = 'failed to get history'; 549 console.error(e); 550 } 551 this.loading = false; 552 }, 553 })); 554 }) 555 </script> 556 </head> 557 <body x-data="debug"> 558 <div class="hero bg-base-200"> 559 <div class="hero-content flex-col"> 560 <h1>PDS Debugger</h1> 561 562 <p class="text-sm">Work in progress!</p> 563 <details class="text-xs"> 564 <summary>Future features</summary> 565 <ul class="list-disc pl-4"> 566 <li>firehose & jetstream listeners</li> 567 </ul> 568 </details> 569 <details class="text-xs"> 570 <summary>Limitations</summary> 571 <ul class="list-disc pl-4"> 572 <li>The Bluesky production relay at <code>bsky.network</code> runs the old bgs implementation, and is missing many relay XRPC endpoints.</li> 573 <li>The Blacksky relay at <code>atproto.africa</code> runs an independent implementation, and is also missing many relay XRPC endpoints.</li> 574 <li>All diagnostics run in your browser, so servers that don't enable CORS (some PDS hosts, Upcloud's relay) will fail tests.</li> 575 </ul> 576 </details> 577 578 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 579 <div class="card-body"> 580 <form @submit.prevent="await diagnose()"> 581 <label> 582 Enter an atproto handle, DID, or HTTPS PDS URL 583 <input 584 class="input" 585 x-model="identifier" 586 :disabled="identifierLoading" 587 autofocus 588 /> 589 </label> 590 </form> 591 </div> 592 </div> 593 594 <template x-if="identifierError"> 595 <p>uh oh: <span x-text="identifierError"></span></p> 596 </template> 597 598 <template x-if="pds != null"> 599 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 600 <div class="card-body"> 601 <h2 class="card-title"> 602 <span class="badge badge-secondary">PDS</span> 603 <span x-text="pds"></span> 604 </h2> 605 606 <div 607 x-data="pdsCheck(pds)" 608 x-init="$watch('pds', v => update(v))" 609 > 610 <h3 class="text-lg mt-3"> 611 Server 612 <span 613 x-show="description !== null" 614 class="badge badge-sm badge-soft badge-success" 615 >online</span> 616 </h3> 617 <p x-show="loadingDesc">Loading&hellip;</p> 618 <p x-show="error" class="text-warning" x-text="error"></p> 619 <template x-if="description !== null"> 620 <div> 621 <div class="overflow-x-auto"> 622 <table class="table table-xs"> 623 <tbody> 624 <tr> 625 <td class="text-sm"> 626 Open registration: 627 <span 628 x-text="!description.inviteCodeRequired" 629 ></span> 630 </td> 631 </tr> 632 <tr> 633 <td class="text-sm"> 634 Version: 635 <code 636 class="text-xs" 637 x-text="version" 638 ></code> 639 </td> 640 </tr> 641 </tbody> 642 </table> 643 </div> 644 <h4 class="font-bold"> 645 Accounts 646 </h4> 647 <div class="overflow-x-auto overflow-y-auto max-h-26"> 648 <table class="table table-xs"> 649 <tbody> 650 <template x-for="account in accounts"> 651 <tr> 652 <td> 653 <code> 654 <a 655 href="#" 656 class="link" 657 x-text="account.did" 658 @click.prevent="goto(account.did)" 659 ></a> 660 </code> 661 </td> 662 <td> 663 <span 664 x-show="account.active" 665 class="badge badge-sm badge-soft badge-success" 666 > 667 active 668 </span> 669 <span 670 x-show="!account.active" 671 x-text="account.status" 672 class="badge badge-sm badge-soft badge-warning" 673 ></span> 674 </td> 675 <td 676 x-data="didToHandle(account.did)" 677 x-intersect:enter.once="load" 678 > 679 <span x-show="loading">Loading&hellip;</span> 680 <span x-show="error !== null" x-text="error"></span> 681 <a 682 href="#" 683 class="link" 684 @click.prevent="goto(handle)" 685 x-show="handle !== null" 686 x-text="`@${handle}`" 687 ></a> 688 </td> 689 </tr> 690 </template> 691 <template x-if="!loadingDesc && !accountsComplete"> 692 <tr> 693 <td colspan="2" class="text-xs text-warning-content"> 694 (more accounts not shown) 695 </td> 696 </tr> 697 </template> 698 </tbody> 699 </table> 700 </div> 701 </div> 702 </template> 703 </div> 704 705 <h3 class="text-lg mt-3">Relay host status</h3> 706 <div class="overflow-x-auto"> 707 <table class="table table-xs"> 708 <tbody> 709 <template x-for="relay in window.relays"> 710 <tr 711 x-data="relayCheckHost(pds, relay)" 712 x-init="$watch('pds', pds => check(pds, relay))" 713 > 714 <td class="text-sm"> 715 <div class="tooltip tooltip-right" :data-tip="relay.hostname"> 716 <img 717 class="inline-block h-4 w-4" 718 :src="relay.icon" 719 alt="" 720 /> 721 <span x-text="relay.name"></span> 722 <span 723 x-show="!!relay.note" 724 x-text="relay.note" 725 class="badge badge-soft badge-neutral badge-xs" 726 ></span> 727 </div> 728 </td> 729 <td> 730 <template x-if="loading"> 731 <em>loading&hellip;</em> 732 </template> 733 <template x-if="error"> 734 <div 735 class="text-xs" 736 :class="expectedErrorInfo 737 ? 'text-info tooltip tooltip-left cursor-help' 738 : 'text-warning'" 739 :data-tip="expectedErrorInfo" 740 > 741 <span x-text="error"></span> 742 <span 743 x-show="!!expectedErrorInfo" 744 class="badge badge-soft badge-info badge-xs" 745 >i</span> 746 </div> 747 </template> 748 <template x-if="status"> 749 <span 750 x-text="status" 751 class="badge badge-sm" 752 :class="status === 'active' && 'badge-soft badge-success'" 753 ></span> 754 </template> 755 </td> 756 <td> 757 <div x-show="status !== 'active' && !expectedErrorInfo"> 758 <button 759 x-show="reqCrawlStatus !== 'done'" 760 class="btn btn-xs btn-ghost whitespace-nowrap" 761 :disabled="reqCrawlStatus === 'loading'" 762 @click="requestCrawl(pds, relay)" 763 > 764 request crawl 765 </button> 766 <span 767 x-show="reqCrawlError !== null" 768 x-text="reqCrawlError" 769 class="text-xs text-warning" 770 ></span> 771 <button 772 x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 773 class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 774 @click="check" 775 > 776 refresh 777 </button> 778 </div> 779 </td> 780 </tr> 781 </template> 782 </tbody> 783 </table> 784 </div> 785 </div> 786 </div> 787 </template> 788 789 <template x-if="did != null"> 790 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 791 <div class="card-body"> 792 <h2 class="card-title"> 793 <span class="badge badge-secondary">DID</span> 794 <code x-text="did"></code> 795 </h2> 796 <template x-if="pds != null"> 797 <div x-data="didRepoState(did, pds)"> 798 <h3 class="text-lg mt-3"> 799 Repo 800 <span 801 x-show="state && state.active" 802 class="badge badge-sm badge-soft badge-success" 803 >active</span> 804 </h3> 805 <div class="overflow-x-auto"> 806 <table class="table table-xs"> 807 <tbody> 808 <tr> 809 <td class="text-sm"> 810 Rev: 811 <code x-text="state && state.rev"></code> 812 </td> 813 </tr> 814 <tr> 815 <td class="text-sm"> 816 PDS: 817 <a 818 href="#" 819 class="link" 820 @click.prevent="goto(pds)" 821 x-text="pds" 822 ></a> 823 </td> 824 </tr> 825 <tr> 826 <td class="text-sm"> 827 Size: 828 <span x-data="repoSize"> 829 <template x-if="loading"> 830 <em>loading&hellip;</em> 831 </template> 832 <template x-if="error"> 833 <span class="text-xs text-warning" x-text="error"></span> 834 </template> 835 <template x-if="size"> 836 <code> 837 <span x-text="size"></span> MiB 838 </code> 839 </template> 840 <template x-if="!size && !error && !loading"> 841 <button 842 class="btn btn-xs btn-soft btn-primary" 843 @click.prevent="loadRepoForSize(did, pds)" 844 >load</button> 845 </template> 846 </span> 847 </td> 848 </tr> 849 </tbody> 850 </table> 851 </div> 852 853 <h3 class="text-lg mt-3"> 854 Relay repo status 855 </h3> 856 <div class="overflow-x-auto"> 857 <table class="table table-xs"> 858 <tbody> 859 <template x-for="relay in window.relays"> 860 <tr 861 x-data="relayCheckRepo(did, relay)" 862 x-init="$watch('pds', pds => check(did, relay))" 863 > 864 <td class="text-sm"> 865 <div class="tooltip tooltip-right" :data-tip="relay.hostname"> 866 <img 867 class="inline-block h-4 w-4" 868 :src="relay.icon" 869 alt="" 870 /> 871 <span x-text="relay.name"></span> 872 <span 873 x-show="!!relay.note" 874 x-text="relay.note" 875 class="badge badge-neutral badge-soft badge-xs" 876 ></span> 877 </div> 878 </td> 879 <template x-if="loading"> 880 <td> 881 <em>loading&hellip;</em> 882 </td> 883 </template> 884 <template x-if="error"> 885 <td> 886 <div 887 class="text-xs" 888 :class="expectedErrorInfo 889 ? 'text-info tooltip tooltip-left cursor-help' 890 : 'text-warning'" 891 :data-tip="expectedErrorInfo" 892 > 893 <span x-text="error"></span> 894 <span 895 x-show="!!expectedErrorInfo" 896 class="badge badge-soft badge-info badge-xs" 897 >i</span> 898 </div> 899 </td> 900 </template> 901 <template x-if="status"> 902 <td> 903 <span 904 x-show="status.active" 905 class="badge badge-sm badge-soft badge-success" 906 > 907 active 908 </span> 909 <span 910 x-show="!status.active" 911 x-text="status.status" 912 class="badge badge-sm badge-soft badge-warning" 913 ></span> 914 </td> 915 </template> 916 <template x-if="revStatus(state && state.rev)"> 917 <td x-data="{ asdf: revStatus(state.rev) }"> 918 <span 919 x-show="asdf === 'current'" 920 class="badge badge-sm badge-soft badge-success" 921 >current</span> 922 <span 923 x-show="asdf === 'behind'" 924 class="badge badge-sm badge-soft badge-warning tooltip tooltip-left" 925 :data-tip="status.rev" 926 >behind</span> 927 <span 928 x-show="asdf === 'ahead'" 929 class="badge badge-sm badge-soft badge-success tooltip tooltip-left" 930 :data-tip="`Account may have updated between checks? ${status.rev}`" 931 >ahead</span> 932 </td> 933 </template> 934 </tr> 935 </template> 936 </tbody> 937 </table> 938 </div> 939 940 <template x-if="did.startsWith('did:plc:')"> 941 <div x-data="pdsHistory(did, pds)"> 942 <h3 class="text-lg mt-3"> 943 PLC PDS history 944 </h3> 945 <div class="overflow-x-auto"> 946 <table class="table table-xs"> 947 <tbody> 948 <template x-if="loading"> 949 <tr> 950 <td>Loading&hellip;</td> 951 </tr> 952 </template> 953 <template x-if="error"> 954 <tr> 955 <td>Error: <span x-text="error"></span></td> 956 </tr> 957 </template> 958 <template x-if="!loading && !error && history.length === 0"> 959 <tr> 960 <td class="text-sm"> 961 <em>no previous PDS</em> 962 </td> 963 </tr> 964 </template> 965 <template x-for="event in history"> 966 <tr x-data="didRepoState(did, event.pds)"> 967 <td> 968 <code x-text="event.date.split('T')[0]"></code> 969 </td> 970 <td> 971 <a 972 href="#" 973 class="link" 974 @click.prevent="goto(event.pds)" 975 x-text="event.pds" 976 ></a> 977 </td> 978 <template x-if="event.current"> 979 <td> 980 <span 981 x-show="state && !state.active" 982 x-text="state && state.status" 983 class="badge badge-sm badge-soft badge-warning" 984 ></span> 985 <span 986 x-show="state && state.active" 987 class="badge badge-sm badge-soft badge-success" 988 >current</span> 989 </td> 990 </template> 991 <template x-if="!event.current"> 992 <td> 993 <span 994 x-show="state && !state.active" 995 x-text="state && state.status" 996 class="badge badge-sm badge-soft badge-success" 997 ></span> 998 <span 999 x-show="state && state.active" 1000 class="badge badge-sm badge-soft badge-warning" 1001 >active</span> 1002 </td> 1003 </template> 1004 </tr> 1005 </template> 1006 </tbody> 1007 </table> 1008 </div> 1009 </div> 1010 </template> 1011 </div> 1012 </template> 1013 </div> 1014 </div> 1015 </template> 1016 1017 <template x-if="handle !== null"> 1018 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 1019 <div 1020 x-data="checkHandle(handle)" 1021 x-init="$watch('handle', h => updateHandle(h))" 1022 class="card-body" 1023 > 1024 <h2 class="card-title"> 1025 <span class="badge badge-secondary">Handle</span> 1026 <span x-text="handle"></span> 1027 </h2> 1028 1029 <h3 class="text-lg mt-3"> 1030 Resolution 1031 </h3> 1032 <p x-show="loading" class="text-i">Loading&hellip;</p> 1033 <div x-show="!loading" class="overflow-x-auto"> 1034 <table class="table table-xs"> 1035 <tbody> 1036 <tr> 1037 <td class="text-sm">DNS</td> 1038 <td class="text-sm"> 1039 <code x-text="dnsDid"></code> 1040 </td> 1041 <td> 1042 <div 1043 class="badge badge-sm badge-soft badge-neutral" 1044 x-show="dnsErr !== null" 1045 x-text="dnsErr" 1046 ></div> 1047 </td> 1048 </tr> 1049 <tr> 1050 <td class="text-sm">Http</td> 1051 <td class="text-sm"> 1052 <code x-text="httpDid"></code> 1053 </td> 1054 <td> 1055 <div 1056 class="badge badge-sm badge-soft badge-neutral" 1057 x-show="httpErr !== null" 1058 x-text="httpErr" 1059 ></div> 1060 </td> 1061 </tr> 1062 </tbody> 1063 </table> 1064 </div> 1065 1066 <template x-if="did !== null && did.startsWith('did:plc:')"> 1067 1068 <div x-data="handleHistory(did, handle)"> 1069 <h3 class="text-lg mt-3"> 1070 PLC handle history 1071 </h3> 1072 <div class="overflow-x-auto"> 1073 <table class="table table-xs"> 1074 <tbody> 1075 <template x-if="loading"> 1076 <tr> 1077 <td>Loading&hellip;</td> 1078 </tr> 1079 </template> 1080 <template x-if="error"> 1081 <tr> 1082 <td>Error: <span x-text="error"></span></td> 1083 </tr> 1084 </template> 1085 <template x-if="!loading && !error && history.length === 0"> 1086 <tr> 1087 <td class="text-sm"> 1088 <em>no previous handle</em> 1089 </td> 1090 </tr> 1091 </template> 1092 <template x-for="event in history"> 1093 <tr> 1094 <td> 1095 <code x-text="event.date.split('T')[0]"></code> 1096 </td> 1097 <td> 1098 <a 1099 href="#" 1100 class="link" 1101 @click.prevent="goto(event.handle)" 1102 x-text="event.handle" 1103 ></a> 1104 </td> 1105 </tr> 1106 </template> 1107 </tbody> 1108 </table> 1109 </div> 1110 </div> 1111 1112 </template> 1113 </div> 1114 </div> 1115 </template> 1116 </div> 1117 </div> 1118 </body> 1119</html>