Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width"/>
6 <title>atproto PDS & account debugger</title>
7 <meta name="description" content="Quick diagnostics for PDS hosts, handles, relay connections, handles, DIDs, ..." />
8
9 <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
10 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
11 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/>
12 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
13
14 <script type="module">
15 import {
16 Client,
17 ClientResponseError,
18 ok,
19 simpleFetchHandler,
20 } from 'https://esm.sh/@atcute/client@4.1.1';
21 import {
22 DohJsonHandleResolver,
23 WellKnownHandleResolver,
24 } from 'https://esm.sh/@atcute/identity-resolver@1.2.1';
25
26 window.SimpleQuery = service => {
27 const client = new Client({ handler: simpleFetchHandler({ service }) });
28 return (...args) => ok(client.get(...args));
29 };
30 window.SimpleProc = service => {
31 const client = new Client({ handler: simpleFetchHandler({ service }) });
32 return (...args) => ok(client.post(...args));
33 };
34 window.isXrpcErr = e => e instanceof ClientResponseError;
35
36 window.isBeforeNow = iso => new Date(iso) < new Date();
37
38 window.dnsResolver = new DohJsonHandleResolver({
39 dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query',
40 });
41 window.httpResolver = new WellKnownHandleResolver();
42
43 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue');
44 window.relays = [
45 {
46 name: 'Bluesky',
47 icon: './icons/bsky-favicon.png',
48 hostname: 'bsky.network',
49 note: 'current',
50 missingApis: {
51 ['com.atproto.sync.getHostStatus']: 'missing API (old relay code)',
52 ['com.atproto.sync.getRepoStatus']: 'missing API (old relay code)',
53 },
54 },
55 {
56 name: 'Microcosm Montreal',
57 icon: './icons/microcosm-favicon.png',
58 hostname: 'relay.fire.hose.cam',
59 },
60 {
61 name: 'Microcosm France',
62 icon: './icons/microcosm-favicon.png',
63 hostname: 'relay3.fr.hose.cam',
64 },
65 {
66 name: 'Upcloud',
67 icon: 'https://upcloud.com/media/android-chrome-512x512-2-150x150.png',
68 hostname: 'relay.upcloud.world',
69 },
70 {
71 name: 'Blacksky',
72 icon: 'https://blacksky.community/static/favicon-32x32.png',
73 hostname: 'atproto.africa',
74 missingApis: {
75 ['com.atproto.sync.getHostStatus']: 'API not yet deployed',
76 ['com.atproto.sync.getRepoStatus']: 'API not implemented',
77 },
78 },
79 {
80 name: 'Bluesky East',
81 icon: './icons/bsky-favicon.png',
82 note: 'future',
83 hostname: 'relay1.us-east.bsky.network',
84 },
85 {
86 name: 'Bluesky West',
87 icon: './icons/bsky-favicon.png',
88 note: 'future',
89 hostname: 'relay1.us-west.bsky.network',
90 },
91 ];
92
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…</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…</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…</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…</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…</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…</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…</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…</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…</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…</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…</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>