Upstream ssb-viewer printed it above every page, aimed at people who had
arrived at a stray scuttlebutt message and needed context. On cust.ooo the
context is the site itself, so it was explaining the wrong thing.
Removes the function, all four call sites and the now-dead .top-tip CSS.
Adds a "Changing how pages look" section to the README, since the real
question was where this lives. There is no template directory - pages are
built as strings in render.js with one <style> block - so the answer is
"grep for the text you can see", and the table maps each visible piece to the
function that emits it. It also records the two things that will bite an
editor: hyperscript silently drops attributes it mistakes for DOM properties
(why loading="lazy" uses setAttribute), and only text passed through h() is
escaped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192zBTNZKZn5svyJ5HTnYds
A `nft: "give"` message is a change of custody, not a thing. It has no photo of
its own, so it borrowed the photo of the item it transferred - which meant the
same object appeared on the grid twice, a few cards apart, distinguished only
by a dimmed image and an id overlay. That reads as a duplicate, not as a
hand-off.
Custody is better answered one level down. Each card already links to its
item's thread, where the mint and every hand-off since are in order, with the
steward resolved. So the overlay was showing a truncated feed id on the grid to
save a click that is worth making.
Filters on nft === "mint" rather than excluding gives: every custodisco message
carries one of the two - checked across all 407 - so matching mint is exact.
301 items from 3 feeds, down from 407 entries.
The feed count now counts feeds that actually contributed an item rather than
the size of the follow set. The pub follows itself and publishes no items, so
the old number was one too high and would have drifted further as feeds get
followed for other reasons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192zBTNZKZn5svyJ5HTnYds
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
Rebrand package.json (name, homepage, repository) so render.js's page footer,
which is built from pkg.homepage + git HEAD, points at this repo rather than
upstream's ssb:// URL.
Track package-lock.json instead of ignoring it. It was gitignored upstream, but
this is a deployed application on a 469MB box running node 18 — a reproducible
dependency tree is the difference between RESTORE.md working and not.
Upstream's README is kept verbatim as UPSTREAM-README.md; the new one documents
the custo data model, including the two things that will otherwise cost someone
an afternoon: custodisco is the string "true", and no message ever sets
content.channel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192zBTNZKZn5svyJ5HTnYds
serveBlob handed any held blob to anyone who knew its hash, on a public
port with require_opt_in:false. Combined with ssb-blobs sympathy that
meant publicly serving 4.6GB of strangers content we never reviewed.
Maintains a set of blob ids referenced by the local log (small, ~360KB,
rescanned every 5 min) and 404s anything outside it. Fails open until the
first scan completes so a read error cannot 404 the whole site.
Verified: a blob present on disk but referenced by no message returns
blobs.has=true over RPC while the viewer 404s it.
Nothing in ssb-server fetches blobs mentioned in replicated messages;
that was happening via ssb-blobs sympathy, which is the same mechanism
that let strangers fill the disk. This replaces it with explicit wants
scoped to feeds we actually replicate.
want() has no expiry and the want map is broadcast to every peer on
connect, so a standing want fires whenever a long-offline peer returns.
sbot keeps that map in memory, so this process ties its lifetime to the
connection and re-arms the backfill on every restart.
Verified: removing a referenced blob makes it report
already_have=266 newly_wanted=1 and issue the want.
ssb-viewer holds one muxrpc handle captured in a closure and has no
reconnect path. When sbot is OOM-killed and restarted by run-server.sh,
the viewer kept port 8807 open while holding a dead connection and never
answered again -- the silent wedge.
Listen for muxrpc closed and exit(1) so the while-loop in
run-ssb-viewer.sh restarts with a fresh connection. Also poll whoami to
catch half-open connections where closed never fires.
Verified: killing sbot now self-heals in ~4s instead of hanging.
- Make sure value is string before passing it to hyperscript,
otherwise it could be an object with property "innerHTML" which would
get included without escaping.
- Make sure value is truthy (or != null) before dereferencing it.
- Make sure value is array before calling array methods on it.