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
···
<h2 id="status">status</h2>
+
<p>Current status: <span id="current-status">Loading...</span></p>
+
<p>off-site status page in case the PDS goes down; in other words,<br>
this page will remain up even when the PDS is down.</p>
+94 -37
katproto_users.ts
···
-
async function getKatprotoUsers() {
-
const users = fetch("https://katproto.girlonthemoon.xyz/xrpc/com.atproto.sync.listRepos")
-
// type cast because no point validating for smthn like this
-
// real type has more info; not needed here
-
.then((res) => res.json() as Promise<{ repos: { did: string }[] }>)
-
.then((res) =>
-
// get display name, handle, and did for each user
-
res.repos.map((repo) => ({
-
display: fetch(
-
`https://katproto.girlonthemoon.xyz/xrpc/com.atproto.repo.getRecord?repo=${repo.did}&collection=app.bsky.actor.profile&rkey=self`
-
)
-
.then((res) => res.json())
-
.then((profile) => profile.value.displayName),
-
// dont validate handles because I'm Lazy + trust myself
-
handle: fetch(
-
repo.did.startsWith("did:plc")
-
? "https://plc.directory/" + repo.did
-
: `https://${repo.did.replace("did:web:", "")}/.well-known/did.json`
-
)
-
.then((res) => res.json())
-
.then((doc) => doc.alsoKnownAs[0].replace("at://", "")),
-
did: repo.did
-
}))
-
)
-
.then(async (users) =>
-
(
-
await Promise.all(
-
users.map(
-
async (x) =>
-
`<span class="user-count"><a href="https://bsky.app/profile/${x.did}">${await x.display}</a></span>`
-
)
-
)
-
)
-
)
-
const fin = Array.from(await users).join("<br>");
-
return (await fin);
+
const ESCAPE_LOOKUP: Record<string, string> = {
+
"&": "&amp;",
+
'"': "&quot;",
+
"'": "&apos;",
+
"<": "&lt;",
+
">": "&gt;",
};
+
const PDS = "https://katproto.girlonthemoon.xyz";
+
+
async function getKatprotoUsers() {
+
const users = await fetch(PDS + "/xrpc/com.atproto.sync.listRepos")
+
// type cast because no point validating for smthn like this
+
// real type has more info; not needed here
+
.then((res) => res.json() as Promise<{ repos: { did: string }[] }>)
+
.then((res) =>
+
// get display name, handle, and did for each user
+
res.repos.map((repo) => ({
+
display: fetch(
+
`${PDS}/xrpc/com.atproto.repo.getRecord?repo=${repo.did}&collection=app.bsky.actor.profile&rkey=self`
+
)
+
.then((res) => res.json())
+
.then((profile) => profile.value.displayName),
+
// dont validate handles because I'm Lazy + trust myself
+
handle: fetch(
+
repo.did.startsWith("did:plc")
+
? "https://plc.directory/" + repo.did
+
: `https://${repo.did.replace("did:web:", "")}/.well-known/did.json`
+
)
+
.then((res) => res.json())
+
.then((doc) => doc.alsoKnownAs[0].replace("at://", "")),
+
did: repo.did,
+
}))
+
)
+
.then(
+
async (users) =>
+
await Promise.all(
+
users.map(
+
async (x) =>
+
`<span class="user-count"><a href="https://bsky.app/profile/${x.did}">${await x.display}</a></span>`
+
)
+
)
+
);
+
+
const fin = Array.from(users).join("<br>");
+
return await fin;
+
}
+
+
async function checkStatus() {
+
const statusElement = document.getElementById("current-status");
+
if (!statusElement) return;
+
+
// try get `/xrpc/_health`
+
const result = await fetch(PDS + "/xrpc/_health")
+
.then(async (res) => ({
+
status: res.status,
+
statusText: res.statusText,
+
res: await res.text(),
+
}))
+
.catch((err) => {
+
// we only return undefined if we get a type error
+
// this means we were blocked by permissions (cors) (upstream is offline)
+
// or the device couldnt connect (main server and index is offline)
+
if (err instanceof TypeError) return undefined;
+
console.warn("Ignoring:", err);
+
// if we didnt expect this error we want to bubble it up as normal
+
throw err;
+
});
+
+
// make sure html is escaped for status text
+
// this could (in theory) be anything so we should make sure its escaped properly
+
// also an & could break things
+
if (result) {
+
result.statusText = result.statusText.replaceAll(
+
/[&"'<>]/g,
+
(c) => ESCAPE_LOOKUP[c]
+
);
+
}
+
+
if (!result) {
+
statusElement.innerHTML = "🔴 Offline";
+
statusElement.title = "The server could not be reached.";
+
return;
+
}
+
+
if (result.status < 200 || result.status > 299) {
+
statusElement.innerHTML = `🟡 Some Issues: ${result.status} ${result.statusText}`;
+
statusElement.title = result.res;
+
}
+
+
statusElement.innerHTML = `🟢 Online`;
+
statusElement.title = result.res;
+
}
-
getKatprotoUsers();
+
// silence errors
+
getKatprotoUsers().catch((err) => console.warn(err));
+
addEventListener("load", () => checkStatus().catch((err) => console.warn(err)));