Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 62f047c8daaf6a096e0b8541cf756b5d3dfa2020 966 lines 36 kB view raw
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 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 let opPds = null; 413 const services = op.operation.services; 414 if (services) { 415 const app = services.atproto_pds; 416 if (app) { 417 opPds = app.endpoint; 418 } 419 } 420 if (opPds === prev) continue; 421 prev = opPds; 422 this.history.push({ 423 pds: opPds, 424 date: op.createdAt, 425 }); 426 } 427 this.history.reverse(); 428 if (this.history[0]) this.history[0].current = true; 429 } else { 430 this.error = `${res.status}: ${await res.text()}`; 431 } 432 } catch (e) { 433 this.error = 'failed to get history'; 434 console.error(e); 435 } 436 this.loading = false; 437 }, 438 })); 439 440 Alpine.data('handleHistory', (did, currentHandle) => ({ 441 loading: false, 442 error: null, 443 history: [], 444 445 async init() { 446 this.loading = true; 447 this.error = null; 448 this.history = []; 449 try { 450 const res = await fetch(`https://plc.directory/${did}/log/audit`); 451 if (res.ok) { 452 const log = await res.json(); 453 let prev = null; 454 for (op of log) { 455 let opHandle = null; 456 if (op.operation.alsoKnownAs) { 457 for (aka of op.operation.alsoKnownAs) { 458 if (aka.startsWith("at://")) { 459 opHandle = aka.slice("at://".length); 460 break; 461 } 462 } 463 } 464 if (opHandle === prev) continue; 465 prev = opHandle; 466 this.history.push({ 467 handle: opHandle, 468 date: op.createdAt, 469 }); 470 } 471 this.history.reverse(); 472 } else { 473 this.error = `${res.status}: ${await res.text()}`; 474 } 475 } catch (e) { 476 this.error = 'failed to get history'; 477 console.error(e); 478 } 479 this.loading = false; 480 }, 481 })); 482 }) 483 </script> 484 </head> 485 <body x-data="debug"> 486 <div class="hero bg-base-200"> 487 <div class="hero-content flex-col"> 488 <h1>PDS Debugger</h1> 489 490 <p class="text-sm">Work in progress!</p> 491 <details class="text-xs"> 492 <summary>Future features</summary> 493 <ul class="list-disc pl-4"> 494 <li>firehose & jetstream listeners</li> 495 </ul> 496 </details> 497 <details class="text-xs"> 498 <summary>Limitations</summary> 499 <ul class="list-disc pl-4"> 500 <li>The Bluesky production relay at <code>bsky.network</code> runs the old bgs implementation, and is missing many relay XRPC endpoints.</li> 501 <li>The Blacksky relay at <code>atproto.africa</code> runs an independent implementation, and is also missing many relay XRPC endpoints.</li> 502 <li>All diagnostics run in your browser, so servers that don't enable CORS (some PDS hosts, Upcloud's relay) will fail tests.</li> 503 </ul> 504 </details> 505 506 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 507 <div class="card-body"> 508 <form @submit.prevent="await diagnose()"> 509 <label> 510 Enter an atproto handle, DID, or HTTPS PDS URL 511 <input 512 class="input" 513 x-model="identifier" 514 :disabled="identifierLoading" 515 autofocus 516 /> 517 </label> 518 </form> 519 </div> 520 </div> 521 522 <template x-if="identifierError"> 523 <p>uh oh: <span x-text="identifierError"></span></p> 524 </template> 525 526 <template x-if="pds != null"> 527 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 528 <div class="card-body"> 529 <h2 class="card-title"> 530 <span class="badge badge-secondary">PDS</span> 531 <span x-text="pds"></span> 532 </h2> 533 534 <div 535 x-data="pdsCheck(pds)" 536 x-init="$watch('pds', v => update(v))" 537 > 538 <h3 class="text-lg mt-3"> 539 Server 540 <span 541 x-show="description !== null" 542 class="badge badge-sm badge-soft badge-success" 543 >online</span> 544 </h3> 545 <p x-show="loadingDesc">Loading&hellip;</p> 546 <p x-show="error" class="text-warning" x-text="error"></p> 547 <template x-if="description !== null"> 548 <div> 549 <div class="overflow-x-auto"> 550 <table class="table table-xs"> 551 <tbody> 552 <tr> 553 <td class="text-sm"> 554 Open registration: 555 <span 556 x-text="!description.inviteCodeRequired" 557 ></span> 558 </td> 559 </tr> 560 <tr> 561 <td class="text-sm"> 562 Version: 563 <code 564 class="text-xs" 565 x-text="version" 566 ></code> 567 </td> 568 </tr> 569 </tbody> 570 </table> 571 </div> 572 <h4 class="font-bold"> 573 Accounts 574 </h4> 575 <div class="overflow-x-auto overflow-y-auto max-h-26"> 576 <table class="table table-xs"> 577 <tbody> 578 <template x-for="account in accounts"> 579 <tr> 580 <td> 581 <code> 582 <a 583 href="#" 584 class="link" 585 x-text="account.did" 586 @click.prevent="goto(account.did)" 587 ></a> 588 </code> 589 </td> 590 <td> 591 <span 592 x-show="account.active" 593 class="badge badge-sm badge-soft badge-success" 594 > 595 active 596 </span> 597 <span 598 x-show="!account.active" 599 x-text="account.status" 600 class="badge badge-sm badge-soft badge-warning" 601 ></span> 602 </td> 603 <td 604 x-data="didToHandle(account.did)" 605 x-intersect:enter.once="load" 606 > 607 <span x-show="loading">Loading&hellip;</span> 608 <span x-show="error !== null" x-text="error"></span> 609 <a 610 href="#" 611 class="link" 612 @click.prevent="goto(handle)" 613 x-show="handle !== null" 614 x-text="`@${handle}`" 615 ></a> 616 </td> 617 </tr> 618 </template> 619 <template x-if="!loadingDesc && !accountsComplete"> 620 <tr> 621 <td colspan="2" class="text-xs text-warning-content"> 622 (more accounts not shown) 623 </td> 624 </tr> 625 </template> 626 </tbody> 627 </table> 628 </div> 629 </div> 630 </template> 631 </div> 632 633 <h3 class="text-lg mt-3">Relay host status</h3> 634 <div class="overflow-x-auto overflow-y-auto max-h-33"> 635 <table class="table table-xs"> 636 <tbody> 637 <template x-for="relay in window.relays"> 638 <tr 639 x-data="relayCheckHost(pds, relay)" 640 x-init="$watch('pds', pds => check(pds, relay))" 641 > 642 <td x-text="relay.name" class="text-sm"></td> 643 <td> 644 <template x-if="loading"> 645 <em>loading&hellip;</em> 646 </template> 647 <template x-if="error"> 648 <span 649 x-text="error" 650 class="text-xs text-warning" 651 ></span> 652 </template> 653 <template x-if="status"> 654 <span 655 x-text="status" 656 class="badge badge-sm" 657 :class="status === 'active' && 'badge-soft badge-success'" 658 ></span> 659 </template> 660 </td> 661 <td> 662 <div x-show="status !== 'active'"> 663 <button 664 x-show="reqCrawlStatus !== 'done'" 665 class="btn btn-xs btn-ghost whitespace-nowrap" 666 :disabled="reqCrawlStatus === 'loading'" 667 @click="requestCrawl(pds, relay)" 668 > 669 request crawl 670 </button> 671 <span 672 x-show="reqCrawlError !== null" 673 x-text="reqCrawlError" 674 class="text-xs text-warning" 675 ></span> 676 <button 677 x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 678 class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 679 @click="check" 680 > 681 refresh 682 </button> 683 </div> 684 </td> 685 </tr> 686 </template> 687 </tbody> 688 </table> 689 </div> 690 </div> 691 </div> 692 </template> 693 694 <template x-if="did != null"> 695 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 696 <div class="card-body"> 697 <h2 class="card-title"> 698 <span class="badge badge-secondary">DID</span> 699 <code x-text="did"></code> 700 </h2> 701 <template x-if="pds != null"> 702 <div x-data="didRepoState(did, pds)"> 703 <h3 class="text-lg mt-3"> 704 Repo 705 <span 706 x-show="state && state.active" 707 class="badge badge-sm badge-soft badge-success" 708 >active</span> 709 </h3> 710 <div class="overflow-x-auto"> 711 <table class="table table-xs"> 712 <tbody> 713 <tr> 714 <td class="text-sm"> 715 Rev: 716 <code x-text="state && state.rev"></code> 717 </td> 718 </tr> 719 <tr> 720 <td class="text-sm"> 721 PDS: 722 <a 723 href="#" 724 class="link" 725 @click.prevent="goto(pds)" 726 x-text="pds" 727 ></a> 728 </td> 729 </tr> 730 </tbody> 731 </table> 732 </div> 733 734 <h3 class="text-lg mt-3"> 735 Relay repo status 736 </h3> 737 <div class="overflow-x-auto overflow-y-auto max-h-33"> 738 <table class="table table-xs"> 739 <tbody> 740 <template x-for="relay in window.relays"> 741 <tr 742 x-data="relayCheckRepo(did, relay)" 743 x-init="$watch('pds', pds => check(did, relay))" 744 > 745 <td x-text="relay.name" class="text-sm"></td> 746 <template x-if="loading"> 747 <td> 748 <em>loading&hellip;</em> 749 </td> 750 </template> 751 <template x-if="error"> 752 <td 753 x-text="error" 754 class="text-xs text-warning" 755 ></td> 756 </template> 757 <template x-if="status"> 758 <td> 759 <span 760 x-show="status.active" 761 class="badge badge-sm badge-soft badge-success" 762 > 763 active 764 </span> 765 <span 766 x-show="!status.active" 767 x-text="status.status" 768 class="badge badge-sm badge-soft badge-warning" 769 ></span> 770 </td> 771 </template> 772 <template x-if="status"> 773 <td> 774 <code 775 x-text="status.rev" 776 class="badge badge-sm badge-soft" 777 :class="status && state && (status.rev >= state.rev) ? 'badge-success' : 'badge-warning'" 778 ></code> 779 </td> 780 </template> 781 </tr> 782 </template> 783 </tbody> 784 </table> 785 </div> 786 787 <template x-if="did.startsWith('did:plc:')"> 788 <div x-data="pdsHistory(did, pds)"> 789 <h3 class="text-lg mt-3"> 790 PLC PDS history 791 </h3> 792 <div class="overflow-x-auto"> 793 <table class="table table-xs"> 794 <tbody> 795 <template x-if="loading"> 796 <tr> 797 <td>Loading&hellip;</td> 798 </tr> 799 </template> 800 <template x-if="error"> 801 <tr> 802 <td>Error: <span x-text="error"></span></td> 803 </tr> 804 </template> 805 <template x-if="!loading && !error && history.length === 0"> 806 <tr> 807 <td class="text-sm"> 808 <em>no previous PDS</em> 809 </td> 810 </tr> 811 </template> 812 <template x-for="event in history"> 813 <tr x-data="didRepoState(did, event.pds)"> 814 <td> 815 <code x-text="event.date.split('T')[0]"></code> 816 </td> 817 <td> 818 <a 819 href="#" 820 class="link" 821 @click.prevent="goto(event.pds)" 822 x-text="event.pds" 823 ></a> 824 </td> 825 <template x-if="event.current"> 826 <td> 827 <span 828 x-show="state && !state.active" 829 x-text="state && state.status" 830 class="badge badge-sm badge-soft badge-warning" 831 ></span> 832 <span 833 x-show="state && state.active" 834 class="badge badge-sm badge-soft badge-success" 835 >current</span> 836 </td> 837 </template> 838 <template x-if="!event.current"> 839 <td> 840 <span 841 x-show="state && !state.active" 842 x-text="state && state.status" 843 class="badge badge-sm badge-soft badge-success" 844 ></span> 845 <span 846 x-show="state && state.active" 847 class="badge badge-sm badge-soft badge-warning" 848 >active</span> 849 </td> 850 </template> 851 </tr> 852 </template> 853 </tbody> 854 </table> 855 </div> 856 </div> 857 </template> 858 </div> 859 </template> 860 </div> 861 </div> 862 </template> 863 864 <template x-if="handle !== null"> 865 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 866 <div 867 x-data="checkHandle(handle)" 868 x-init="$watch('handle', h => updateHandle(h))" 869 class="card-body" 870 > 871 <h2 class="card-title"> 872 <span class="badge badge-secondary">Handle</span> 873 <span x-text="handle"></span> 874 </h2> 875 876 <h3 class="text-lg mt-3"> 877 Resolution 878 </h3> 879 <p x-show="loading" class="text-i">Loading&hellip;</p> 880 <div x-show="!loading" class="overflow-x-auto"> 881 <table class="table table-xs"> 882 <tbody> 883 <tr> 884 <td class="text-sm">DNS</td> 885 <td class="text-sm"> 886 <code x-text="dnsDid"></code> 887 </td> 888 <td> 889 <div 890 class="badge badge-sm badge-soft badge-neutral" 891 x-show="dnsErr !== null" 892 x-text="dnsErr" 893 ></div> 894 </td> 895 </tr> 896 <tr> 897 <td class="text-sm">Http</td> 898 <td class="text-sm"> 899 <code x-text="httpDid"></code> 900 </td> 901 <td> 902 <div 903 class="badge badge-sm badge-soft badge-neutral" 904 x-show="httpErr !== null" 905 x-text="httpErr" 906 ></div> 907 </td> 908 </tr> 909 </tbody> 910 </table> 911 </div> 912 913 <template x-if="did !== null && did.startsWith('did:plc:')"> 914 915 <div x-data="handleHistory(did, handle)"> 916 <h3 class="text-lg mt-3"> 917 PLC handle history 918 </h3> 919 <div class="overflow-x-auto"> 920 <table class="table table-xs"> 921 <tbody> 922 <template x-if="loading"> 923 <tr> 924 <td>Loading&hellip;</td> 925 </tr> 926 </template> 927 <template x-if="error"> 928 <tr> 929 <td>Error: <span x-text="error"></span></td> 930 </tr> 931 </template> 932 <template x-if="!loading && !error && history.length === 0"> 933 <tr> 934 <td class="text-sm"> 935 <em>no previous handle</em> 936 </td> 937 </tr> 938 </template> 939 <template x-for="event in history"> 940 <tr> 941 <td> 942 <code x-text="event.date.split('T')[0]"></code> 943 </td> 944 <td> 945 <a 946 href="#" 947 class="link" 948 @click.prevent="goto(event.handle)" 949 x-text="event.handle" 950 ></a> 951 </td> 952 </tr> 953 </template> 954 </tbody> 955 </table> 956 </div> 957 </div> 958 959 </template> 960 </div> 961 </div> 962 </template> 963 </div> 964 </div> 965 </body> 966</html>