Setting up the Discord bot used to mean editing your compose file and recreating the container. @NotRetarded wrote a screenshot-by-screenshot guide to walk people through it (#57), then asked the obvious question: could this not just be in the interface?
A Connections page
Yes. And once the Discord fields were in, the Settings page's Channels tab was the longest thing on it — with SMTP, ntfy, Matrix, Apprise and Gotify still environment-only and still to come, another twenty-five fields would have finished it off.
So the notification channels have a page of their own now. One card per channel, each readable on its own. It also matches how you think about it: the rest of Settings is about when Docksentry acts, this is about where it talks.
Telegram topic and allowed users, the bot label, both webhooks, and the Discord bot all live there.
The Discord bot, and what it tells you
Bot token, application ID, server ID and the allowed-user list. Saving restarts the bot and says what happened:
Saved, but Discord rejected the token. (HTTP 401: {"message": "401: Unauthorized"})
That is the part worth building. start() always logged its reasons, which was fine while the only way to change the credentials was editing compose and recreating the container — the log was right in front of you. The moment you can type a token into a form, the console is somewhere else entirely, and "nothing happened" is the worst possible answer to a typo.
The token is masked in the field, never rendered back into the page, kept out of the logs and redacted in the audit trail. Removing it is a separate checkbox, because an empty password field has always meant "leave it alone" here and cannot also mean "delete it".
These were environment-only on the stated grounds that a credential has no business in settings.json. That does not survive looking at the file — it already holds your Web UI password in plaintext and webhook URLs whose path is the credential, and it is written 0600 for exactly that reason.
Three fixes, all of which made something look broken when it was not
An empty log page answered with nothing at all. Not an error, not an empty page — the connection just closed. v1.73.0 merged a container's two output streams so /logs stopped showing half of them (#2); subprocess then leaves .stderr as None, and every caller here reads result.stdout or result.stderr, which is None the moment stdout is empty. Containers that had written something were never affected, which is how it got past a release.
While fixing it: Telegram's /logs, Discord's /logs and the crash diagnostics never got that v1.73.0 merge at all. Each built its own command line instead of calling the method that does the merging, so all three showed one stream and dropped the other — and for a container that writes its errors to stderr, the dropped half is the half you opened the logs to read.
No text field on the Settings page could be emptied. Set a Discord webhook, clear the field, save: the page said "saved" and the old URL was still there. parse_qs drops name= with an empty value, so the one submission meaning "clear this" was indistinguishable from the field never having been sent. Same for the webhook URL, the Telegram topic ID, both allowed-user lists, the bot label and the quiet hours.
A browser hanging up no longer looks like a crash. @NotRetarded found thirteen lines of Python internals in his container log and filed them, reasonably, as a bug (#58) — ConnectionResetError: [Errno 104] Connection reset by peer, with nothing anywhere saying "a client on your LAN closed a socket". The reset is ordinary and unavoidable. The traceback was the defect: a log line that sends you to open an issue about a healthy system has cost you time and told you nothing. Those now print one line, and only with DEBUG on. Everything else escaping a request thread keeps its full traceback, because that would be a real bug.
Nothing to do on upgrade. Your existing DISCORD_* variables keep working exactly as before — they seed the fields, and the page marks any value you have overruled from the environment.