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 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 7 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/> 8 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> 9 10 <script type="module"> 11 import { Client, ClientResponseError, ok, simpleFetchHandler } from 'https://esm.sh/@atcute/client@4.1.1'; 12 window.SimpleQuery = service => { 13 const client = new Client({ handler: simpleFetchHandler({ service }) }); 14 return (...args) => ok(client.get(...args)); 15 }; 16 window.SimpleProc = service => { 17 const client = new Client({ handler: simpleFetchHandler({ service }) }); 18 return (...args) => ok(client.post(...args)); 19 }; 20 window.isXrpcErr = e => e instanceof ClientResponseError; 21 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue'); 22 window.relays = [ 23 { 24 name: 'Bluesky production', 25 hostname: 'bsky.network', 26 }, 27 { 28 name: 'Bluesky sync1.1 East', 29 hostname: 'relay1.us-east.bsky.network', 30 }, 31 { 32 name: 'Bluesky sync1.1 West', 33 hostname: 'relay1.us-west.bsky.network', 34 }, 35 { 36 name: 'Blacksky', 37 hostname: 'atproto.africa', 38 }, 39 { 40 name: 'Microcosm Montreal', 41 hostname: 'relay.fire.hose.cam', 42 }, 43 { 44 name: 'Microcosm France', 45 hostname: 'relay3.fr.hose.cam', 46 }, 47 ]; 48 </script> 49 50 <script> 51 document.addEventListener('alpine:init', () => { 52 Alpine.data('debug', () => ({ 53 // form input 54 identifier: '', 55 56 // state 57 identifierLoading: false, 58 identifierError: null, 59 60 // stuff to check 61 pds: null, 62 did: null, 63 handle: null, 64 65 async diagnose() { 66 this.identifierLoading = true; 67 this.identifierError = null; 68 this.pds = null; 69 this.did = null; 70 if (this.identifier.startsWith('https://')) { 71 this.pds = this.identifier; 72 } else { 73 if (this.identifier.startsWith('at://')) { 74 this.identifier = this.identifier.slice('at://'.length); 75 } 76 if (this.identifier.startsWith('did:')) { 77 this.did = this.identifier; 78 } else { 79 let data; 80 try { 81 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 82 params: { identifier: this.identifier }, 83 }); 84 this.did = data.did; 85 this.pds = data.pds; 86 } catch (e) { 87 if (window.isXrpcErr(e)) { 88 this.identifierError = e.error; 89 if (e.message) this.description += ` ${e.description}`; 90 } else { 91 this.identifierError = 'Failed to resolve identifier, see console for error.'; 92 console.error(e); 93 } 94 } 95 } 96 } 97 this.identifierLoading = false; 98 }, 99 })); 100 101 Alpine.data('relayCheckHost', (pds, relay) => ({ 102 loading: false, 103 error: null, 104 status: null, 105 reqCrawlStatus: null, 106 reqCrawlError: null, 107 108 async init() { 109 await this.check(); 110 }, 111 112 async check() { 113 this.loading = true; 114 this.error = null; 115 this.status = null; 116 let query = window.SimpleQuery(`https://${relay.hostname}`); 117 const hostname = pds.split('://')[1]; 118 let data; 119 try { 120 data = await query('com.atproto.sync.getHostStatus', { 121 params: { hostname }, 122 }); 123 this.status = data.status; 124 } catch(e) { 125 if (window.isXrpcErr(e)) { 126 this.error = e.error; 127 } else { 128 this.error = 'Failed to check (see console)'; 129 console.error(e); 130 } 131 } 132 this.loading = false; 133 this.reqCrawlStatus = null; 134 this.reqCrawlError = null; 135 }, 136 137 async requestCrawl() { 138 this.reqCrawlStatus = "loading"; 139 const proc = window.SimpleProc(`https://${relay.hostname}`); 140 const hostname = pds.split('://')[1]; 141 let data; 142 try { 143 data = await proc('com.atproto.sync.requestCrawl', { 144 input: { hostname }, 145 }); 146 } catch (e) { 147 if (window.isXrpcErr(e)) { 148 this.reqCrawlError = e.error; 149 } else { 150 this.reqCrawlError = 'failed (see console)'; 151 console.error(e); 152 } 153 } 154 this.reqCrawlStatus = "done"; 155 }, 156 })) 157 }) 158 </script> 159 </head> 160 <body x-data="debug"> 161 <div class="hero bg-base-200"> 162 <div class="hero-content flex-col"> 163 <h1>PDS Debugger</h1> 164 165 <p>Work in progress!</p> 166 <details class="text-xs"> 167 <summary>Would be nice</summary> 168 <ul> 169 <li>anything that actually works</li> 170 <li>firehose listener for missing pds events</li> 171 <li>jetstream listener for missing pds events</li> 172 <li>check relays for account status</li> 173 <li>check relays for pds state</li> 174 <li>plc: check old pds hosts for active account state</li> 175 </ul> 176 </details> 177 <details class="text-xs"> 178 <summary>Limitations</summary> 179 <ul> 180 <li>it's all client-side</li> 181 </ul> 182 </details> 183 184 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 185 <div class="card-body"> 186 <form @submit.prevent="await diagnose()"> 187 <label> 188 Enter an atproto handle, DID, or HTTPS PDS URL 189 <input 190 class="input" 191 x-model="identifier" 192 :disabled="identifierLoading" 193 autofocus 194 /> 195 </label> 196 </form> 197 </div> 198 </div> 199 200 <template x-if="identifierError"> 201 <p>uh oh: <span x-text="identifierError"></span></p> 202 </template> 203 204 <template x-if="pds != null"> 205 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 206 <div class="card-body"> 207 <h2 class="card-title"> 208 <span class="badge badge-secondary">PDS</span> 209 <span x-text="pds"></span> 210 </h2> 211 212 <h3 class="text-lg">Relay host status</h3> 213 <div class="overflow-x-auto"> 214 <table class="table table-xs"> 215 <tbody> 216 <template x-for="relay in window.relays"> 217 <tr x-data="relayCheckHost(pds, relay)"> 218 <td x-text="relay.name" class="text-sm"></td> 219 <td> 220 <template x-if="loading"> 221 <em>loading&hellip;</em> 222 </template> 223 <template x-if="error"> 224 <span 225 x-text="error" 226 class="text-xs text-warning" 227 ></span> 228 </template> 229 <template x-if="status"> 230 <span 231 x-text="status" 232 class="badge badge-sm" 233 :class="status === 'active' && 'badge-soft badge-success'" 234 ></span> 235 </template> 236 </td> 237 <td> 238 <div x-show="status !== 'active'"> 239 <button 240 x-show="reqCrawlStatus !== 'done'" 241 class="btn btn-xs btn-ghost whitespace-nowrap" 242 :disabled="reqCrawlStatus === 'loading'" 243 @click="requestCrawl" 244 > 245 request crawl 246 </button> 247 <span 248 x-show="reqCrawlError !== null" 249 x-text="reqCrawlError" 250 class="text-xs text-warning" 251 ></span> 252 <button 253 x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 254 class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 255 @click="check" 256 > 257 refresh 258 </button> 259 </div> 260 </td> 261 </tr> 262 </template> 263 </tbody> 264 </table> 265 </div> 266 </div> 267 </div> 268 </template> 269 270 <template x-if="did != null"> 271 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 272 <div class="card-body"> 273 <p x-text="`DID: ${did}`"></p> 274 </div> 275 </div> 276 </template> 277 </div> 278 </div> 279 </body> 280</html>