igneum/docs/community/discord-hooks.md
igneum-labs 8ca045eb5f Discord structure: the build sheet (channel map, moderation, onboarding, bots, events) and the hourly network feed (7 October 2026, 11:4x UK)
docs/community/discord/structure.md: every channel with topic, pinned first post, slowmode and thread settings; the
AutoMod rules; the rules screen; the server guide to-dos and the pre-join question aligned with the map; the two bot
specs; the office-hour template; what needs the founder. Every row carries "set / read back"; all wait on the Discord
sign-in (the session expired; nothing typed).

tools/community/discord-hooks.mjs: `feed` (hourly: height, hash rate, keys, the last lock from /api/live), milestones
posted once each, the daily hash-origin field, `--via updates`, optional DISCORD_WEBHOOK_FEED, the tick wiring. Six
tests, 37 pass. One live post through the updates webhook as the proof of the path (feed:2026-10-07:11).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 12:14:12 +00:00

90 lines
7.9 KiB
Markdown

# Discord webhooks: the bot's posts to #numbers, #announcements and #incidents
6 October 2026. Three channel webhooks named "Igneum" (the Bot role in the server kit, docs/community/reddit/discord.md)
carry the project's numbers to the Discord server. One script, `tools/community/discord-hooks.mjs` (Node 22, standard
library only), shapes every post as a single embed: ember accent (#F2541B), the site icon as thumbnail, plain words, at most
eight fields, UK time in the footer with UTC in brackets, never a mention, never a reply. Dry run is the default.
## The posts
| Post | Channel | When (UK) | Content |
|---|---|---|---|
| Network pulse | #numbers | 03:00, 09:00, 15:00, 21:00 | chain block and blocks/s over the 6 h window, hash rate and active vote keys ("a card runs several"), difficulty and its 6 h change, last lock (index, share of weight, age, voters), shards paid in the window and IGN paid to provers in the last 5 min, median proof lag and provers, reward and ramp, miner version share when /api/live exposes engines, Devnet 2 in one line (boxes, height, last gate PASS or FAIL) |
| Daily digest | #numbers | 09:00 | a small monospace table, yesterday against now, with the 24 h deltas |
| Weekly numbers | #numbers | Monday 09:00 | the reddit kit's template (docs/community/reddit/posts/02-weekly-numbers-template.md) as six fields with the last-week column, a link to the ledger |
| Release | #announcements | on a publish, by the shipper | version, three download links with full sha256, node commit, consensus digest, up to five "what changed" lines read from the release plan |
| Incident open / resolve | #incidents | by hand, or by the watcher | UTC time, what happened, who is affected per tier, what is being done; then cause, the rule or fix, duration |
| Network feed | #network-feed | every hour, when `DISCORD_WEBHOOK_FEED` is set | height and blocks/s over the hour, hash rate and difficulty, keys active and voters, the last lock; the daily hash-origin field once per report date from `/api/live state.hash_origin`; milestones once each (every 100,000 blocks, 10,000 locks, 10,000 paid shards; the first crossing of a vote-key count and of a hash-rate step). Added 7 October 2026; docs/community/discord/structure.md section 4.1 |
Every number comes from the public API (`/api/stats`, `/api/live?window=300`, `/api/supply`; docs/api/public-stats.md). The
Devnet 2 line reads the fleet file and uses its numbers and the gate word only; the seed address and the state text never pass.
## The watcher
`watch` (and every `tick`) opens one incident per condition and resolves it when the condition has been clear for 2 minutes.
One open incident per condition at a time; the state file carries since / clear_since / open.
| Condition | Opens when | Text |
|---|---|---|
| finality_paused | `finality.active` false, or the locked index unchanged, for over 5 min | finality paused; miners and provers paid as usual; nothing final until weight returns |
| proof_lag | `proving.median_proof_lag_s` over 900 s | proving behind; blocks final as usual, proofs late |
| observer_silent | `stale` with `age_s` over 180 s, or the API unreachable for over 3 min | the live numbers are stale; the chain is unaffected |
The watcher's texts are fixed sentences in the script, each a stated rule, and are the only incidents nobody typed.
## Commands
```
node tools/community/discord-hooks.mjs pulse | digest | weekly [--live] [--force]
node tools/community/discord-hooks.mjs feed [--live] [--via updates] the hourly feed; --via updates posts it through DISCORD_WEBHOOK_UPDATES until #network-feed has a webhook
node tools/community/discord-hooks.mjs release 0.3.14 --windows <url> --windows-sha <hex> --mac <url> --mac-sha <hex> --hive <url> --hive-sha <hex> \
--node-commit 4c6b129d --digest "b18ed271 (thirteen fields, unchanged)" --plan docs/plans/release-0.3.14.md --section "1. Why" [--changed "line"]... [--live]
node tools/community/discord-hooks.mjs incident open --what "..." --affected "..." --doing "..." [--at 2026-10-06T15:44:00Z] [--id inc-...] [--live]
node tools/community/discord-hooks.mjs incident resolve --id inc-... --cause "..." --fix "..." [--at ...] [--live]
node tools/community/discord-hooks.mjs watch [--live] one watcher pass
node tools/community/discord-hooks.mjs tick [--live] one scheduler pass (what the London clock says is due, then watch)
node tools/community/discord-hooks.mjs preview rebuild tools/community/out/preview.html
node tools/community/discord-hooks.mjs check which webhooks are configured (names, never values)
node --test tools/community/discord-hooks.test.mjs
```
Dry run writes `tools/community/out/<key>.json` (git-ignored) and renders `out/preview.html`, a Discord-like dark page of
every payload in the folder. The release subcommand takes the "what changed" lines from `--changed` flags or from the named
section of the release plan: bullet lines as they are, table rows by their first cell, cut at the first semicolon, a long
parenthetical dropped, 160 characters at most; a line that trips the guard is dropped with a note on stderr.
## Guard rails
| Rail | How |
|---|---|
| Forbidden strings | before every post: the founder's name and logins, hosting providers, internal host names, machine ids, internal paths, IP addresses, a standalone 32-hex token (a sha256 passes), any dl.igneum.network path outside /public/, a webhook URL, any mention; the post is refused with the pattern named |
| Limits | title 256, description 4,096, field value 1,024, eight fields, 6,000 in total, content 2,000; refused, never cut silently |
| Idempotency | a post id file (`posts` in the state) keyed `pulse:<date>:<hour>`, `digest:<date>`, `weekly:<date>`, `release:<version>`, `incident:<id>:open|resolve`; a rerun is skipped, `--force` posts again |
| 429 | exponential backoff from 1 s, doubling to 60 s, `retry_after` honoured, six tries, then exit 1 |
| State | written only after a successful live post; a dry run keeps its own state in out/state.json |
| Mentions | `allowed_mentions: {parse: []}` on every payload and a guard that refuses @everyone, @here and user mentions in the text |
## Credentials and deployment
`~/.config/igneum/discord`, mode 600, KEY=VALUE lines: `DISCORD_WEBHOOK_NUMBERS`, `DISCORD_WEBHOOK_ANNOUNCEMENTS`,
`DISCORD_WEBHOOK_INCIDENTS`; optional `DISCORD_WEBHOOK_FEED` (no key, no feed, one "feed off" note per tick) and
`DISCORD_WEBHOOK_UPDATES` (the hidden updates channel, the `--via updates` route). Never in the repository, never printed;
`check` prints which keys are set.
The scheduler runs on igneum-build-1 because the Mac sleeps: `infra/build-server/discord-hooks/{igneum-discord-hooks.service,
igneum-discord-hooks.timer, install.sh}`, a tick every minute. `install.sh` copies the script to `/srv/discord-hooks/bin`, the
credentials file to `/srv/discord-hooks/env` (mode 600, owner build, over ssh stdin) and enables the timer. The repository rule
says the box never holds a secret; a webhook URL is one, so the script refuses unless `IGNEUM_SECRET_ON_BOX_OK=1` is set,
which records the coordinator's ruling. A leaked webhook is deleted in Discord and the file replaced.
## Tested, 6 October 2026
Dry run against the live API at 18:51 UTC rendered all six posts (pulse 641 characters, digest 477, weekly 1,699, release
1,501, incident open 716 and resolve 659). The guard refused the fleet file's seed address and a line with an internal path
from the release plan. Finality was paused at render time (`finality.active` false, last lock 6842 at about 18:42Z, the newest
checkpoints at 55% of total weight): the watcher's first condition, seen in dry run. Not yet run: a live post (no credentials
file yet), the install on the box.
## The server invite
The standing invite is https://discord.gg/igneum (created 6 October 2026, main; the vanity names discord.gg/igneum and discord.gg/igneumnetwork were free on 3 October and are not yet claimed). The home page's join line and any public text use this URL; nobody asks for it again.