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