Update frontend UI/UX

This commit is contained in:
2026-10-20 04:59:08 +01:00
parent 59f5acb7bc
commit e2093b0df6
17 changed files with 163 additions and 11 deletions
+7
View File
@@ -0,0 +1,7 @@
/**
* How a bot names a crew member in a message: `[bot:3]`. The chat shows it
* as a link to that bot's conversation.
*/
export default function botRef({ id }: { id: number | string }) {
return `[bot:${id}]`;
}
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test";
import isSecureRequest from "./is-secure-request";
describe("isSecureRequest", () => {
test("plain HTTP on a VPN address is not secure", () => {
const req = new Request("http://10.8.0.1:7247/api/auth/password-login");
expect(isSecureRequest({ req })).toBe(false);
});
test("an HTTPS request is secure", () => {
const req = new Request("https://cobble.example.com/");
expect(isSecureRequest({ req })).toBe(true);
});
test("trusts the proxy's forwarded scheme", () => {
const req = new Request("http://localhost:7247/", {
headers: { "X-Forwarded-Proto": "https" },
});
expect(isSecureRequest({ req })).toBe(true);
});
test("reads the first of several forwarded schemes", () => {
const req = new Request("https://cobble.example.com/", {
headers: { "X-Forwarded-Proto": "http, https" },
});
expect(isSecureRequest({ req })).toBe(false);
});
});
+21
View File
@@ -0,0 +1,21 @@
type Params = {
req: Request;
};
/**
* Whether the browser reached the app over HTTPS: the request's own scheme,
* or the first `X-Forwarded-Proto` a proxy (Cloudflare, Caddy) set in front.
* Plain HTTP (a WireGuard address, a LAN IP) gets cookies without `Secure`,
* which the browser would otherwise drop.
*/
export default function isSecureRequest({ req }: Params): boolean {
const forwarded = req.headers
.get("x-forwarded-proto")
?.split(",")[0]
?.trim()
.toLowerCase();
if (forwarded) return forwarded === "https";
return new URL(req.url).protocol === "https:";
}
+9
View File
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
import botRef from "./bot-ref";
import computerRef from "./computer-ref";
import linkComputerRefs from "./link-computer-refs";
import takeOverRef from "./take-over-ref";
@@ -25,4 +26,12 @@ describe("linkComputerRefs", () => {
}),
).toBe("Please log in to Gmail. [take over](#take-over-3-browser)");
});
test("links every bot a message names", () => {
expect(
linkComputerRefs({
text: `${botRef({ id: 4 })} is on it. See [bot:x] and [bot:5](https://example.com).`,
}),
).toBe("[bot 4](#bot-4) is on it. See [bot:x] and [bot:5](https://example.com).");
});
});
+9 -2
View File
@@ -12,9 +12,15 @@ export const ComputerRefHrefPrefix = "#computer-";
*/
export const TakeOverRefHrefPrefix = "#take-over-";
/**
* The link target a bot ref (`botRef`) becomes, e.g. `#bot-3`. The chat
* renders these as links to that bot's conversation.
*/
export const BotRefHrefPrefix = "#bot-";
/**
* Turns every computer ref (`[computer:3]`) and take-over ref
* (`[take-over:3:browser]`) in a message into a markdown link, so it
* (`[take-over:3:browser]`) and bot ref (`[bot:3]`) in a message into a markdown link, so it
* survives markdown rendering as a link the chat can swap for a button.
*/
export default function linkComputerRefs({ text }: { text: string }) {
@@ -26,5 +32,6 @@ export default function linkComputerRefs({ text }: { text: string }) {
.replace(
/\[take-over:(\d+):([a-z]+)\](?!\()/g,
(_, id: string, mode: string) => `[take over](${TakeOverRefHrefPrefix}${id}-${mode})`,
);
)
.replace(/\[bot:(\d+)\](?!\()/g, (_, id: string) => `[bot ${id}](${BotRefHrefPrefix}${id})`);
}
+4
View File
@@ -10,6 +10,10 @@ describe("messagePreview", () => {
).toBe("Found it Your GitHub session is on a computer. See the docs and search_computers. one two");
});
test("names bots plainly", () => {
expect(messagePreview({ text: "[bot:4] is on it." })).toBe("a bot is on it.");
});
test("keeps code text without its fences", () => {
expect(messagePreview({ text: "Run:\n```bash\nls ~\n```" })).toBe("Run: ls ~");
});
+2 -1
View File
@@ -1,12 +1,13 @@
/**
* A bot message as one line of plain text, for the rail's preview: no
* markdown syntax, links as their text, computer refs (`computerRef`) as
* "a computer", whitespace collapsed.
* "a computer", bot refs (`botRef`) as "a bot", whitespace collapsed.
*/
export default function messagePreview({ text }: { text: string }) {
return text
.replace(/```[^\n]*\n?/g, "")
.replace(/\[computer:\d+\](?!\()/g, "a computer")
.replace(/\[bot:\d+\](?!\()/g, "a bot")
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
.replace(/^\s{0,3}(#{1,6}\s+|>\s?|[-*+]\s+|\d+\.\s+)/gm, "")
.replace(/(\*\*|__|~~|`)/g, "")