4 Commits
Author SHA1 Message Date
clawbot eda8e80844 Fix six README sentences (closes #565)
check / check (push) Waiting to run
Rewrite the six sentences the documents check failed, each checked
against the code. The Sign-in limits section now opens with its rule.
The recovery routine's sentences say it resumes a delivery: it queues a
pending one at once, but gives a retrying one the rest of its wait.

Model: opus-5-5
2026-10-26 06:53:59 +01:00
clawbot e584718eb5 Commit Alpine.js to the repo instead of unpacking it at build time (closes #163)
check / check (push) Waiting to run
static/js/alpine.min.js is now committed: package/dist/cdn.min.js from
the @alpinejs/csp 3.14.9 npm package, byte for byte, taken once from the
package file that was in 3p/. The build no longer unpacks it, so
script/assets, the assets make target, its Dockerfile steps and the
ignore entries for the file are gone, and so is the package file.
Prettier and ESLint both ignore the file, so it stays as published.
Comments and the README now describe the committed file.

Model: opus-5-5
2026-10-26 05:16:05 +01:00
clawbot a8d226942e Rewrite TODO.md sentence by sentence (closes #558)
check / check (push) Canceled after 0s
Rewritten from scratch, sections kept, every statement checked against the code. Passed independent review in round 4.

Model: fable-5-1 (writing); opus-5-5 (review, merge)
2026-10-25 12:05:48 +01:00
clawbot 6df1fc7875 Rewrite README.md sentence by sentence (#561)
check / check (push) Canceled after 0s
Rewritten from scratch, about 4,600 words, every statement checked against the code. Part of #558; passed independent review in round 10.

Model: fable-5-1 (writing); opus-5-5 (review, merge)
2026-10-25 12:05:41 +01:00
14 changed files with 660 additions and 3482 deletions
+1 -3
View File
@@ -72,10 +72,8 @@
**/*.sublime-* **/*.sublime-*
# This repository's own host-built artifacts: the binary `make build` # This repository's own host-built artifacts: the binary `make build`
# writes, and the Alpine.js file `make assets` extracts from 3p/ (the # writes.
# build extracts its own).
/bin /bin
/static/js/alpine.min.js
# SQLite databases, which hold the session key and webhook payloads, at # SQLite databases, which hold the session key and webhook payloads, at
# any depth. # any depth.
-4
View File
@@ -77,7 +77,3 @@ data/
# Temporary files # Temporary files
tmp/ tmp/
temp/ temp/
# Alpine.js, extracted by `make assets` from its tarball in 3p/, which is
# what is committed.
/static/js/alpine.min.js
+1
View File
@@ -1,2 +1,3 @@
node_modules/ node_modules/
yarn.lock yarn.lock
static/js/alpine.min.js
Binary file not shown.
+2 -11
View File
@@ -92,10 +92,6 @@ COPY . .
# fmt-check` is the markdown-check stage above. # fmt-check` is the markdown-check stage above.
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
# static/static.go embeds the Alpine.js file this extracts from 3p/; without
# it the static package does not compile and cannot be linted.
RUN script/assets
# The golangci-lint steps run with --network=none. `golangci-lint config # The golangci-lint steps run with --network=none. `golangci-lint config
# verify` is documented as fetching its JSON schema over HTTPS; this pinned # verify` is documented as fetching its JSON schema over HTTPS; this pinned
# image resolves the schema without any network, and --network=none enforces # image resolves the schema without any network, and --network=none enforces
@@ -130,9 +126,6 @@ RUN go mod download
COPY . . COPY . .
# static/static.go embeds the Alpine.js file this extracts from 3p/.
RUN script/assets
# -timeout applies to each package on its own, so 90s has only to clear the # -timeout applies to each package on its own, so 90s has only to clear the
# slowest one. -p 4 -parallel 8 keep the run under 2 GB of memory: at most # slowest one. -p 4 -parallel 8 keep the run under 2 GB of memory: at most
# four test binaries build or run at once, each with at most eight parallel # four test binaries build or run at once, each with at most eight parallel
@@ -211,10 +204,8 @@ RUN version="$(make version VERSION="$VERSION")"; \
esac; \ esac; \
fi fi
# Builds through the Makefile's build target, which runs script/assets # The Makefile's build target builds with cgo off, so the binary is
# (Alpine.js, extracted from its tarball in 3p/) first. The target builds # statically linked and runs on the Alpine runtime image below.
# with cgo off, so the binary is statically linked and runs on the Alpine
# runtime image below as it is.
RUN make build VERSION="$VERSION" RUN make build VERSION="$VERSION"
# Runtime stage # Runtime stage
+1 -1
View File
@@ -17,7 +17,7 @@ COPY . .
# The test binary embeds the templates and static files, so the browser # The test binary embeds the templates and static files, so the browser
# stage needs nothing else. It is built with cgo off and -p 4 keeps the # stage needs nothing else. It is built with cgo off and -p 4 keeps the
# compile's memory down, both as in the test phase of Dockerfile. # compile's memory down, both as in the test phase of Dockerfile.
RUN make assets && CGO_ENABLED=0 go test -c -p 4 -tags browser -o /browser.test ./internal/server RUN CGO_ENABLED=0 go test -c -p 4 -tags browser -o /browser.test ./internal/server
# chromedp/headless-shell:151.0.7922.109 (Debian trixie), 2026-08-11. The # chromedp/headless-shell:151.0.7922.109 (Debian trixie), 2026-08-11. The
# browser is on PATH as headless-shell, where the test's browser library # browser is on PATH as headless-shell, where the test's browser library
+3 -6
View File
@@ -1,4 +1,4 @@
.PHONY: bootstrap setup assets test test-browser lint fmt fmt-check check build run dev deps docker clean hooks css css-check version .PHONY: bootstrap setup test test-browser lint fmt fmt-check check build run dev deps docker clean hooks css css-check version
# Default target # Default target
.DEFAULT_GOAL := check .DEFAULT_GOAL := check
@@ -22,9 +22,6 @@ bootstrap:
setup: setup:
@script/setup @script/setup
assets:
@script/assets
test: test:
@script/test @script/test
@@ -47,13 +44,13 @@ check:
# REPO_POLICIES.md does: the binary carries no build paths, symbol table or # REPO_POLICIES.md does: the binary carries no build paths, symbol table or
# debug information, and on Linux, the image's included, it is statically # debug information, and on Linux, the image's included, it is statically
# linked. # linked.
build: assets build:
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=$(VERSION)' -o bin/webhooker ./cmd/webhooker CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=$(VERSION)' -o bin/webhooker ./cmd/webhooker
run: build run: build
./bin/webhooker ./bin/webhooker
dev: assets dev:
go run ./cmd/webhooker go run ./cmd/webhooker
deps: deps:
+618 -3419
View File
File diff suppressed because it is too large Load Diff
+17 -14
View File
@@ -4,15 +4,18 @@ One issue per unit of work, and one branch and one PR per issue:
- make sure a tracked issue exists with a definition of done - make sure a tracked issue exists with a definition of done
- branch from `next`, never from `main` - branch from `next`, never from `main`
- do the work, then open a PR with base `next`, never `main` - do the work and push the branch
- open a PR with base `next`, never `main`
- pass an independent review - pass an independent review
- the repository manager, the role that merges every PR, then squash-merges it - the repository manager squash-merges it into `next`
into `next`
- push; nothing stays local-only The repository manager is the role that merges each issue's PR into `next`.
Nothing stays local-only: every commit is pushed.
`next` is the branch for the next milestone. It must stay green and mergeable `next` is the branch for the next milestone. It must stay green and mergeable
into `main` at all times. A milestone's work reaches `main` through one PR from into `main` at all times. A milestone's work reaches `main` through one PR from
`next`. Cutting a release from `main` is a separate step after that. `next`. The owner, @sneak, merges that PR. Cutting a release from `main` is a
separate step after that.
The tracker (https://git.eeqj.de/sneak/webhooker/issues) records what is done The tracker (https://git.eeqj.de/sneak/webhooker/issues) records what is done
and what is under way. This file keeps no list of either. and what is under way. This file keeps no list of either.
@@ -26,20 +29,20 @@ What is still open for 1.0.0 is in its milestone
Delivery is at least once by design. When webhooker cannot record the outcome of Delivery is at least once by design. When webhooker cannot record the outcome of
a delivery, it sends that delivery again. The target can then receive the same a delivery, it sends that delivery again. The target can then receive the same
event twice. The alternative is a lost delivery that nothing reports. The README event twice. The alternative is a lost delivery that nothing reports. The README
says so under How it works. Such a duplicate is not a defect to re-file. says so. Such a duplicate is not a defect to re-file.
# Next Step # Next Step
@sneak runs webhooker in limited production, then tags `v1.0.0`. Merging `next` @sneak runs webhooker in limited production, then tags `v1.0.0`. Merging `next`
into `main` is a separate act. It waits on neither the production run nor the into `main` is a separate act. It waits on neither the production run nor the
tag. `next` is kept mergeable at all times; that is the point of the branch. tag.
# Future Steps # Future Steps
- Per-webhook rate limits, set in the web UI and enforced at the entrypoint. - Per-webhook rate limits, set in the web UI and enforced at the entrypoint.
Today `RECEIVER_RATE_LIMIT` is the only setting. It is how many requests per Today `RECEIVER_RATE_LIMIT` is the only rate-limit setting. It is how many
minute one client address may send to one entrypoint. The same address is also requests per minute one client address may send to one entrypoint. The same
capped at ten times that across all entrypoints together. address is also capped at ten times that across all entrypoints together.
- API key authentication for programmatic access. The `APIKey` model and its - API key authentication for programmatic access. The `APIKey` model and its
`api_keys` table exist. Nothing creates a key and nothing checks one. `api_keys` table exist. Nothing creates a key and nothing checks one.
- REST API v1. `/api/v1` is mounted with no routes today. - REST API v1. `/api/v1` is mounted with no routes today.
@@ -56,16 +59,16 @@ tag. `next` is kept mergeable at all times; that is the point of the branch.
It lasts up to 7 days. It ends sooner after `SESSION_IDLE_TIMEOUT` (default 24 It lasts up to 7 days. It ends sooner after `SESSION_IDLE_TIMEOUT` (default 24
hours) without use. hours) without use.
- Password reset over the web for a forgotten password. Today a signed-in user - Password reset over the web for a forgotten password. Today a signed-in user
can change their password. A lost password is reset from the console with can change their password. A lost password is reset from the command line with
`webhooker resetpw` (https://git.eeqj.de/sneak/webhooker/issues/208). `webhooker resetpw` (https://git.eeqj.de/sneak/webhooker/issues/208).
- Later, nice to have - Later, nice to have
- email delivery target type - email delivery target type
- SNS and S3 delivery targets - SNS and S3 delivery targets
- data transformations, such as a configurable Slack message format. The - data transformations, such as a configurable Slack message format. The
Slack target sends one fixed format today. Slack target sends one fixed format today.
- JSONL file delivery with periodic S3 upload. Today the `database` target - JSONL file delivery with periodic S3 upload. Today an archive target
archives events to SQLite files. A target can rotate those files hourly, writes events to SQLite files. An archive target can rotate those files
daily or monthly. The webhook page offers them as one gzipped JSON hourly, daily or monthly. The webhook page offers them as one gzipped JSON
download. download.
- webhook event search, and filtering beyond the event log's failed and - webhook event search, and filtering beyond the event log's failed and
pending views pending views
+1 -1
View File
@@ -5,7 +5,7 @@
// REPO_POLICIES.md states that a linter can check: const for everything, // REPO_POLICIES.md states that a linter can check: const for everything,
// let only for a variable that is reassigned, never var. // let only for a variable that is reassigned, never var.
export default [ export default [
// Alpine.js, extracted from 3p/ by make assets; not ours to lint. // Alpine.js, committed as published; not ours to lint.
{ ignores: ["static/js/alpine.min.js"] }, { ignores: ["static/js/alpine.min.js"] },
{ {
// The pages load static/js/app.js as a classic script, not a module. // The pages load static/js/app.js as a classic script, not a module.
-3
View File
@@ -13,9 +13,6 @@ import (
// TestBaseTemplateScriptsAreServed walks every /s/ script the base // TestBaseTemplateScriptsAreServed walks every /s/ script the base
// template loads on each page and fetches it through the real router. // template loads on each page and fetches it through the real router.
// Alpine.js is extracted from its tarball in 3p/ at build time, so the
// file is not in the tree: this is the check that the page still gets
// the JavaScript it asks for.
func TestBaseTemplateScriptsAreServed(t *testing.T) { func TestBaseTemplateScriptsAreServed(t *testing.T) {
t.Parallel() t.Parallel()
-17
View File
@@ -1,17 +0,0 @@
#!/bin/sh
# script/assets: extract Alpine.js from its npm package tarball, committed
# in 3p/, to static/js/alpine.min.js, where go:embed reads it. The package
# is @alpinejs/csp, Alpine's build for pages whose Content-Security-Policy
# forbids eval. The extracted file is not committed. make build, make dev
# and the Dockerfile's lint, test and build stages run this first.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
tar -xzOf 3p/alpinejs-csp-3.14.9.tgz package/dist/cdn.min.js \
>static/js/alpine.min.js
}
main "$@"
+11
View File
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -6,9 +6,11 @@ import (
) )
// Static holds the CSS and JavaScript files the web UI's pages load. They // Static holds the CSS and JavaScript files the web UI's pages load. They
// are named one by one so that a missing js/alpine.min.js, which make // are named one by one, so a missing file fails the build instead of
// assets extracts and git does not track, fails the build instead of // leaving the pages without it.
// leaving the pages without Alpine.js. //
// js/alpine.min.js is Alpine.js, committed unchanged. It is
// package/dist/cdn.min.js from the npm package @alpinejs/csp 3.14.9.
// //
//go:embed css/tailwind.css css/style.css js/app.js js/alpine.min.js //go:embed css/tailwind.css css/style.css js/app.js js/alpine.min.js
var Static embed.FS var Static embed.FS