Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam

relay host status

Changed files
+280
+280
index.html
··· 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>