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

Compare changes

Choose any two refs to compare.

icons/bsky-favicon.png

This is a binary file and will not be displayed.

icons/microcosm-favicon.png

This is a binary file and will not be displayed.

+1026 -197
index.html
··· 3 3 <head> 4 4 <meta charset="utf-8"> 5 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> 6 10 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 7 11 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/> 8 12 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> ··· 17 21 import { 18 22 DohJsonHandleResolver, 19 23 WellKnownHandleResolver, 20 - } from 'https://esm.sh/@atcute/identity-resolver@1.2.0'; 24 + } from 'https://esm.sh/@atcute/identity-resolver@1.2.1'; 21 25 22 26 window.SimpleQuery = service => { 23 27 const client = new Client({ handler: simpleFetchHandler({ service }) }); ··· 28 32 return (...args) => ok(client.post(...args)); 29 33 }; 30 34 window.isXrpcErr = e => e instanceof ClientResponseError; 35 + 36 + window.isBeforeNow = iso => new Date(iso) < new Date(); 31 37 32 38 window.dnsResolver = new DohJsonHandleResolver({ 33 39 dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query', ··· 37 43 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue'); 38 44 window.relays = [ 39 45 { 40 - name: 'Bluesky production', 46 + name: 'Bluesky', 47 + icon: './icons/bsky-favicon.png', 41 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 + }, 42 54 }, 43 55 { 44 - name: 'Bluesky sync1.1 East', 45 - hostname: 'relay1.us-east.bsky.network', 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', 46 64 }, 47 65 { 48 - name: 'Bluesky sync1.1 West', 49 - hostname: 'relay1.us-west.bsky.network', 66 + name: 'Upcloud', 67 + icon: 'https://upcloud.com/media/android-chrome-512x512-2-150x150.png', 68 + hostname: 'relay.upcloud.world', 50 69 }, 51 70 { 52 71 name: 'Blacksky', 72 + icon: 'https://blacksky.community/static/favicon-32x32.png', 53 73 hostname: 'atproto.africa', 74 + missingApis: { 75 + ['com.atproto.sync.getHostStatus']: 'API not yet deployed', 76 + ['com.atproto.sync.getRepoStatus']: 'API not implemented', 77 + }, 54 78 }, 55 79 { 56 - name: 'Microcosm Montreal', 57 - hostname: 'relay.fire.hose.cam', 80 + name: 'Bluesky East', 81 + icon: './icons/bsky-favicon.png', 82 + note: 'future', 83 + hostname: 'relay1.us-east.bsky.network', 58 84 }, 59 85 { 60 - name: 'Microcosm France', 61 - hostname: 'relay3.fr.hose.cam', 86 + name: 'Bluesky West', 87 + icon: './icons/bsky-favicon.png', 88 + note: 'future', 89 + hostname: 'relay1.us-west.bsky.network', 62 90 }, 63 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; 64 113 </script> 65 114 115 + <style> 116 + body:not(.ready) .hide-until-ready, 117 + body.ready .show-until-ready { 118 + display: none; 119 + } 120 + </style> 121 + 66 122 <script> 67 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 + 68 130 Alpine.data('debug', () => ({ 69 131 // form input 70 132 identifier: '', ··· 117 179 } 118 180 } 119 181 } else { 120 - this.handle = this.identifier; 182 + this.handle = this.identifier.toLowerCase(); 121 183 let data; 122 184 try { 123 185 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 124 - params: { identifier: this.identifier }, 186 + params: { identifier: this.identifier.toLowerCase() }, 125 187 }); 126 188 this.did = data.did; 127 189 this.pds = data.pds; ··· 146 208 description: null, 147 209 accounts: [], 148 210 accountsComplete: false, 211 + version: null, 149 212 150 213 async init() { 151 214 await this.update(pds); ··· 156 219 this.error = null; 157 220 this.description = null; 158 221 this.accounts = []; 222 + this.accountsComplete = false; 223 + this.version = null; 224 + 225 + if (!pds) { 226 + this.loadingDesc = false; 227 + return; 228 + } 229 + 159 230 let query = window.SimpleQuery(pds); 160 231 try { 161 232 this.description = await query('com.atproto.server.describeServer'); ··· 167 238 console.error(e); 168 239 } 169 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 + } 170 253 let accountsRes; 171 254 try { 172 255 accountsRes = await query('com.atproto.sync.listRepos', { 173 - params: { limit: 7 }, 256 + params: { limit: 100 }, 174 257 }); 175 258 this.accounts = accountsRes.repos; 176 - this.accountsComplete == !accountsRes.cursor; 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 + 177 276 } catch (e) { 178 277 if (window.isXrpcErr(e)) { 179 278 this.error = e.error; ··· 190 289 loading: false, 191 290 error: null, 192 291 status: null, 292 + expectedErrorInfo: null, 193 293 reqCrawlStatus: null, 194 294 reqCrawlError: null, 195 295 ··· 201 301 this.loading = true; 202 302 this.error = null; 203 303 this.status = null; 304 + this.expectedError = false; 204 305 const query = window.SimpleQuery(`https://${relay.hostname}`); 205 306 const hostname = pds.split('://')[1]; 206 307 let data; ··· 210 311 }); 211 312 this.status = data.status; 212 313 } catch(e) { 213 - if (window.isXrpcErr(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)) { 214 318 this.error = e.error; 215 319 } else { 216 320 this.error = 'Failed to check (see console)'; ··· 272 376 this.loading = false; 273 377 }, 274 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 + })); 275 661 }) 276 662 </script> 277 663 </head> 278 - <body x-data="debug"> 279 - <div class="hero bg-base-200"> 664 + <body x-data="debug" class="bg-base-200"> 665 + <div class="hero bg-base-200 p-8"> 280 666 <div class="hero-content flex-col"> 281 - <h1>PDS Debugger</h1> 282 - 283 - <p>Work in progress!</p> 284 - <details class="text-xs"> 285 - <summary>Would be nice</summary> 286 - <ul> 287 - <li>anything that actually works</li> 288 - <li>firehose listener for missing pds events</li> 289 - <li>jetstream listener for missing pds events</li> 290 - <li>check relays for account status</li> 291 - <li>check relays for pds state</li> 292 - <li>plc: check old pds hosts for active account state</li> 293 - </ul> 294 - </details> 295 - <details class="text-xs"> 296 - <summary>Limitations</summary> 297 - <ul> 298 - <li>it's all client-side</li> 299 - </ul> 300 - </details> 301 - 302 - <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 303 - <div class="card-body"> 304 - <form @submit.prevent="await diagnose()"> 305 - <label> 306 - Enter an atproto handle, DID, or HTTPS PDS URL 307 - <input 308 - class="input" 309 - x-model="identifier" 310 - :disabled="identifierLoading" 311 - autofocus 312 - /> 313 - </label> 314 - </form> 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> 315 686 </div> 316 - </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> 317 695 318 - <template x-if="identifierError"> 319 - <p>uh oh: <span x-text="identifierError"></span></p> 320 - </template> 696 + <template x-if="pds != null"> 321 697 322 - <template x-if="pds != null"> 323 - <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 324 - <div class="card-body"> 325 - <h2 class="card-title"> 326 - <span class="badge badge-secondary">PDS</span> 327 - <span x-text="pds"></span> 328 - </h2> 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> 329 704 330 - <div 331 - x-data="pdsCheck(pds)" 332 - x-init="$watch('pds', v => update(v))" 333 - > 334 - <h3 class="text-lg"> 335 - Server 336 - <span 337 - x-show="description !== null" 338 - class="badge badge-sm badge-soft badge-success" 339 - >online</span> 340 - </h3> 341 - <p x-show="loadingDesc">Loading&hellip;</p> 342 - <p x-show="error" class="text-warning" x-text="error"></p> 343 - <template x-if="description !== null"> 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> 344 720 <div class="overflow-x-auto"> 345 721 <table class="table table-xs"> 346 722 <tbody> 347 723 <tr> 348 - <td class="text-sm">Open registration</td> 349 - <td 350 - class="text-sm" 351 - x-text="!description.inviteCodeRequired" 352 - ></td> 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> 353 739 </tr> 354 740 </tbody> 355 741 </table> 356 - <h4 class="font-bold"> 357 - Accounts 358 - </h4> 742 + </div> 743 + <h4 class="font-bold"> 744 + Accounts 745 + </h4> 746 + <div class="overflow-x-auto overflow-y-auto max-h-26"> 359 747 <table class="table table-xs"> 360 748 <tbody> 361 749 <template x-for="account in accounts"> ··· 383 771 class="badge badge-sm badge-soft badge-warning" 384 772 ></span> 385 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> 386 788 </tr> 387 789 </template> 388 - <template x-if="!accountsComplete"> 790 + <template x-if="!loadingDesc && !accountsComplete"> 389 791 <tr> 390 - <td colspan="2" class="text-sm text-warning-content"> 391 - (account list clipped) 792 + <td colspan="2" class="text-xs text-warning-content"> 793 + (more accounts not shown) 392 794 </td> 393 795 </tr> 394 796 </template> 395 797 </tbody> 396 798 </table> 397 799 </div> 398 - </template> 399 - </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> 400 961 401 - <h3 class="text-lg">Relay host status</h3> 402 - <div class="overflow-x-auto"> 403 - <table class="table table-xs"> 404 - <tbody> 405 - <template x-for="relay in window.relays"> 406 - <tr 407 - x-data="relayCheckHost(pds, relay)" 408 - x-init="$watch('pds', pds => check(pds, relay))" 409 - > 410 - <td x-text="relay.name" class="text-sm"></td> 411 - <td> 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> 412 988 <template x-if="loading"> 413 - <em>loading&hellip;</em> 989 + <td> 990 + <em>loading&hellip;</em> 991 + </td> 414 992 </template> 415 993 <template x-if="error"> 416 - <span 417 - x-text="error" 418 - class="text-xs text-warning" 419 - ></span> 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> 420 1009 </template> 421 1010 <template x-if="status"> 422 - <span 423 - x-text="status" 424 - class="badge badge-sm" 425 - :class="status === 'active' && 'badge-soft badge-success'" 426 - ></span> 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> 427 1204 </template> 428 - </td> 429 - <td> 430 - <div x-show="status !== 'active'"> 431 - <button 432 - x-show="reqCrawlStatus !== 'done'" 433 - class="btn btn-xs btn-ghost whitespace-nowrap" 434 - :disabled="reqCrawlStatus === 'loading'" 435 - @click="requestCrawl(pds, relay)" 436 - > 437 - request crawl 438 - </button> 439 - <span 440 - x-show="reqCrawlError !== null" 441 - x-text="reqCrawlError" 442 - class="text-xs text-warning" 443 - ></span> 444 - <button 445 - x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 446 - class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 447 - @click="check" 448 - > 449 - refresh 450 - </button> 451 - </div> 452 - </td> 453 - </tr> 454 - </template> 455 - </tbody> 456 - </table> 1205 + </tbody> 1206 + </table> 1207 + </div> 1208 + </div> 1209 + </template> 457 1210 </div> 458 - </div> 1211 + </template> 459 1212 </div> 460 - </template> 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> 461 1227 462 - <template x-if="did != null"> 463 - <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 464 - <div class="card-body"> 465 - <h2 class="card-title"> 466 - <span class="badge badge-secondary">DID</span> 467 - <code x-text="did"></code> 468 - </h2> 469 - <p>(wip)</p> 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> 470 1263 </div> 471 - </div> 472 - </template> 473 1264 474 - <template x-if="handle != null"> 475 - <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 476 - <div 477 - x-data="checkHandle(handle)" 478 - x-init="$watch('handle', h => updateHandle(h))" 479 - class="card-body" 480 - > 481 - <h2 class="card-title"> 482 - <span class="badge badge-secondary">Handle</span> 483 - <span x-text="handle"></span> 484 - </h2> 485 - <p x-show="loading" class="text-i">Loading&hellip;</p> 486 - <div x-show="!loading" class="overflow-x-auto"> 487 - <table class="table table-xs"> 488 - <tbody> 489 - <tr> 490 - <td class="text-sm">DNS</td> 491 - <td class="text-sm"> 492 - <code x-text="dnsDid"></code> 493 - </td> 494 - <td> 495 - <div 496 - class="badge badge-sm badge-soft badge-neutral" 497 - x-show="dnsErr !== null" 498 - x-text="dnsErr" 499 - ></div> 500 - </td> 501 - </tr> 502 - <tr> 503 - <td class="text-sm">Http</td> 504 - <td class="text-sm"> 505 - <code x-text="httpDid"></code> 506 - </td> 507 - <td> 508 - <div 509 - class="badge badge-sm badge-soft badge-neutral" 510 - x-show="httpErr !== null" 511 - x-text="httpErr" 512 - ></div> 513 - </td> 514 - </tr> 515 - </tbody> 516 - </table> 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> 517 1309 </div> 518 - </div> 1310 + 1311 + </template> 519 1312 </div> 520 - </template> 521 - </div> 1313 + </div> 1314 + </template> 522 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 + 523 1352 </body> 524 1353 </html>
+5
readme.md
··· 1 + # PDS Debugger 2 + 3 + https://debug.hose.cam 4 + 5 + Diagnostics for atproto PDS hosts, DIDs, and handles
+13
useful-accounts.txt
··· 1 + some accounts that show things useful for testing the debugger 2 + 3 + 4 + Labels 5 + 6 + - did:plc:bnwrgnvwkg2n5cbvk4xodb3h | !hide | no other labels 7 + - did:plc:qhl3vg5tmwey536z2fil2lrh | !hide | from moderation-tr.bsky.app 8 + - did:plc:fsmaoqqnm6knqh4cuphb4jow | !hide, ~!takedown | takedown negated 9 + - did:plc:iv3yod6zf2j4zaakq6qyiz46 | !takedown | 10 + - did:plc:nwrcwcrhpkgrqqvkg3lmaqky | ~needs-review, ~!takedown | both negated 11 + - did:plc:2tinwgqvf4asiwh36ii6ko7l | needs-review | expired 12 + - did:plc:5plqrpw3x6j5wzaosssqams7 | spam | no other labels 13 + - did:plc:tqww7jdpqx5tb3w435fugmxi | intolerant |