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 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…</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…</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…</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…</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…</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…</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…</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…</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…</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…</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…</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>