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;
147 let data;
148 try {
149 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
150 params: { identifier: this.identifier },
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 this.accountsComplete == !accountsRes.cursor;
224 } catch (e) {
225 if (window.isXrpcErr(e)) {
226 this.error = e.error;
227 } else {
228 this.error = 'Failed to reach (see console)';
229 console.error(e);
230 }
231 }
232 this.loadingDesc = false;
233 },
234 }));
235
236 Alpine.data('relayCheckHost', (pds, relay) => ({
237 loading: false,
238 error: null,
239 status: null,
240 expectedErrorInfo: null,
241 reqCrawlStatus: null,
242 reqCrawlError: null,
243
244 async init() {
245 await this.check(pds, relay);
246 },
247
248 async check(pds, relay) {
249 this.loading = true;
250 this.error = null;
251 this.status = null;
252 this.expectedError = false;
253 const query = window.SimpleQuery(`https://${relay.hostname}`);
254 const hostname = pds.split('://')[1];
255 let data;
256 try {
257 data = await query('com.atproto.sync.getHostStatus', {
258 params: { hostname },
259 });
260 this.status = data.status;
261 } catch(e) {
262 if (relay.missingApis['com.atproto.sync.getHostStatus']) {
263 this.error = 'Can\'t check';
264 this.expectedErrorInfo = relay.missingApis['com.atproto.sync.getHostStatus'];
265 } else if (window.isXrpcErr(e)) {
266 this.error = e.error;
267 } else {
268 this.error = 'Failed to check (see console)';
269 console.error(e);
270 }
271 }
272 this.loading = false;
273 this.reqCrawlStatus = null;
274 this.reqCrawlError = null;
275 },
276
277 async requestCrawl(pds, relay) {
278 this.reqCrawlStatus = "loading";
279 const proc = window.SimpleProc(`https://${relay.hostname}`);
280 const hostname = pds.split('://')[1];
281 let data;
282 try {
283 data = await proc('com.atproto.sync.requestCrawl', {
284 input: { hostname },
285 });
286 } catch (e) {
287 if (window.isXrpcErr(e)) {
288 this.reqCrawlError = e.error;
289 } else {
290 this.reqCrawlError = 'failed (see console)';
291 console.error(e);
292 }
293 }
294 this.reqCrawlStatus = "done";
295 },
296 }));
297
298 Alpine.data('checkHandle', handle => ({
299 loading: false,
300 dnsDid: null,
301 dnsErr: null,
302 httpDid: null,
303 httpErr: null,
304
305 async init() {
306 await this.updateHandle(handle);
307 },
308 async updateHandle(handle) {
309 this.loading = true;
310 this.dnsDid = null;
311 this.dnsErr = null;
312 this.httpDid = null;
313 this.httpErr = null;
314 try {
315 this.dnsDid = await window.dnsResolver.resolve(handle);
316 } catch (e) {
317 this.dnsErr = e.name;
318 }
319 try {
320 this.httpDid = await window.httpResolver.resolve(handle);
321 } catch (e) {
322 this.httpErr = e.name;
323 }
324 this.loading = false;
325 },
326 }));
327
328 Alpine.data('didToHandle', did => ({
329 loading: false,
330 error: null,
331 handle: null,
332 async load() {
333 loading = true;
334 error = null;
335 handle = null;
336 let data;
337 try {
338 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
339 params: { identifier: did },
340 });
341 this.handle = data.handle;
342 } catch (e) {
343 if (window.isXrpcErr(e)) {
344 this.error = e.error;
345 } else {
346 this.error = 'failed (see console)';
347 console.error(e);
348 }
349 }
350 loading = false;
351 }
352 }));
353
354 Alpine.data('didRepoState', (did, pds) => ({
355 loading: false,
356 error: null,
357 state: null,
358
359 async init() {
360 await this.checkRepoState(did, pds);
361 },
362 async checkRepoState(did, pds) {
363 this.loading = true;
364 this.error = null;
365 this.state = null;
366
367 if (!did || !pds) {
368 this.loading = false;
369 return;
370 }
371 const query = window.SimpleQuery(pds);
372 try {
373 this.state = await query('com.atproto.sync.getRepoStatus', {
374 params: { did },
375 });
376 } catch (e) {
377 if (window.isXrpcErr(e)) {
378 this.error = e.error;
379 } else {
380 this.error = 'failed (see console)';
381 console.error(e);
382 }
383 }
384 this.loading = false;
385 },
386 }));
387
388 Alpine.data('repoSize', () => ({
389 lading: false,
390 error: null,
391 size: null,
392
393 async loadRepoForSize(did, pds) {
394 this.loading = true;
395
396 if (!did || !pds) {
397 this.loading = false;
398 return;
399 }
400 const query = window.SimpleQuery(pds);
401 try {
402 const res = await query('com.atproto.sync.getRepo', {
403 params: { did },
404 as: 'blob',
405 });
406 let bytes = res.size;
407 let mbs = bytes / Math.pow(2, 20);
408 this.size = mbs.toFixed(1);
409 } catch (e) {
410 if (window.isXrpcErr(e)) {
411 this.error = e.error;
412 } else {
413 this.error = 'failed (see console)';
414 console.error(e);
415 }
416 }
417 this.loading = false;
418 },
419 }));
420
421 Alpine.data('relayCheckRepo', (did, relay) => ({
422 loading: false,
423 error: null,
424 status: null,
425 expectedErrorInfo: null,
426
427 async init() {
428 await this.check(did, relay);
429 },
430
431 async check(did, relay) {
432 this.loading = true;
433 this.error = null;
434 this.status = null;
435 this.expectedErrorInfo = null;
436
437 const query = window.SimpleQuery(`https://${relay.hostname}`);
438 try {
439 this.status = await query('com.atproto.sync.getRepoStatus', {
440 params: { did },
441 });
442 } catch(e) {
443 if (relay.missingApis['com.atproto.sync.getRepoStatus']) {
444 this.error = 'Can\'t check';
445 this.expectedErrorInfo = relay.missingApis['com.atproto.sync.getRepoStatus'];
446 } else if (window.isXrpcErr(e)) {
447 this.error = e.error;
448 } else {
449 this.error = 'Failed to check (see console)';
450 console.error(e);
451 }
452 }
453
454 this.loading = false;
455 },
456
457 revStatus(repoRev) {
458 if (
459 !repoRev ||
460 !(this.status && this.status.rev)
461 ) return null;
462
463 if (this.status.rev < repoRev) return 'behind';
464 if (this.status.rev === repoRev) return 'current';
465 if (this.status.rev > repoRev) return 'ahead';
466 }
467 }));
468
469 Alpine.data('pdsHistory', (did, currentPds) => ({
470 loading: false,
471 error: null,
472 history: [],
473
474 async init() {
475 this.loading = true;
476 this.error = null;
477 this.history = [];
478 try {
479 const res = await fetch(`https://plc.directory/${did}/log/audit`);
480 if (res.ok) {
481 const log = await res.json();
482 let prev = null;
483 for (op of log) {
484 let opPds = null;
485 const services = op.operation.services;
486 if (services) {
487 const app = services.atproto_pds;
488 if (app) {
489 opPds = app.endpoint;
490 }
491 }
492 if (opPds === prev) continue;
493 prev = opPds;
494 this.history.push({
495 pds: opPds,
496 date: op.createdAt,
497 });
498 }
499 this.history.reverse();
500 if (this.history[0]) this.history[0].current = true;
501 } else {
502 this.error = `${res.status}: ${await res.text()}`;
503 }
504 } catch (e) {
505 this.error = 'failed to get history';
506 console.error(e);
507 }
508 this.loading = false;
509 },
510 }));
511
512 Alpine.data('handleHistory', (did, currentHandle) => ({
513 loading: false,
514 error: null,
515 history: [],
516
517 async init() {
518 this.loading = true;
519 this.error = null;
520 this.history = [];
521 try {
522 const res = await fetch(`https://plc.directory/${did}/log/audit`);
523 if (res.ok) {
524 const log = await res.json();
525 let prev = null;
526 for (op of log) {
527 let opHandle = null;
528 if (op.operation.alsoKnownAs) {
529 for (aka of op.operation.alsoKnownAs) {
530 if (aka.startsWith("at://")) {
531 opHandle = aka.slice("at://".length);
532 break;
533 }
534 }
535 }
536 if (opHandle === prev) continue;
537 prev = opHandle;
538 this.history.push({
539 handle: opHandle,
540 date: op.createdAt,
541 });
542 }
543 this.history.reverse();
544 } else {
545 this.error = `${res.status}: ${await res.text()}`;
546 }
547 } catch (e) {
548 this.error = 'failed to get history';
549 console.error(e);
550 }
551 this.loading = false;
552 },
553 }));
554 })
555 </script>
556 </head>
557 <body x-data="debug">
558 <div class="hero bg-base-200">
559 <div class="hero-content flex-col">
560 <h1>PDS Debugger</h1>
561
562 <p class="text-sm">Work in progress!</p>
563 <details class="text-xs">
564 <summary>Future features</summary>
565 <ul class="list-disc pl-4">
566 <li>firehose & jetstream listeners</li>
567 </ul>
568 </details>
569 <details class="text-xs">
570 <summary>Limitations</summary>
571 <ul class="list-disc pl-4">
572 <li>The Bluesky production relay at <code>bsky.network</code> runs the old bgs implementation, and is missing many relay XRPC endpoints.</li>
573 <li>The Blacksky relay at <code>atproto.africa</code> runs an independent implementation, and is also missing many relay XRPC endpoints.</li>
574 <li>All diagnostics run in your browser, so servers that don't enable CORS (some PDS hosts, Upcloud's relay) will fail tests.</li>
575 </ul>
576 </details>
577
578 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
579 <div class="card-body">
580 <form @submit.prevent="await diagnose()">
581 <label>
582 Enter an atproto handle, DID, or HTTPS PDS URL
583 <input
584 class="input"
585 x-model="identifier"
586 :disabled="identifierLoading"
587 autofocus
588 />
589 </label>
590 </form>
591 </div>
592 </div>
593
594 <template x-if="identifierError">
595 <p>uh oh: <span x-text="identifierError"></span></p>
596 </template>
597
598 <template x-if="pds != null">
599 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
600 <div class="card-body">
601 <h2 class="card-title">
602 <span class="badge badge-secondary">PDS</span>
603 <span x-text="pds"></span>
604 </h2>
605
606 <div
607 x-data="pdsCheck(pds)"
608 x-init="$watch('pds', v => update(v))"
609 >
610 <h3 class="text-lg mt-3">
611 Server
612 <span
613 x-show="description !== null"
614 class="badge badge-sm badge-soft badge-success"
615 >online</span>
616 </h3>
617 <p x-show="loadingDesc">Loading…</p>
618 <p x-show="error" class="text-warning" x-text="error"></p>
619 <template x-if="description !== null">
620 <div>
621 <div class="overflow-x-auto">
622 <table class="table table-xs">
623 <tbody>
624 <tr>
625 <td class="text-sm">
626 Open registration:
627 <span
628 x-text="!description.inviteCodeRequired"
629 ></span>
630 </td>
631 </tr>
632 <tr>
633 <td class="text-sm">
634 Version:
635 <code
636 class="text-xs"
637 x-text="version"
638 ></code>
639 </td>
640 </tr>
641 </tbody>
642 </table>
643 </div>
644 <h4 class="font-bold">
645 Accounts
646 </h4>
647 <div class="overflow-x-auto overflow-y-auto max-h-26">
648 <table class="table table-xs">
649 <tbody>
650 <template x-for="account in accounts">
651 <tr>
652 <td>
653 <code>
654 <a
655 href="#"
656 class="link"
657 x-text="account.did"
658 @click.prevent="goto(account.did)"
659 ></a>
660 </code>
661 </td>
662 <td>
663 <span
664 x-show="account.active"
665 class="badge badge-sm badge-soft badge-success"
666 >
667 active
668 </span>
669 <span
670 x-show="!account.active"
671 x-text="account.status"
672 class="badge badge-sm badge-soft badge-warning"
673 ></span>
674 </td>
675 <td
676 x-data="didToHandle(account.did)"
677 x-intersect:enter.once="load"
678 >
679 <span x-show="loading">Loading…</span>
680 <span x-show="error !== null" x-text="error"></span>
681 <a
682 href="#"
683 class="link"
684 @click.prevent="goto(handle)"
685 x-show="handle !== null"
686 x-text="`@${handle}`"
687 ></a>
688 </td>
689 </tr>
690 </template>
691 <template x-if="!loadingDesc && !accountsComplete">
692 <tr>
693 <td colspan="2" class="text-xs text-warning-content">
694 (more accounts not shown)
695 </td>
696 </tr>
697 </template>
698 </tbody>
699 </table>
700 </div>
701 </div>
702 </template>
703 </div>
704
705 <h3 class="text-lg mt-3">Relay host status</h3>
706 <div class="overflow-x-auto">
707 <table class="table table-xs">
708 <tbody>
709 <template x-for="relay in window.relays">
710 <tr
711 x-data="relayCheckHost(pds, relay)"
712 x-init="$watch('pds', pds => check(pds, relay))"
713 >
714 <td class="text-sm">
715 <div class="tooltip tooltip-right" :data-tip="relay.hostname">
716 <img
717 class="inline-block h-4 w-4"
718 :src="relay.icon"
719 alt=""
720 />
721 <span x-text="relay.name"></span>
722 <span
723 x-show="!!relay.note"
724 x-text="relay.note"
725 class="badge badge-soft badge-neutral badge-xs"
726 ></span>
727 </div>
728 </td>
729 <td>
730 <template x-if="loading">
731 <em>loading…</em>
732 </template>
733 <template x-if="error">
734 <div
735 class="text-xs"
736 :class="expectedErrorInfo
737 ? 'text-info tooltip tooltip-left cursor-help'
738 : 'text-warning'"
739 :data-tip="expectedErrorInfo"
740 >
741 <span x-text="error"></span>
742 <span
743 x-show="!!expectedErrorInfo"
744 class="badge badge-soft badge-info badge-xs"
745 >i</span>
746 </div>
747 </template>
748 <template x-if="status">
749 <span
750 x-text="status"
751 class="badge badge-sm"
752 :class="status === 'active' && 'badge-soft badge-success'"
753 ></span>
754 </template>
755 </td>
756 <td>
757 <div x-show="status !== 'active' && !expectedErrorInfo">
758 <button
759 x-show="reqCrawlStatus !== 'done'"
760 class="btn btn-xs btn-ghost whitespace-nowrap"
761 :disabled="reqCrawlStatus === 'loading'"
762 @click="requestCrawl(pds, relay)"
763 >
764 request crawl
765 </button>
766 <span
767 x-show="reqCrawlError !== null"
768 x-text="reqCrawlError"
769 class="text-xs text-warning"
770 ></span>
771 <button
772 x-show="reqCrawlError === null && reqCrawlStatus === 'done'"
773 class="btn btn-xs btn-soft btn-primary whitespace-nowrap"
774 @click="check"
775 >
776 refresh
777 </button>
778 </div>
779 </td>
780 </tr>
781 </template>
782 </tbody>
783 </table>
784 </div>
785 </div>
786 </div>
787 </template>
788
789 <template x-if="did != null">
790 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
791 <div class="card-body">
792 <h2 class="card-title">
793 <span class="badge badge-secondary">DID</span>
794 <code x-text="did"></code>
795 </h2>
796 <template x-if="pds != null">
797 <div x-data="didRepoState(did, pds)">
798 <h3 class="text-lg mt-3">
799 Repo
800 <span
801 x-show="state && state.active"
802 class="badge badge-sm badge-soft badge-success"
803 >active</span>
804 </h3>
805 <div class="overflow-x-auto">
806 <table class="table table-xs">
807 <tbody>
808 <tr>
809 <td class="text-sm">
810 Rev:
811 <code x-text="state && state.rev"></code>
812 </td>
813 </tr>
814 <tr>
815 <td class="text-sm">
816 PDS:
817 <a
818 href="#"
819 class="link"
820 @click.prevent="goto(pds)"
821 x-text="pds"
822 ></a>
823 </td>
824 </tr>
825 <tr>
826 <td class="text-sm">
827 Size:
828 <span x-data="repoSize">
829 <template x-if="loading">
830 <em>loading…</em>
831 </template>
832 <template x-if="error">
833 <span class="text-xs text-warning" x-text="error"></span>
834 </template>
835 <template x-if="size">
836 <code>
837 <span x-text="size"></span> MiB
838 </code>
839 </template>
840 <template x-if="!size && !error && !loading">
841 <button
842 class="btn btn-xs btn-soft btn-primary"
843 @click.prevent="loadRepoForSize(did, pds)"
844 >load</button>
845 </template>
846 </span>
847 </td>
848 </tr>
849 </tbody>
850 </table>
851 </div>
852
853 <h3 class="text-lg mt-3">
854 Relay repo status
855 </h3>
856 <div class="overflow-x-auto">
857 <table class="table table-xs">
858 <tbody>
859 <template x-for="relay in window.relays">
860 <tr
861 x-data="relayCheckRepo(did, relay)"
862 x-init="$watch('pds', pds => check(did, relay))"
863 >
864 <td class="text-sm">
865 <div class="tooltip tooltip-right" :data-tip="relay.hostname">
866 <img
867 class="inline-block h-4 w-4"
868 :src="relay.icon"
869 alt=""
870 />
871 <span x-text="relay.name"></span>
872 <span
873 x-show="!!relay.note"
874 x-text="relay.note"
875 class="badge badge-neutral badge-soft badge-xs"
876 ></span>
877 </div>
878 </td>
879 <template x-if="loading">
880 <td>
881 <em>loading…</em>
882 </td>
883 </template>
884 <template x-if="error">
885 <td>
886 <div
887 class="text-xs"
888 :class="expectedErrorInfo
889 ? 'text-info tooltip tooltip-left cursor-help'
890 : 'text-warning'"
891 :data-tip="expectedErrorInfo"
892 >
893 <span x-text="error"></span>
894 <span
895 x-show="!!expectedErrorInfo"
896 class="badge badge-soft badge-info badge-xs"
897 >i</span>
898 </div>
899 </td>
900 </template>
901 <template x-if="status">
902 <td>
903 <span
904 x-show="status.active"
905 class="badge badge-sm badge-soft badge-success"
906 >
907 active
908 </span>
909 <span
910 x-show="!status.active"
911 x-text="status.status"
912 class="badge badge-sm badge-soft badge-warning"
913 ></span>
914 </td>
915 </template>
916 <template x-if="revStatus(state && state.rev)">
917 <td x-data="{ asdf: revStatus(state.rev) }">
918 <span
919 x-show="asdf === 'current'"
920 class="badge badge-sm badge-soft badge-success"
921 >current</span>
922 <span
923 x-show="asdf === 'behind'"
924 class="badge badge-sm badge-soft badge-warning tooltip tooltip-left"
925 :data-tip="status.rev"
926 >behind</span>
927 <span
928 x-show="asdf === 'ahead'"
929 class="badge badge-sm badge-soft badge-success tooltip tooltip-left"
930 :data-tip="`Account may have updated between checks? ${status.rev}`"
931 >ahead</span>
932 </td>
933 </template>
934 </tr>
935 </template>
936 </tbody>
937 </table>
938 </div>
939
940 <template x-if="did.startsWith('did:plc:')">
941 <div x-data="pdsHistory(did, pds)">
942 <h3 class="text-lg mt-3">
943 PLC PDS history
944 </h3>
945 <div class="overflow-x-auto">
946 <table class="table table-xs">
947 <tbody>
948 <template x-if="loading">
949 <tr>
950 <td>Loading…</td>
951 </tr>
952 </template>
953 <template x-if="error">
954 <tr>
955 <td>Error: <span x-text="error"></span></td>
956 </tr>
957 </template>
958 <template x-if="!loading && !error && history.length === 0">
959 <tr>
960 <td class="text-sm">
961 <em>no previous PDS</em>
962 </td>
963 </tr>
964 </template>
965 <template x-for="event in history">
966 <tr x-data="didRepoState(did, event.pds)">
967 <td>
968 <code x-text="event.date.split('T')[0]"></code>
969 </td>
970 <td>
971 <a
972 href="#"
973 class="link"
974 @click.prevent="goto(event.pds)"
975 x-text="event.pds"
976 ></a>
977 </td>
978 <template x-if="event.current">
979 <td>
980 <span
981 x-show="state && !state.active"
982 x-text="state && state.status"
983 class="badge badge-sm badge-soft badge-warning"
984 ></span>
985 <span
986 x-show="state && state.active"
987 class="badge badge-sm badge-soft badge-success"
988 >current</span>
989 </td>
990 </template>
991 <template x-if="!event.current">
992 <td>
993 <span
994 x-show="state && !state.active"
995 x-text="state && state.status"
996 class="badge badge-sm badge-soft badge-success"
997 ></span>
998 <span
999 x-show="state && state.active"
1000 class="badge badge-sm badge-soft badge-warning"
1001 >active</span>
1002 </td>
1003 </template>
1004 </tr>
1005 </template>
1006 </tbody>
1007 </table>
1008 </div>
1009 </div>
1010 </template>
1011 </div>
1012 </template>
1013 </div>
1014 </div>
1015 </template>
1016
1017 <template x-if="handle !== null">
1018 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
1019 <div
1020 x-data="checkHandle(handle)"
1021 x-init="$watch('handle', h => updateHandle(h))"
1022 class="card-body"
1023 >
1024 <h2 class="card-title">
1025 <span class="badge badge-secondary">Handle</span>
1026 <span x-text="handle"></span>
1027 </h2>
1028
1029 <h3 class="text-lg mt-3">
1030 Resolution
1031 </h3>
1032 <p x-show="loading" class="text-i">Loading…</p>
1033 <div x-show="!loading" class="overflow-x-auto">
1034 <table class="table table-xs">
1035 <tbody>
1036 <tr>
1037 <td class="text-sm">DNS</td>
1038 <td class="text-sm">
1039 <code x-text="dnsDid"></code>
1040 </td>
1041 <td>
1042 <div
1043 class="badge badge-sm badge-soft badge-neutral"
1044 x-show="dnsErr !== null"
1045 x-text="dnsErr"
1046 ></div>
1047 </td>
1048 </tr>
1049 <tr>
1050 <td class="text-sm">Http</td>
1051 <td class="text-sm">
1052 <code x-text="httpDid"></code>
1053 </td>
1054 <td>
1055 <div
1056 class="badge badge-sm badge-soft badge-neutral"
1057 x-show="httpErr !== null"
1058 x-text="httpErr"
1059 ></div>
1060 </td>
1061 </tr>
1062 </tbody>
1063 </table>
1064 </div>
1065
1066 <template x-if="did !== null && did.startsWith('did:plc:')">
1067
1068 <div x-data="handleHistory(did, handle)">
1069 <h3 class="text-lg mt-3">
1070 PLC handle history
1071 </h3>
1072 <div class="overflow-x-auto">
1073 <table class="table table-xs">
1074 <tbody>
1075 <template x-if="loading">
1076 <tr>
1077 <td>Loading…</td>
1078 </tr>
1079 </template>
1080 <template x-if="error">
1081 <tr>
1082 <td>Error: <span x-text="error"></span></td>
1083 </tr>
1084 </template>
1085 <template x-if="!loading && !error && history.length === 0">
1086 <tr>
1087 <td class="text-sm">
1088 <em>no previous handle</em>
1089 </td>
1090 </tr>
1091 </template>
1092 <template x-for="event in history">
1093 <tr>
1094 <td>
1095 <code x-text="event.date.split('T')[0]"></code>
1096 </td>
1097 <td>
1098 <a
1099 href="#"
1100 class="link"
1101 @click.prevent="goto(event.handle)"
1102 x-text="event.handle"
1103 ></a>
1104 </td>
1105 </tr>
1106 </template>
1107 </tbody>
1108 </table>
1109 </div>
1110 </div>
1111
1112 </template>
1113 </div>
1114 </div>
1115 </template>
1116 </div>
1117 </div>
1118 </body>
1119</html>