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

Configure Feed

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

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