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.0'; 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 sync1.1 East', 45 hostname: 'relay1.us-east.bsky.network', 46 }, 47 { 48 name: 'Bluesky sync1.1 West', 49 hostname: 'relay1.us-west.bsky.network', 50 }, 51 { 52 name: 'Microcosm Montreal', 53 hostname: 'relay.fire.hose.cam', 54 }, 55 { 56 name: 'Microcosm France', 57 hostname: 'relay3.fr.hose.cam', 58 }, 59 { 60 name: 'Bluesky prod (old)', 61 hostname: 'bsky.network', 62 }, 63 { 64 name: 'Blacksky (partial xrpc)', 65 hostname: 'atproto.africa', 66 }, 67 { 68 name: 'Upcloud (no CORS)', 69 hostname: 'relay.upcloud.world', 70 }, 71 ]; 72 </script> 73 74 <script> 75 document.addEventListener('alpine:init', () => { 76 Alpine.data('debug', () => ({ 77 // form input 78 identifier: '', 79 80 // state 81 identifierLoading: false, 82 identifierError: null, 83 84 // stuff to check 85 pds: null, 86 did: null, 87 handle: null, 88 89 async goto(identifier) { 90 this.identifier = identifier; 91 await this.diagnose(); 92 }, 93 94 async diagnose() { 95 this.identifierLoading = true; 96 this.identifierError = null; 97 this.pds = null; 98 this.did = null; 99 this.handle = null; 100 this.identifier = this.identifier.trim(); 101 if (this.identifier === '') { 102 // do nothing 103 } else if (this.identifier.startsWith('https://')) { 104 this.pds = this.identifier; 105 } else { 106 if (this.identifier.startsWith('at://')) { 107 this.identifier = this.identifier.slice('at://'.length); 108 } 109 if (this.identifier.startsWith('did:')) { 110 this.did = this.identifier; 111 let data; 112 try { 113 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 114 params: { identifier: this.identifier }, 115 }); 116 this.pds = data.pds; 117 this.handle = data.handle; 118 } catch (e) { 119 if (window.isXrpcErr(e)) { 120 this.identifierError = e.error; 121 if (e.message) this.description += ` ${e.description}`; 122 } else { 123 this.identifierError = 'Failed to resolve identifier, see console for error.'; 124 console.error(e); 125 } 126 } 127 } else { 128 this.handle = 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.did = data.did; 135 this.pds = data.pds; 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 } 146 } 147 this.identifierLoading = false; 148 }, 149 })); 150 151 Alpine.data('pdsCheck', pds => ({ 152 loadingDesc: false, 153 error: null, 154 description: null, 155 accounts: [], 156 accountsComplete: false, 157 version: null, 158 159 async init() { 160 await this.update(pds); 161 }, 162 163 async update(pds) { 164 this.loadingDesc = true; 165 this.error = null; 166 this.description = null; 167 this.accounts = []; 168 this.accountsComplete = false; 169 this.version = null; 170 171 if (!pds) { 172 this.loadingDesc = false; 173 return; 174 } 175 176 let query = window.SimpleQuery(pds); 177 try { 178 this.description = await query('com.atproto.server.describeServer'); 179 } catch (e) { 180 if (window.isXrpcErr(e)) { 181 this.error = e.error; 182 } else { 183 this.error = 'Failed to reach (see console)'; 184 console.error(e); 185 } 186 } 187 let health 188 try { 189 health = await query('_health'); 190 this.version = health.version; 191 } catch (e) { 192 if (window.isXrpcErr(e)) { 193 this.error = e.error; 194 } else { 195 this.error = 'Failed to reach (see console)'; 196 console.error(e); 197 } 198 } 199 let accountsRes; 200 try { 201 accountsRes = await query('com.atproto.sync.listRepos', { 202 params: { limit: 100 }, 203 }); 204 this.accounts = accountsRes.repos; 205 this.accountsComplete == !accountsRes.cursor; 206 } catch (e) { 207 if (window.isXrpcErr(e)) { 208 this.error = e.error; 209 } else { 210 this.error = 'Failed to reach (see console)'; 211 console.error(e); 212 } 213 } 214 this.loadingDesc = false; 215 }, 216 })); 217 218 Alpine.data('relayCheckHost', (pds, relay) => ({ 219 loading: false, 220 error: null, 221 status: null, 222 reqCrawlStatus: null, 223 reqCrawlError: null, 224 225 async init() { 226 await this.check(pds, relay); 227 }, 228 229 async check(pds, relay) { 230 this.loading = true; 231 this.error = null; 232 this.status = null; 233 const query = window.SimpleQuery(`https://${relay.hostname}`); 234 const hostname = pds.split('://')[1]; 235 let data; 236 try { 237 data = await query('com.atproto.sync.getHostStatus', { 238 params: { hostname }, 239 }); 240 this.status = data.status; 241 } catch(e) { 242 if (window.isXrpcErr(e)) { 243 this.error = e.error; 244 } else { 245 this.error = 'Failed to check (see console)'; 246 console.error(e); 247 } 248 } 249 this.loading = false; 250 this.reqCrawlStatus = null; 251 this.reqCrawlError = null; 252 }, 253 254 async requestCrawl(pds, relay) { 255 this.reqCrawlStatus = "loading"; 256 const proc = window.SimpleProc(`https://${relay.hostname}`); 257 const hostname = pds.split('://')[1]; 258 let data; 259 try { 260 data = await proc('com.atproto.sync.requestCrawl', { 261 input: { hostname }, 262 }); 263 } catch (e) { 264 if (window.isXrpcErr(e)) { 265 this.reqCrawlError = e.error; 266 } else { 267 this.reqCrawlError = 'failed (see console)'; 268 console.error(e); 269 } 270 } 271 this.reqCrawlStatus = "done"; 272 }, 273 })); 274 275 Alpine.data('checkHandle', handle => ({ 276 loading: false, 277 dnsDid: null, 278 dnsErr: null, 279 httpDid: null, 280 httpErr: null, 281 282 async init() { 283 await this.updateHandle(handle); 284 }, 285 async updateHandle(handle) { 286 this.loading = true; 287 this.dnsDid = null; 288 this.dnsErr = null; 289 this.httpDid = null; 290 this.httpErr = null; 291 try { 292 this.dnsDid = await window.dnsResolver.resolve(handle); 293 } catch (e) { 294 this.dnsErr = e.name; 295 } 296 try { 297 this.httpDid = await window.httpResolver.resolve(handle); 298 } catch (e) { 299 this.httpErr = e.name; 300 } 301 this.loading = false; 302 }, 303 })); 304 305 Alpine.data('didToHandle', did => ({ 306 loading: false, 307 error: null, 308 handle: null, 309 async load() { 310 loading = true; 311 error = null; 312 handle = null; 313 let data; 314 try { 315 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 316 params: { identifier: did }, 317 }); 318 this.handle = data.handle; 319 } catch (e) { 320 if (window.isXrpcErr(e)) { 321 this.error = e.error; 322 } else { 323 this.error = 'failed (see console)'; 324 console.error(e); 325 } 326 } 327 loading = false; 328 } 329 })); 330 331 Alpine.data('didRepoState', (did, pds) => ({ 332 loading: false, 333 error: null, 334 state: null, 335 336 async init() { 337 await this.checkRepoState(did, pds); 338 }, 339 async checkRepoState(did, pds) { 340 this.loading = true; 341 this.error = null; 342 this.state = null; 343 344 if (!did || !pds) { 345 this.loading = false; 346 return; 347 } 348 const query = window.SimpleQuery(pds); 349 try { 350 this.state = await query('com.atproto.sync.getRepoStatus', { 351 params: { did }, 352 }); 353 } catch (e) { 354 if (window.isXrpcErr(e)) { 355 this.error = e.error; 356 } else { 357 this.error = 'failed (see console)'; 358 console.error(e); 359 } 360 } 361 this.loading = false; 362 }, 363 })); 364 365 Alpine.data('relayCheckRepo', (did, relay) => ({ 366 loading: false, 367 error: null, 368 status: null, 369 370 async init() { 371 await this.check(did, relay); 372 }, 373 374 async check(did, relay) { 375 this.loading = true; 376 this.error = null; 377 this.status = null; 378 379 const query = window.SimpleQuery(`https://${relay.hostname}`); 380 try { 381 this.status = await query('com.atproto.sync.getRepoStatus', { 382 params: { did }, 383 }); 384 } catch(e) { 385 if (window.isXrpcErr(e)) { 386 this.error = e.error; 387 } else { 388 this.error = 'Failed to check (see console)'; 389 console.error(e); 390 } 391 } 392 393 this.loading = false; 394 }, 395 })); 396 397 Alpine.data('pdsHistory', (did, currentPds) => ({ 398 loading: false, 399 error: null, 400 history: [], 401 402 async init() { 403 this.loading = true; 404 this.error = null; 405 this.history = []; 406 try { 407 const res = await fetch(`https://plc.directory/${did}/log/audit`); 408 if (res.ok) { 409 const log = await res.json(); 410 let prev = null; 411 for (op of log) { 412 const opPds = op.operation.services.atproto_pds.endpoint; 413 if (opPds === prev) continue; 414 prev = opPds; 415 this.history.push({ 416 pds: opPds, 417 date: op.createdAt, 418 }); 419 } 420 this.history.reverse(); 421 if (this.history[0]) this.history[0].current = true; 422 } else { 423 this.error = `${res.status}: ${await res.text()}`; 424 } 425 } catch (e) { 426 this.error = 'failed to get history'; 427 console.error(e); 428 } 429 this.loading = false; 430 }, 431 })); 432 433 Alpine.data('handleHistory', (did, currentHandle) => ({ 434 loading: false, 435 error: null, 436 history: [], 437 438 async init() { 439 this.loading = true; 440 this.error = null; 441 this.history = []; 442 try { 443 const res = await fetch(`https://plc.directory/${did}/log/audit`); 444 if (res.ok) { 445 const log = await res.json(); 446 let prev = null; 447 for (op of log) { 448 let opHandle = null; 449 for (aka of op.operation.alsoKnownAs) { 450 if (aka.startsWith("at://")) { 451 opHandle = aka.slice("at://".length); 452 break; 453 } 454 } 455 if (opHandle === prev) continue; 456 prev = opHandle; 457 this.history.push({ 458 handle: opHandle, 459 date: op.createdAt, 460 }); 461 } 462 this.history.reverse(); 463 } else { 464 this.error = `${res.status}: ${await res.text()}`; 465 } 466 } catch (e) { 467 this.error = 'failed to get history'; 468 console.error(e); 469 } 470 this.loading = false; 471 }, 472 })); 473 }) 474 </script> 475 </head> 476 <body x-data="debug"> 477 <div class="hero bg-base-200"> 478 <div class="hero-content flex-col"> 479 <h1>PDS Debugger</h1> 480 481 <p class="text-sm">Work in progress!</p> 482 <details class="text-xs"> 483 <summary>Future features</summary> 484 <ul class="list-disc pl-4"> 485 <li>firehose & jetstream listeners</li> 486 </ul> 487 </details> 488 <details class="text-xs"> 489 <summary>Limitations</summary> 490 <ul class="list-disc pl-4"> 491 <li>The Bluesky production relay at <code>bsky.network</code> runs the old bgs implementation, and is missing many relay XRPC endpoints.</li> 492 <li>The Blacksky relay at <code>atproto.africa</code> runs an independent implementation, and is also missing many relay XRPC endpoints.</li> 493 <li>All diagnostics run in your browser, so servers that don't enable CORS (some PDS hosts, Upcloud's relay) will fail tests.</li> 494 </ul> 495 </details> 496 497 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 498 <div class="card-body"> 499 <form @submit.prevent="await diagnose()"> 500 <label> 501 Enter an atproto handle, DID, or HTTPS PDS URL 502 <input 503 class="input" 504 x-model="identifier" 505 :disabled="identifierLoading" 506 autofocus 507 /> 508 </label> 509 </form> 510 </div> 511 </div> 512 513 <template x-if="identifierError"> 514 <p>uh oh: <span x-text="identifierError"></span></p> 515 </template> 516 517 <template x-if="pds != null"> 518 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 519 <div class="card-body"> 520 <h2 class="card-title"> 521 <span class="badge badge-secondary">PDS</span> 522 <span x-text="pds"></span> 523 </h2> 524 525 <div 526 x-data="pdsCheck(pds)" 527 x-init="$watch('pds', v => update(v))" 528 > 529 <h3 class="text-lg mt-3"> 530 Server 531 <span 532 x-show="description !== null" 533 class="badge badge-sm badge-soft badge-success" 534 >online</span> 535 </h3> 536 <p x-show="loadingDesc">Loading&hellip;</p> 537 <p x-show="error" class="text-warning" x-text="error"></p> 538 <template x-if="description !== null"> 539 <div> 540 <div class="overflow-x-auto"> 541 <table class="table table-xs"> 542 <tbody> 543 <tr> 544 <td class="text-sm"> 545 Open registration: 546 <span 547 x-text="!description.inviteCodeRequired" 548 ></span> 549 </td> 550 </tr> 551 <tr> 552 <td class="text-sm"> 553 Version: 554 <code 555 class="text-xs" 556 x-text="version" 557 ></code> 558 </td> 559 </tr> 560 </tbody> 561 </table> 562 </div> 563 <h4 class="font-bold"> 564 Accounts 565 </h4> 566 <div class="overflow-x-auto overflow-y-auto max-h-26"> 567 <table class="table table-xs"> 568 <tbody> 569 <template x-for="account in accounts"> 570 <tr> 571 <td> 572 <code> 573 <a 574 href="#" 575 class="link" 576 x-text="account.did" 577 @click.prevent="goto(account.did)" 578 ></a> 579 </code> 580 </td> 581 <td> 582 <span 583 x-show="account.active" 584 class="badge badge-sm badge-soft badge-success" 585 > 586 active 587 </span> 588 <span 589 x-show="!account.active" 590 x-text="account.status" 591 class="badge badge-sm badge-soft badge-warning" 592 ></span> 593 </td> 594 <td 595 x-data="didToHandle(account.did)" 596 x-intersect:enter.once="load" 597 > 598 <span x-show="loading">Loading&hellip;</span> 599 <span x-show="error !== null" x-text="error"></span> 600 <a 601 href="#" 602 class="link" 603 @click.prevent="goto(handle)" 604 x-show="handle !== null" 605 x-text="`@${handle}`" 606 ></a> 607 </td> 608 </tr> 609 </template> 610 <template x-if="!loadingDesc && !accountsComplete"> 611 <tr> 612 <td colspan="2" class="text-xs text-warning-content"> 613 (more accounts not shown) 614 </td> 615 </tr> 616 </template> 617 </tbody> 618 </table> 619 </div> 620 </div> 621 </template> 622 </div> 623 624 <h3 class="text-lg mt-3">Relay host status</h3> 625 <div class="overflow-x-auto overflow-y-auto max-h-33"> 626 <table class="table table-xs"> 627 <tbody> 628 <template x-for="relay in window.relays"> 629 <tr 630 x-data="relayCheckHost(pds, relay)" 631 x-init="$watch('pds', pds => check(pds, relay))" 632 > 633 <td x-text="relay.name" class="text-sm"></td> 634 <td> 635 <template x-if="loading"> 636 <em>loading&hellip;</em> 637 </template> 638 <template x-if="error"> 639 <span 640 x-text="error" 641 class="text-xs text-warning" 642 ></span> 643 </template> 644 <template x-if="status"> 645 <span 646 x-text="status" 647 class="badge badge-sm" 648 :class="status === 'active' && 'badge-soft badge-success'" 649 ></span> 650 </template> 651 </td> 652 <td> 653 <div x-show="status !== 'active'"> 654 <button 655 x-show="reqCrawlStatus !== 'done'" 656 class="btn btn-xs btn-ghost whitespace-nowrap" 657 :disabled="reqCrawlStatus === 'loading'" 658 @click="requestCrawl(pds, relay)" 659 > 660 request crawl 661 </button> 662 <span 663 x-show="reqCrawlError !== null" 664 x-text="reqCrawlError" 665 class="text-xs text-warning" 666 ></span> 667 <button 668 x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 669 class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 670 @click="check" 671 > 672 refresh 673 </button> 674 </div> 675 </td> 676 </tr> 677 </template> 678 </tbody> 679 </table> 680 </div> 681 </div> 682 </div> 683 </template> 684 685 <template x-if="did != null"> 686 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 687 <div class="card-body"> 688 <h2 class="card-title"> 689 <span class="badge badge-secondary">DID</span> 690 <code x-text="did"></code> 691 </h2> 692 <template x-if="pds != null"> 693 <div x-data="didRepoState(did, pds)"> 694 <h3 class="text-lg mt-3"> 695 Repo 696 <span 697 x-show="state && state.active" 698 class="badge badge-sm badge-soft badge-success" 699 >active</span> 700 </h3> 701 <div class="overflow-x-auto"> 702 <table class="table table-xs"> 703 <tbody> 704 <tr> 705 <td class="text-sm"> 706 Rev: 707 <code x-text="state && state.rev"></code> 708 </td> 709 </tr> 710 <tr> 711 <td class="text-sm"> 712 PDS: 713 <a 714 href="#" 715 class="link" 716 @click.prevent="goto(pds)" 717 x-text="pds" 718 ></a> 719 </td> 720 </tr> 721 </tbody> 722 </table> 723 </div> 724 725 <h3 class="text-lg mt-3"> 726 Relay repo status 727 </h3> 728 <div class="overflow-x-auto overflow-y-auto max-h-33"> 729 <table class="table table-xs"> 730 <tbody> 731 <template x-for="relay in window.relays"> 732 <tr 733 x-data="relayCheckRepo(did, relay)" 734 x-init="$watch('pds', pds => check(did, relay))" 735 > 736 <td x-text="relay.name" class="text-sm"></td> 737 <template x-if="loading"> 738 <td> 739 <em>loading&hellip;</em> 740 </td> 741 </template> 742 <template x-if="error"> 743 <td 744 x-text="error" 745 class="text-xs text-warning" 746 ></td> 747 </template> 748 <template x-if="status"> 749 <td> 750 <span 751 x-show="status.active" 752 class="badge badge-sm badge-soft badge-success" 753 > 754 active 755 </span> 756 <span 757 x-show="!status.active" 758 x-text="status.status" 759 class="badge badge-sm badge-soft badge-warning" 760 ></span> 761 </td> 762 </template> 763 <template x-if="status"> 764 <td> 765 <code 766 x-text="status.rev" 767 class="badge badge-sm badge-soft" 768 :class="status && state && (status.rev >= state.rev) ? 'badge-success' : 'badge-warning'" 769 ></code> 770 </td> 771 </template> 772 </tr> 773 </template> 774 </tbody> 775 </table> 776 </div> 777 778 <template x-if="did.startsWith('did:plc:')"> 779 <div x-data="pdsHistory(did, pds)"> 780 <h3 class="text-lg mt-3"> 781 PLC PDS history 782 </h3> 783 <div class="overflow-x-auto"> 784 <table class="table table-xs"> 785 <tbody> 786 <template x-if="loading"> 787 <tr> 788 <td>Loading&hellip;</td> 789 </tr> 790 </template> 791 <template x-if="error"> 792 <tr> 793 <td>Error: <span x-text="error"></span></td> 794 </tr> 795 </template> 796 <template x-if="!loading && !error && history.length === 0"> 797 <tr> 798 <td class="text-sm"> 799 <em>no previous PDS</em> 800 </td> 801 </tr> 802 </template> 803 <template x-for="event in history"> 804 <tr x-data="didRepoState(did, event.pds)"> 805 <td> 806 <code x-text="event.date.split('T')[0]"></code> 807 </td> 808 <td> 809 <a 810 href="#" 811 class="link" 812 @click.prevent="goto(event.pds)" 813 x-text="event.pds" 814 ></a> 815 </td> 816 <template x-if="event.current"> 817 <td> 818 <span 819 x-show="state && !state.active" 820 x-text="state && state.status" 821 class="badge badge-sm badge-soft badge-warning" 822 ></span> 823 <span 824 x-show="state && state.active" 825 class="badge badge-sm badge-soft badge-success" 826 >current</span> 827 </td> 828 </template> 829 <template x-if="!event.current"> 830 <td> 831 <span 832 x-show="state && !state.active" 833 x-text="state && state.status" 834 class="badge badge-sm badge-soft badge-success" 835 ></span> 836 <span 837 x-show="state && state.active" 838 class="badge badge-sm badge-soft badge-warning" 839 >active</span> 840 </td> 841 </template> 842 </tr> 843 </template> 844 </tbody> 845 </table> 846 </div> 847 </div> 848 </template> 849 </div> 850 </template> 851 </div> 852 </div> 853 </template> 854 855 <template x-if="handle !== null"> 856 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 857 <div 858 x-data="checkHandle(handle)" 859 x-init="$watch('handle', h => updateHandle(h))" 860 class="card-body" 861 > 862 <h2 class="card-title"> 863 <span class="badge badge-secondary">Handle</span> 864 <span x-text="handle"></span> 865 </h2> 866 867 <h3 class="text-lg mt-3"> 868 Resolution 869 </h3> 870 <p x-show="loading" class="text-i">Loading&hellip;</p> 871 <div x-show="!loading" class="overflow-x-auto"> 872 <table class="table table-xs"> 873 <tbody> 874 <tr> 875 <td class="text-sm">DNS</td> 876 <td class="text-sm"> 877 <code x-text="dnsDid"></code> 878 </td> 879 <td> 880 <div 881 class="badge badge-sm badge-soft badge-neutral" 882 x-show="dnsErr !== null" 883 x-text="dnsErr" 884 ></div> 885 </td> 886 </tr> 887 <tr> 888 <td class="text-sm">Http</td> 889 <td class="text-sm"> 890 <code x-text="httpDid"></code> 891 </td> 892 <td> 893 <div 894 class="badge badge-sm badge-soft badge-neutral" 895 x-show="httpErr !== null" 896 x-text="httpErr" 897 ></div> 898 </td> 899 </tr> 900 </tbody> 901 </table> 902 </div> 903 904 <template x-if="did !== null && did.startsWith('did:plc:')"> 905 906 <div x-data="handleHistory(did, handle)"> 907 <h3 class="text-lg mt-3"> 908 PLC handle history 909 </h3> 910 <div class="overflow-x-auto"> 911 <table class="table table-xs"> 912 <tbody> 913 <template x-if="loading"> 914 <tr> 915 <td>Loading&hellip;</td> 916 </tr> 917 </template> 918 <template x-if="error"> 919 <tr> 920 <td>Error: <span x-text="error"></span></td> 921 </tr> 922 </template> 923 <template x-if="!loading && !error && history.length === 0"> 924 <tr> 925 <td class="text-sm"> 926 <em>no previous handle</em> 927 </td> 928 </tr> 929 </template> 930 <template x-for="event in history"> 931 <tr> 932 <td> 933 <code x-text="event.date.split('T')[0]"></code> 934 </td> 935 <td> 936 <a 937 href="#" 938 class="link" 939 @click.prevent="goto(event.handle)" 940 x-text="event.handle" 941 ></a> 942 </td> 943 </tr> 944 </template> 945 </tbody> 946 </table> 947 </div> 948 </div> 949 950 </template> 951 </div> 952 </div> 953 </template> 954 </div> 955 </div> 956 </body> 957</html>