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