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
106 lines
4.3 KiB
Markdown
106 lines
4.3 KiB
Markdown
# custo-viewer
|
|
|
|
The web view behind **[www.cust.ooo](https://www.cust.ooo)** — a fork of
|
|
[ssb-viewer](https://gitlab.com/dwynen/ssb-viewer) that renders custo items from a
|
|
Scuttlebutt pub.
|
|
|
|
Upstream's own docs are kept verbatim in [UPSTREAM-README.md](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:
|
|
|
|
```json
|
|
{ "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:
|
|
|
|
```json
|
|
{ "type": "post", "custodisco": "true", "nft": "give",
|
|
"target": "@…ed25519", "root": "%…sha256", "branch": "%…sha256" }
|
|
```
|
|
|
|
Two things to know before writing a query against this:
|
|
|
|
- `custodisco` is the **string** `"true"`, not a boolean.
|
|
- **No message ever sets `content.channel`.** The `#custodisco` hashtag exists only in
|
|
post text. Anything keyed on the channel index will silently return nothing — which is
|
|
why `/channel/custodisco` renders 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.
|
|
|
|
## Changing how pages look
|
|
|
|
Everything visual is in **`render.js`**. There is no template directory and no CSS
|
|
files to hunt through — pages are built as strings and there is exactly one `<style>`
|
|
block, near the middle of the file.
|
|
|
|
Find things by searching for the text you can see on the page. `grep -n` on a phrase
|
|
from the rendered page lands you on the line that produces it:
|
|
|
|
```bash
|
|
grep -n "Join Scuttlebutt" render.js
|
|
```
|
|
|
|
The pieces, in the order you meet them on a page:
|
|
|
|
| What you see | Where |
|
|
|---|---|
|
|
| `<title>` and the whole `<head>` | `wrapPage()` |
|
|
| "custo items", the count, the search box | `itemsHeader()` |
|
|
| an item card: photo, caption, kiosk, time | `renderItemCard()` |
|
|
| "older items" / "show everything" | `itemsFooter()` |
|
|
| the "Join Scuttlebutt now" button | `callToAction()` |
|
|
| licence, repo link, deployed commit | the `footer` constant |
|
|
| all colours, spacing, the grid itself | the `styles` template string |
|
|
|
|
The grid rules are the `.item-*` selectors in `styles`. Card width is one number —
|
|
`minmax(190px, 1fr)` in `main.item-grid` — raise it for fewer, larger cards.
|
|
|
|
Two gotchas worth knowing before you edit:
|
|
|
|
- **Attributes set through `h()` can vanish.** hyperscript assigns anything that looks
|
|
like a DOM property rather than an attribute, and `outerHTML` then drops it. That is
|
|
why `loading="lazy"` is applied with `media.setAttribute(...)` instead. If an
|
|
attribute you added does not appear in the output, this is why.
|
|
- **Text passed to `h()` is escaped; strings concatenated into `wrap()` are not.**
|
|
Anything derived from a message must go through `h()`.
|
|
|
|
To see a change: edit, then from the ops repo
|
|
|
|
```bash
|
|
bin/deploy-viewer.sh --yes
|
|
```
|
|
|
|
which syncs, restarts the viewer and checks `/items` answers before it returns. Commit
|
|
and push separately — the deploy only moves files.
|
|
|
|
## 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.
|