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