Add "Current Status" to index page #1

merged
opened by vielle.dev targeting main

The main change is that the site will call /xrpc/_health and display a status based on the response

  • Offline: The endpoint threw an error (either due to CORS or it could not connect. We aren't able to tell which one it is, so treat them the same)
  • Some Issues: The endpoint returned a non 2xx response. This shouldn't happen but it still COULD, so we should have an edge case for it. Hovering the status will show the response body.
  • Online: The endpoint returned a 2xx status code. Hovering the status will show the response body

Other changes:

  • PDS is a constant at the top of the file. I would just remove the PDS prefix entirely, since this should just be deployed in front of a PDS on the same domain, but I used file:/// to test it and I wanted my testing to work lol. Added so ppl using this code (if allowed) can tweak it for their PDS easily. Would also allow a PDS domain migration with a single endpoint change.
  • Move the await on the fetch call in getKatprotoUsers. calling await on the fetch call makes more sense.
  • If getKatprotoUsers throws an error, just console.warn it. This is to avoid it interupting other functions. It should be fine, but better safe than sorry.
  • Apply formatting by prettier. I couldn't be bothered to turn it of so I'm leaving it in the PR unless u hate it lolol
Changed files
+96 -37
+2
index.html
··· 162 162 163 163 <h2 id="status">status</h2> 164 164 165 + <p>Current status: <span id="current-status">Loading...</span></p> 166 + 165 167 <p>off-site status page in case the PDS goes down; in other words,<br> 166 168 this page will remain up even when the PDS is down.</p> 167 169
+94 -37
katproto_users.ts
··· 1 - async function getKatprotoUsers() { 2 - const users = fetch("https://katproto.girlonthemoon.xyz/xrpc/com.atproto.sync.listRepos") 3 - // type cast because no point validating for smthn like this 4 - // real type has more info; not needed here 5 - .then((res) => res.json() as Promise<{ repos: { did: string }[] }>) 6 - .then((res) => 7 - // get display name, handle, and did for each user 8 - res.repos.map((repo) => ({ 9 - display: fetch( 10 - `https://katproto.girlonthemoon.xyz/xrpc/com.atproto.repo.getRecord?repo=${repo.did}&collection=app.bsky.actor.profile&rkey=self` 11 - ) 12 - .then((res) => res.json()) 13 - .then((profile) => profile.value.displayName), 14 - // dont validate handles because I'm Lazy + trust myself 15 - handle: fetch( 16 - repo.did.startsWith("did:plc") 17 - ? "https://plc.directory/" + repo.did 18 - : `https://${repo.did.replace("did:web:", "")}/.well-known/did.json` 19 - ) 20 - .then((res) => res.json()) 21 - .then((doc) => doc.alsoKnownAs[0].replace("at://", "")), 22 - did: repo.did 23 - })) 24 - ) 25 - .then(async (users) => 26 - ( 27 - await Promise.all( 28 - users.map( 29 - async (x) => 30 - `<span class="user-count"><a href="https://bsky.app/profile/${x.did}">${await x.display}</a></span>` 31 - ) 32 - ) 33 - ) 34 - ) 35 - const fin = Array.from(await users).join("<br>"); 36 - return (await fin); 1 + const ESCAPE_LOOKUP: Record<string, string> = { 2 + "&": "&amp;", 3 + '"': "&quot;", 4 + "'": "&apos;", 5 + "<": "&lt;", 6 + ">": "&gt;", 37 7 }; 8 + const PDS = "https://katproto.girlonthemoon.xyz"; 9 + 10 + async function getKatprotoUsers() { 11 + const users = await fetch(PDS + "/xrpc/com.atproto.sync.listRepos") 12 + // type cast because no point validating for smthn like this 13 + // real type has more info; not needed here 14 + .then((res) => res.json() as Promise<{ repos: { did: string }[] }>) 15 + .then((res) => 16 + // get display name, handle, and did for each user 17 + res.repos.map((repo) => ({ 18 + display: fetch( 19 + `${PDS}/xrpc/com.atproto.repo.getRecord?repo=${repo.did}&collection=app.bsky.actor.profile&rkey=self` 20 + ) 21 + .then((res) => res.json()) 22 + .then((profile) => profile.value.displayName), 23 + // dont validate handles because I'm Lazy + trust myself 24 + handle: fetch( 25 + repo.did.startsWith("did:plc") 26 + ? "https://plc.directory/" + repo.did 27 + : `https://${repo.did.replace("did:web:", "")}/.well-known/did.json` 28 + ) 29 + .then((res) => res.json()) 30 + .then((doc) => doc.alsoKnownAs[0].replace("at://", "")), 31 + did: repo.did, 32 + })) 33 + ) 34 + .then( 35 + async (users) => 36 + await Promise.all( 37 + users.map( 38 + async (x) => 39 + `<span class="user-count"><a href="https://bsky.app/profile/${x.did}">${await x.display}</a></span>` 40 + ) 41 + ) 42 + ); 43 + 44 + const fin = Array.from(users).join("<br>"); 45 + return await fin; 46 + } 47 + 48 + async function checkStatus() { 49 + const statusElement = document.getElementById("current-status"); 50 + if (!statusElement) return; 51 + 52 + // try get `/xrpc/_health` 53 + const result = await fetch(PDS + "/xrpc/_health") 54 + .then(async (res) => ({ 55 + status: res.status, 56 + statusText: res.statusText, 57 + res: await res.text(), 58 + })) 59 + .catch((err) => { 60 + // we only return undefined if we get a type error 61 + // this means we were blocked by permissions (cors) (upstream is offline) 62 + // or the device couldnt connect (main server and index is offline) 63 + if (err instanceof TypeError) return undefined; 64 + console.warn("Ignoring:", err); 65 + // if we didnt expect this error we want to bubble it up as normal 66 + throw err; 67 + }); 68 + 69 + // make sure html is escaped for status text 70 + // this could (in theory) be anything so we should make sure its escaped properly 71 + // also an & could break things 72 + if (result) { 73 + result.statusText = result.statusText.replaceAll( 74 + /[&"'<>]/g, 75 + (c) => ESCAPE_LOOKUP[c] 76 + ); 77 + } 78 + 79 + if (!result) { 80 + statusElement.innerHTML = "🔴 Offline"; 81 + statusElement.title = "The server could not be reached."; 82 + return; 83 + } 84 + 85 + if (result.status < 200 || result.status > 299) { 86 + statusElement.innerHTML = `🟡 Some Issues: ${result.status} ${result.statusText}`; 87 + statusElement.title = result.res; 88 + } 89 + 90 + statusElement.innerHTML = `🟢 Online`; 91 + statusElement.title = result.res; 92 + } 38 93 39 - getKatprotoUsers(); 94 + // silence errors 95 + getKatprotoUsers().catch((err) => console.warn(err)); 96 + addEventListener("load", () => checkStatus().catch((err) => console.warn(err)));