cust.ooo linked to one viewer page per kiosk, so every new kiosk meant
another hand-added link. /items selects on "whatever this pub follows"
instead: follow a new kiosk from the pub and its items appear on the next
cache miss, with no code change and no edit to the site.
serveUserFeed already reduced a feed's contact messages into a follow set;
that moves to lib/follows.js so both routes share one implementation rather
than drifting. It also picks up a fix on the way: the old filter passed
messages with no .value through and then dereferenced .value.content on them.
Two properties of the data drive items.js, both verified against the live log
and both quietly wrong to assume otherwise:
* custodisco is the STRING "true", not a boolean.
* no custo message has ever set content.channel, so the channel index finds
nothing and the filter has to read content fields directly. This is why
/channel/custodisco renders an empty page.
Log order is arrival order, which diverges from publish order whenever an old
feed is backfilled, so the collected set is sorted by timestamp. That makes
"newest first" and the ?before cursor agree; verified as zero overlap between
consecutive pages.
Cards rather than articles: 300 photos in a single column reads like a mailing
list, not an archive. A transfer borrows the photo of the item it transfers and
links to that item's thread, so the grid stays regular and the give is still
legible as a hand-off. Images are lazy, and 267 blobs are held against 301
mints, so a missing photo degrades to a placeholder instead of a broken icon.
serveHome now shows the grid instead of a bare id-lookup box, which was a dead
end for anyone arriving without an id already in hand. The ?id= redirect stays.
The footer commit now comes from .deployed-commit, written by the deploy
script. Deploys are rsync, so the server's checkout keeps whatever HEAD it was
cloned at and `git rev-parse` there names a commit unrelated to the files
actually running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192zBTNZKZn5svyJ5HTnYds
custo-viewer
The web view behind www.cust.ooo — a fork of ssb-viewer that renders custo items from a Scuttlebutt pub.
Upstream's own docs are kept verbatim in UPSTREAM-README.md.
What custo adds
/items |
One merged grid of every item the pub knows about, across all kiosks. New kiosks appear by being followed — no code change. |
| known-blob gate | serveBlob only serves blobs referenced by a feed we replicate, so the node never hands out a stranger's cached blob. |
| exit on dead sbot | bin.js exits when its muxrpc handle dies, instead of holding the port open and hanging every request forever. |
blob-wanter.js |
Standing blobs.want orders for our own feeds' blobs, replacing what ssb-blobs sympathy used to do before it was turned off. |
The data model
An item is an ordinary type: "post" carrying custo's own fields:
{ "type": "post", "custodisco": "true", "nft": "mint",
"text": "\n\n…\n\n a #custodisco item ",
"mentions": [{ "name": "photo.jpg", "type": "image/jpeg", "link": "&…sha256" }] }
A transfer of custody is a reply to that message:
{ "type": "post", "custodisco": "true", "nft": "give",
"target": "@…ed25519", "root": "%…sha256", "branch": "%…sha256" }
Two things to know before writing a query against this:
custodiscois the string"true", not a boolean.- No message ever sets
content.channel. The#custodiscohashtag exists only in post text. Anything keyed on the channel index will silently return nothing — which is why/channel/custodiscorenders an empty page.
Messages are published by the kiosks (/home/trav/custodisco-kiosk/ssb-post.sh), not by
this viewer. The viewer is read-only.
Running it
bin.js connects to a local ssb-server and serves on conf.viewer.port (8807).
See UPSTREAM-README.md for the plugin-vs-standalone options.
Deployment
Deployed by rsync from a laptop, not by git pull — the server holds no push
credentials, and one deploy path is better than two. The script, the systemd units, the
nginx config, and the disaster-recovery runbook all live in the ops repo alongside this
one (documents/custo/ssb-viewer), which is the source of truth for the server itself.
Never commit secrets here. Keys and server config belong in the ops repo.
AGPL-3.0+, inherited from upstream.