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