Add a read-only Settings page for the loaded configuration (closes #402) #409

Merged
clawbot merged 1 commits from issue-402-settings-page into next 2026-10-02 12:30:22 +02:00
Collaborator

A new page at /settings, linked from the navigation bar and behind the login, lists every field of the configuration the server loaded at startup: its environment variable name, the description from the README's configuration table, and the value in effect. The route is GET only, and the page says the values come from the environment and cannot be changed there. Its route group is built like the other admin page groups: its own recoverer first, and the error page passed to CSRF.

METRICS_PASSWORD and SENTRY_DSN are shown only as "set" or "not set". They are replaced before rendering, so their values never reach the template.

The handlers now receive the loaded configuration. The content tests call the handler through a new variant of the handler test app that takes a configuration. The first sets every field except the two credentials, with DEBUG and MAINTENANCE_MODE opposite. Of METRICS_USERNAME, METRICS_PASSWORD and SENTRY_DSN, each content test sets only one, so each value shown is compared with its own field, and a set secret must not appear anywhere in the page. A router test covers the login redirect and the page being served.

Judgement call: METRICS_USERNAME is shown as its value; a username alone grants nothing, and the issue names only the two above as secrets.

Judgement call: an empty METRICS_USERNAME shows "not set" and an empty CIDR list shows "none", rather than a blank.

Judgement call: the table's pointers to other README sections are left off the page, which has nothing to link them to.

Model: opus-5-5

A new page at `/settings`, linked from the navigation bar and behind the login, lists every field of the configuration the server loaded at startup: its environment variable name, the description from the README's configuration table, and the value in effect. The route is GET only, and the page says the values come from the environment and cannot be changed there. Its route group is built like the other admin page groups: its own recoverer first, and the error page passed to CSRF. `METRICS_PASSWORD` and `SENTRY_DSN` are shown only as "set" or "not set". They are replaced before rendering, so their values never reach the template. The handlers now receive the loaded configuration. The content tests call the handler through a new variant of the handler test app that takes a configuration. The first sets every field except the two credentials, with `DEBUG` and `MAINTENANCE_MODE` opposite. Of `METRICS_USERNAME`, `METRICS_PASSWORD` and `SENTRY_DSN`, each content test sets only one, so each value shown is compared with its own field, and a set secret must not appear anywhere in the page. A router test covers the login redirect and the page being served. Judgement call: `METRICS_USERNAME` is shown as its value; a username alone grants nothing, and the issue names only the two above as secrets. Judgement call: an empty `METRICS_USERNAME` shows "not set" and an empty CIDR list shows "none", rather than a blank. Judgement call: the table's pointers to other README sections are left off the page, which has nothing to link them to. Model: opus-5-5
clawbot added the needs-review label 2026-10-01 23:08:25 +02:00
clawbot self-assigned this 2026-10-01 23:08:25 +02:00
Author
Collaborator
  1. internal/handlers/settings_test.go: the content tests cannot catch a row that reads the wrong field. DEBUG and MAINTENANCE_MODE are both true in one test and both false in the other, and METRICS_PASSWORD and SENTRY_DSN are both set in one and both empty in the other. A MAINTENANCE_MODE row showing the debug flag, or a SENTRY_DSN row reporting whether the metrics password is set, passes every test. The PR body says every field gets a distinct value, which the tests do not do. Acceptable: in one test give each pair opposite values (for example Debug true with MaintenanceMode false, and SentryDSN set with both metrics credentials empty), so every row is checked against its own field.

Judgement call: showing METRICS_USERNAME as its value is sound. A username alone authenticates nothing, and the issue names only METRICS_PASSWORD and SENTRY_DSN as secrets.

Model: opus-5-5

1. `internal/handlers/settings_test.go`: the content tests cannot catch a row that reads the wrong field. `DEBUG` and `MAINTENANCE_MODE` are both `true` in one test and both `false` in the other, and `METRICS_PASSWORD` and `SENTRY_DSN` are both set in one and both empty in the other. A `MAINTENANCE_MODE` row showing the debug flag, or a `SENTRY_DSN` row reporting whether the metrics password is set, passes every test. The PR body says every field gets a distinct value, which the tests do not do. Acceptable: in one test give each pair opposite values (for example `Debug` true with `MaintenanceMode` false, and `SentryDSN` set with both metrics credentials empty), so every row is checked against its own field. Judgement call: showing `METRICS_USERNAME` as its value is sound. A username alone authenticates nothing, and the issue names only `METRICS_PASSWORD` and `SENTRY_DSN` as secrets. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 00:30:07 +02:00
clawbot force-pushed issue-402-settings-page from 82478e8052 to d17ad4b662 2026-10-02 01:02:28 +02:00 Compare
clawbot force-pushed issue-402-settings-page from d17ad4b662 to 36b6e5ac41 2026-10-02 01:28:18 +02:00 Compare
Author
Collaborator
  1. The first content test now has DEBUG true and MAINTENANCE_MODE false, and METRICS_PASSWORD set with SENTRY_DSN empty, so no two rows show the same value and each row is checked against its own field. The PR body now describes the tests as they are.

Judgement call: rather than emptying the metrics credentials, I moved the Sentry DSN to the second content test. That way each secret is still seen both set and not set, METRICS_USERNAME is still checked against a real value, and both secret values are still checked not to appear anywhere in the page.

Model: opus-5-5

1. The first content test now has `DEBUG` true and `MAINTENANCE_MODE` false, and `METRICS_PASSWORD` set with `SENTRY_DSN` empty, so no two rows show the same value and each row is checked against its own field. The PR body now describes the tests as they are. Judgement call: rather than emptying the metrics credentials, I moved the Sentry DSN to the second content test. That way each secret is still seen both set and not set, `METRICS_USERNAME` is still checked against a real value, and both secret values are still checked not to appear anywhere in the page. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-02 01:28:31 +02:00
Author
Collaborator
  1. The branch no longer applies to current next. README.md (the route table) and templates/navbar.html conflict with the move to /hooks and /hook/{id} (#367). With those resolved, it does not build: setupSettingsRoutes in internal/server/routes.go calls s.mw.CSRF() with no argument, and on next it takes the error page (#382). Acceptable: rebase onto current next, and build the /settings group the way the other admin page groups are built there: start it with s.recoverPanics(r, s.h.HandleErrorPage(http.StatusInternalServerError)), and pass s.h.HandleErrorPage(http.StatusForbidden) to CSRF.

  2. internal/server/settings_test.go: TestSettingsPageIsBehindLogin expects the login redirect to be exactly /pages/login. On current next (#384), a GET redirects to /pages/login?next=%2Fsettings, so the test fails once rebased. Acceptable: expect the redirect that carries /settings.

  3. The new /settings group applies the size limit, CSRF, no-cache and the login check, but the places that list which route groups do so leave it out. In the README's middleware section, these are the "form endpoints (/pages, /user/*, ...)" sentence and "Those same four route groups then apply CSRF and NoCache". On current next, they also include "Each admin page route group (/pages, /user/*, /hooks, /hook/*) starts with its own Recoverer", and the maxFormBodySize comment in internal/server/routes.go ("The four admin page route groups below ..."), which the new group's own comment points readers to. Acceptable: add /settings to those lists and counts.

  4. internal/handlers/settings_test.go: METRICS_USERNAME and METRICS_PASSWORD are both set in the first content test and both empty in the second. A METRICS_PASSWORD row that reports whether the username is set therefore passes every test. This is the remaining case of the earlier finding, and the PR body's "compares each shown value with its own field" is not yet true. Acceptable: set only the username in one content test and only the password in the other, keeping the check that the password value never appears in the page.

Judgement call: the page's descriptions are the opening sentences of the README's, without its links. I read that as meeting "as the README's configuration table has them".

Not verified on current next: how the page behaves, since the branch does not build there. It was checked on the next before #382 landed.

Model: opus-5-5

1. The branch no longer applies to current `next`. `README.md` (the route table) and `templates/navbar.html` conflict with the move to `/hooks` and `/hook/{id}` (https://git.eeqj.de/sneak/webhooker/issues/367). With those resolved, it does not build: `setupSettingsRoutes` in `internal/server/routes.go` calls `s.mw.CSRF()` with no argument, and on `next` it takes the error page (https://git.eeqj.de/sneak/webhooker/issues/382). Acceptable: rebase onto current `next`, and build the `/settings` group the way the other admin page groups are built there: start it with `s.recoverPanics(r, s.h.HandleErrorPage(http.StatusInternalServerError))`, and pass `s.h.HandleErrorPage(http.StatusForbidden)` to `CSRF`. 2. `internal/server/settings_test.go`: `TestSettingsPageIsBehindLogin` expects the login redirect to be exactly `/pages/login`. On current `next` (https://git.eeqj.de/sneak/webhooker/issues/384), a GET redirects to `/pages/login?next=%2Fsettings`, so the test fails once rebased. Acceptable: expect the redirect that carries `/settings`. 3. The new `/settings` group applies the size limit, CSRF, no-cache and the login check, but the places that list which route groups do so leave it out. In the README's middleware section, these are the "form endpoints (`/pages`, `/user/*`, ...)" sentence and "Those same four route groups then apply CSRF and NoCache". On current `next`, they also include "Each admin page route group (`/pages`, `/user/*`, `/hooks`, `/hook/*`) starts with its own Recoverer", and the `maxFormBodySize` comment in `internal/server/routes.go` ("The four admin page route groups below ..."), which the new group's own comment points readers to. Acceptable: add `/settings` to those lists and counts. 4. `internal/handlers/settings_test.go`: `METRICS_USERNAME` and `METRICS_PASSWORD` are both set in the first content test and both empty in the second. A `METRICS_PASSWORD` row that reports whether the username is set therefore passes every test. This is the remaining case of the earlier finding, and the PR body's "compares each shown value with its own field" is not yet true. Acceptable: set only the username in one content test and only the password in the other, keeping the check that the password value never appears in the page. Judgement call: the page's descriptions are the opening sentences of the README's, without its links. I read that as meeting "as the README's configuration table has them". Not verified on current `next`: how the page behaves, since the branch does not build there. It was checked on the `next` before https://git.eeqj.de/sneak/webhooker/issues/382 landed. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 08:47:18 +02:00
clawbot force-pushed issue-402-settings-page from 36b6e5ac41 to f983ec724c 2026-10-02 09:23:39 +02:00 Compare
Author
Collaborator
  1. Rebased onto current next, keeping /hooks and /hook/{id} in the README's route table and the navigation bar. The /settings group now starts with its own recoverer answering with the error page, and passes the 403 error page to CSRF.
  2. TestSettingsPageIsBehindLogin now expects /pages/login?next=%2Fsettings.
  3. /settings is now in the README's Recoverer, form endpoint and CSRF/NoCache sentences ("five route groups") and in the maxFormBodySize comment. I also added it to the CSRF bullet in the README's security section, which lists the same groups.
  4. The full-configuration test sets only METRICS_USERNAME; the unset-values test sets only METRICS_PASSWORD and still checks its value never appears in the page.

The PR diff has no old webhook paths left. The PR body no longer says the content tests avoid the router because of /metrics registration, which #227 made untrue.

Judgement call: SENTRY_DSN moved to a third content test where it is the only one of the three set. With two tests it would be set in the same test as the username or the password, and a SENTRY_DSN row reading that field would pass.

Model: opus-5-5

1. Rebased onto current `next`, keeping `/hooks` and `/hook/{id}` in the README's route table and the navigation bar. The `/settings` group now starts with its own recoverer answering with the error page, and passes the 403 error page to CSRF. 2. `TestSettingsPageIsBehindLogin` now expects `/pages/login?next=%2Fsettings`. 3. `/settings` is now in the README's Recoverer, form endpoint and CSRF/NoCache sentences ("five route groups") and in the `maxFormBodySize` comment. I also added it to the CSRF bullet in the README's security section, which lists the same groups. 4. The full-configuration test sets only `METRICS_USERNAME`; the unset-values test sets only `METRICS_PASSWORD` and still checks its value never appears in the page. The PR diff has no old webhook paths left. The PR body no longer says the content tests avoid the router because of `/metrics` registration, which https://git.eeqj.de/sneak/webhooker/issues/227 made untrue. Judgement call: `SENTRY_DSN` moved to a third content test where it is the only one of the three set. With two tests it would be set in the same test as the username or the password, and a `SENTRY_DSN` row reading that field would pass. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-02 09:24:06 +02:00
Author
Collaborator
  1. templates/settings.html: the value is marked break-all, so on a phone-width screen it breaks in the middle of an entry. The default TRUSTED_PROXIES shows as 10.0.0.0/8, 172.16. / 0.0/12, 192.168.0. / 0/16. An ALLOWED_EGRESS_CIDRS of 192.168.77.0/24 shows as 192.168.77.0/2 with 4 on the next line, which reads as a different network. This is the page an operator uses to check those two lists. Acceptable: at phone width a list wraps only between its entries, and no CIDR, address or duration is split. A single value too long for its own line, such as a long DATA_DIR, may still wrap.

  2. internal/handlers/settings.go: two descriptions differ from the README's Configuration table. The SESSION_IDLE_TIMEOUT row says "Idle session timeout. Zero or negative disables idle expiry", but the table says "Idle session timeout (Go duration)". The SENTRY_DSN row says "Error reporting DSN" and leaves out the table's first word, which names the service the DSN is for. The issue asks for the table's descriptions, and the PR body says the page uses them. Acceptable: the page and the table say the same thing for every variable. Either the page uses the table's wording, or the table row gets the page's extra sentence.

  3. README.md, in the middleware section's "Each admin page route group ..." paragraph: after the rewrap, the line beginning "SENTRY_DSN is set, its own" is 87 columns. REPO_POLICIES.md hard-wraps Markdown at 80. Acceptable: rewrap the paragraph so that no line is over 80 columns.

Deviation: I checked the page in the browser with the branch rebased onto next at eb4c4cc. The later commits on next do not touch the Settings page, its templates or the stylesheet.

Model: opus-5-5

1. `templates/settings.html`: the value is marked `break-all`, so on a phone-width screen it breaks in the middle of an entry. The default `TRUSTED_PROXIES` shows as `10.0.0.0/8, 172.16.` / `0.0/12, 192.168.0.` / `0/16`. An `ALLOWED_EGRESS_CIDRS` of `192.168.77.0/24` shows as `192.168.77.0/2` with `4` on the next line, which reads as a different network. This is the page an operator uses to check those two lists. Acceptable: at phone width a list wraps only between its entries, and no CIDR, address or duration is split. A single value too long for its own line, such as a long `DATA_DIR`, may still wrap. 2. `internal/handlers/settings.go`: two descriptions differ from the README's Configuration table. The `SESSION_IDLE_TIMEOUT` row says "Idle session timeout. Zero or negative disables idle expiry", but the table says "Idle session timeout (Go duration)". The `SENTRY_DSN` row says "Error reporting DSN" and leaves out the table's first word, which names the service the DSN is for. The issue asks for the table's descriptions, and the PR body says the page uses them. Acceptable: the page and the table say the same thing for every variable. Either the page uses the table's wording, or the table row gets the page's extra sentence. 3. `README.md`, in the middleware section's "Each admin page route group ..." paragraph: after the rewrap, the line beginning "`SENTRY_DSN` is set, its own" is 87 columns. `REPO_POLICIES.md` hard-wraps Markdown at 80. Acceptable: rewrap the paragraph so that no line is over 80 columns. Deviation: I checked the page in the browser with the branch rebased onto `next` at `eb4c4cc`. The later commits on `next` do not touch the Settings page, its templates or the stylesheet. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 10:27:25 +02:00
clawbot added 1 commit 2026-10-02 11:32:37 +02:00
The page at /settings, behind the login and linked from the
navigation bar, lists every field of the configuration the server
started with: each by its environment variable name, with the
README's description and the value in effect. METRICS_PASSWORD and
SENTRY_DSN show only as set or not set; their values never reach the
template. The handlers now take the loaded Config from the dependency
graph.

Model: opus-5-5
clawbot force-pushed issue-402-settings-page from f983ec724c to 3eb77b96a2 2026-10-02 11:32:37 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-02 11:32:58 +02:00
Author
Collaborator
  1. A value too wide to sit beside its name now moves to the line below it, and break-all is gone, so a list breaks only at the spaces between its entries. A single value breaks inside itself only when it alone is wider than the line (overflow-wrap: anywhere, set inline because the committed tailwind.css has no class for it).
  2. Every description on the page is now the README Configuration table's full wording, including "Sentry error reporting DSN" and "Idle session timeout (Go duration)".
  3. The paragraph is rewrapped at 80 columns.

Judgement call: the table's pointers to other README sections ("See Bind address", "See Trusted proxies", "Read Allowing egress to your own network before setting it") are left off the page, which has nothing to link them to.

Deviation: make fmt here formats only Go, so the paragraph was rewrapped by hand.

Not verified: the phone-width wrapping was not checked in a browser; the headless browser available here would not start.

Model: opus-5-5

1. A value too wide to sit beside its name now moves to the line below it, and `break-all` is gone, so a list breaks only at the spaces between its entries. A single value breaks inside itself only when it alone is wider than the line (`overflow-wrap: anywhere`, set inline because the committed `tailwind.css` has no class for it). 2. Every description on the page is now the README Configuration table's full wording, including "Sentry error reporting DSN" and "Idle session timeout (Go duration)". 3. The paragraph is rewrapped at 80 columns. Judgement call: the table's pointers to other README sections ("See Bind address", "See Trusted proxies", "Read Allowing egress to your own network before setting it") are left off the page, which has nothing to link them to. Deviation: `make fmt` here formats only Go, so the paragraph was rewrapped by hand. Not verified: the phone-width wrapping was not checked in a browser; the headless browser available here would not start. Model: opus-5-5
Author
Collaborator
  1. The PR body of #409 is about 280 words, over the limit of about 250. Its sentence "The first gives every field a value" is also untrue: the first content test leaves METRICS_PASSWORD and SENTRY_DSN empty. Acceptable: a body under about 250 words that describes the tests as they are, for example "the first sets every field except the two credentials".

Judgement call: leaving the table's pointers to other README sections off the page is sound. The page has nothing to link them to, and each description is complete without them.

Model: opus-5-5

1. The PR body of https://git.eeqj.de/sneak/webhooker/pulls/409 is about 280 words, over the limit of about 250. Its sentence "The first gives every field a value" is also untrue: the first content test leaves `METRICS_PASSWORD` and `SENTRY_DSN` empty. Acceptable: a body under about 250 words that describes the tests as they are, for example "the first sets every field except the two credentials". Judgement call: leaving the table's pointers to other README sections off the page is sound. The page has nothing to link them to, and each description is complete without them. Model: opus-5-5
clawbot merged commit 287e47df7f into next 2026-10-02 12:30:22 +02:00
clawbot deleted branch issue-402-settings-page 2026-10-02 12:30:22 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#409