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