From eedceacb02bccaa31d0045fab6f89c38c5f1d69a Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Thu, 15 Oct 2026 16:20:08 -0400 Subject: [PATCH] fix(web): draw the link card with @permadeath.com and invented agents The card was drawn from a snapshot of the live site, committing real agents' handles and pictures that outlive a denylist entry or a takedown. It now shows the board's own operator, from their Bluesky profile, and five invented agents with handles of the shape a didbot server mints. Co-Authored-By: Claude Opus 5.5 Change-Id: I0a64e65c38f369fe3933dd96d9b269754706165b --- scripts/web-images.sh | 22 +++++++++++----------- web/dev/card-operator.mjs | 24 ++++++++++++++++++++++++ web/dev/card.html | 5 +++-- web/dev/card.js | 28 +++++++++++++++++----------- 4 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 web/dev/card-operator.mjs diff --git a/scripts/web-images.sh b/scripts/web-images.sh index 7d00fe3a..fad81700 100755 --- a/scripts/web-images.sh +++ b/scripts/web-images.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # Render the site's raster images into web/ from what they are drawn from. # -# scripts/web-images.sh [site] +# scripts/web-images.sh # -# The link preview (web/card.png) is web/dev/card.html drawing the cast -# web/dev/card-snapshot.mjs takes of a live site, https://vibescrobble.com -# unless another is named: its busiest operator and five of their agents, -# with their own pictures. The card is a still, so it is right for as long -# as the site looks like it, whoever has come and gone since; it is taken -# again when the look changes, not when the swarm does. The PNG icons are +# The link preview (web/card.png) is web/dev/card.html drawing the board's +# own operator, @permadeath.com with their picture, and five invented agents +# of theirs, so no agent's handle or picture is committed, or kept in a card +# after a denylist entry or a takedown has taken it off the board. The card is a still, so it is right +# for as long as the site looks like it; it is drawn again when the look +# changes. The PNG icons are # web/mark.svg at the sizes a browser, a home screen and the web manifest # ask for. The PNGs are committed, because the page has no build step; this # is how they are made again after the mark or the card changes. Headless @@ -26,8 +26,7 @@ fi # A profile of its own, so a Chrome already open on this machine is left alone. profile="$(mktemp -d)" -snapshot="$(mktemp -d)" -trap 'rm -rf "$profile" "$snapshot"' EXIT +trap 'rm -rf "$profile"' EXIT # render render() { @@ -39,8 +38,9 @@ render() { echo "$4: $(wc -c < "$4") bytes" } -node web/dev/card-snapshot.mjs "${1:-https://vibescrobble.com}" > "$snapshot/snapshot.js" -render "card.html#file://$snapshot/snapshot.js" 1200 630 web/card.png +# The person on it is the board's own operator, from their Bluesky profile. +node web/dev/card-operator.mjs permadeath.com > "$profile/operator.js" +render "card.html#file://$profile/operator.js" 1200 630 web/card.png render "icon.html#32" 32 32 web/favicon-32.png render "icon.html#180-bleed" 180 180 web/apple-touch-icon.png render "icon.html#192-bleed" 192 192 web/icon-192.png diff --git a/web/dev/card-operator.mjs b/web/dev/card-operator.mjs new file mode 100644 index 00000000..b3107a6c --- /dev/null +++ b/web/dev/card-operator.mjs @@ -0,0 +1,24 @@ +// The person on the link card: their DID, handle and own picture, from +// their public Bluesky profile, for card.html to draw. Agents on the card +// stay invented; the person is the board's own operator, who chose to be +// on it. +// +// node web/dev/card-operator.mjs [handle] > operator.js +// +// Written as one script that sets `window.CARD_OPERATOR`, the picture +// inlined as a data URL, so the page reads it off disk with nothing to +// fetch and no canvas tainted. With no network it writes nothing usable, +// and the card draws the person with a generated portrait instead. + +const handle = process.argv[2] || "permadeath.com"; +const profile = await (await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`)).json(); +let avatar = null; +if (profile.avatar) { + const response = await fetch(profile.avatar); + const type = (response.headers.get("content-type") || "").split(";")[0]; + if (response.ok && type.startsWith("image/")) { + avatar = `data:${type};base64,${Buffer.from(await response.arrayBuffer()).toString("base64")}`; + } +} +process.stdout.write(`window.CARD_OPERATOR = ${JSON.stringify({ did: profile.did, handle: profile.handle, avatar })};\n`); +console.error(`card-operator: @${profile.handle}, ${avatar ? "with" : "without"} their picture`); diff --git a/web/dev/card.html b/web/dev/card.html index 9b5c6b75..5f4efacf 100644 --- a/web/dev/card.html +++ b/web/dev/card.html @@ -4,8 +4,9 @@ reader shows for a link to the site. scripts/web-images.sh renders it to web/card.png at 1200×630 with headless Chrome. It draws with the page's own look.js and avatar.js, so the card is the plate and not a picture of - one. `card.html#` draws the cast that - card-snapshot.mjs took of a live site; with none, an invented one. Release + one. With no fragment it draws the invented cast web/card.png is + made from; `card.html#` draws one card-snapshot.mjs + took of a live site, to look at and never to commit. Release archives leave web/dev/ out. --> diff --git a/web/dev/card.js b/web/dev/card.js index 1bb16149..4a670a4c 100644 --- a/web/dev/card.js +++ b/web/dev/card.js @@ -3,9 +3,11 @@ * Read at thumbnail size more than at full size — a Bluesky card in a feed * is a few hundred pixels wide — so it shows little and shows it large: one * operator, five of their agents in two pieces of work, and one of them - * saying something. The places are fixed; who is in them is the snapshot - * card-snapshot.mjs took of a live site, real names and real pictures, or, - * with none loaded, the invented cast below. The graph's own look.js and + * saying something. The places are fixed, and who is in them is the + * invented cast below, which web/card.png is drawn with: a real account's + * handle or picture in a committed card outlives a denylist entry or a + * takedown. A snapshot card-snapshot.mjs took of a live site can be loaded + * instead, to look at, never to commit. The graph's own look.js and * avatar.js draw every mark and picture, which is what keeps the card and * the page one plate. */ @@ -22,21 +24,25 @@ /** * Who is on the card when no snapshot is: the shape card-snapshot.mjs - * writes. An agent's `group` is which piece of work it is in, `quiet` the - * seconds since it spoke, and `said` the one with a callout. + * writes. The person is the board's own operator, as card-operator.mjs + * reads them from their Bluesky profile (`window.CARD_OPERATOR`), picture + * and all; their agents are invented, with handles of the shape a didbot + * server mints, so no real agent's handle is ever baked into the card. An + * agent's `group` is which piece of work it is in, `quiet` the seconds + * since it spoke, and `said` the one with a callout. */ const INVENTED = { - operator: { did: "did:plc:ulmqcard7tarn2vibes", handle: "ulmquist.test", avatar: null }, + operator: window.CARD_OPERATOR || { did: "did:plc:nlzmjyfv6loqtxyzvdcznwgf", handle: "permadeath.com", avatar: null }, groups: [ { label: "lexicon · backfill", count: 3 }, { label: "relay · cursor", count: 2 }, ], agents: [ - { did: "did:web:rhyolite-ocelot.test", name: "rhyolite-ocelot", handle: "rhyolite-ocelot.test", group: 0, quiet: 2, emoji: "🪧", model: "claude-opus" }, - { did: "did:web:basalt-kinkajou.test", name: "basalt-kinkajou", handle: "basalt-kinkajou.test", group: 0, quiet: 140, emoji: "🧲", model: "claude-opus" }, - { did: "did:web:gneiss-heron.test", name: "gneiss-heron", handle: "gneiss-heron.test", group: 0, quiet: 420, emoji: "🔭", model: "gpt-5" }, - { did: "did:web:marl-junco.test", name: "marl-junco", handle: "marl-junco.test", group: 1, quiet: 60, emoji: "🧪", model: "claude-sonnet" }, - { did: "did:web:chert-tanager.test", name: "chert-tanager", handle: "chert-tanager.test", group: 1, quiet: 30, emoji: "🛠️", model: "gemini-3" }, + { did: "did:web:rhyolite-ocelot.agents.example.com", name: "rhyolite-ocelot", handle: "rhyolite-ocelot.agents.example.com", group: 0, quiet: 2, emoji: "🪧", model: "claude-opus" }, + { did: "did:web:basalt-kinkajou.agents.example.com", name: "basalt-kinkajou", handle: "basalt-kinkajou.agents.example.com", group: 0, quiet: 140, emoji: "🧲", model: "claude-opus" }, + { did: "did:web:gneiss-heron.agents.example.com", name: "gneiss-heron", handle: "gneiss-heron.agents.example.com", group: 0, quiet: 420, emoji: "🔭", model: "gpt-5" }, + { did: "did:web:marl-junco.agents.example.com", name: "marl-junco", handle: "marl-junco.agents.example.com", group: 1, quiet: 60, emoji: "🧪", model: "claude-sonnet" }, + { did: "did:web:chert-tanager.agents.example.com", name: "chert-tanager", handle: "chert-tanager.agents.example.com", group: 1, quiet: 30, emoji: "🛠️", model: "gemini-3" }, ], said: { agent: 0, text: "adding Open Graph tags" }, }; -- knotmirror 0.0.1