Mostly the receive side this time. beta.18 got all eight outgoing channels saying the same thing; this one does the same for the commands coming back in — seventeen of them are one implementation with two renderings now instead of two implementations.
Before tagging I had five reviewers go over the whole branch. They found things I would not have, and the worst one was mine.
Fixed
The Web UI's Start / Stop / Restart buttons did nothing. They stopped working when the lifecycle code moved into the shared core — the Web UI still called a method that no longer existed, the error got swallowed, and the page reloaded as if it had worked. Press Stop, nothing happens, no error anywhere you would see it. On the way back in it gained the two guards it never had: it only ever checked "would this stop Docksentry itself?", so its Stop button could take down a container both chats refuse to touch, which for most people is the VPN carrying their remote access.
A recreate could lose its log driver. A container on json-file with a max-size option, on a host whose daemon default is journald, got the option without the driver — journald refuses max-size, so the recreate died. Options always carry their driver now.
/cleanup said ✅ twice — both chats and the Web UI checked the result for an English word the message stopped containing a while back, so the check never fired and a second checkmark got stuck in front. Most common outcome of the most-run maintenance command, in every language.
/updateall @nas updated every host. The host was never parsed at all. On a multi-host setup "just the NAS please" meant everything, everywhere, no confirmation.
/note web something confirmed with an empty note — saved fine, reported nothing.
/note, /trustrunning, /askmajor and /setlink were permanently local in Discord. All four took a host internally; none of them offered the option.
A crash-restart loop printed its stack trace five times. That is what a restart loop writes — same error on every attempt. Folded to one copy with a count, so the lines that differ are the ones you see.
Changed
/stop asks first, in Telegram too. Discord already did. The refusals run before the question, so you never get asked about a container that is stop-protected and then refused — /stop * says up front what it will skip and asks about the rest. /start and /restart do not ask. Telegram's confirmation expires after fifteen minutes now and only the person who asked can press it.
/stop web* works in Discord, and /cleanup reaches every host from there. Both only ever worked in Telegram, purely because that is where the code happened to live.
/cooldown with no container lists what is set, in both chats.
/checkimages says it is an estimate and names what the grace period is holding back, instead of promising a number /cleanup then does not deliver. Sizes are formatted the same way in both chats — Docker writes 8.534MB, which reads as 8534 MB wherever a dot is the thousands separator.
Under the hood
Seventeen commands now do nothing but parse their own syntax and render a shared result. Two new core modules and seven guard tests, each aimed at the blind spot that let its bug through — the Web UI checked against the real bot class, every core message checked against the sentence it fills in, Discord's write commands checked for a host option in both directions.
The update family (/update, /updateall, /check, /updates) is the last big block each chat still implements for itself. That is next.
Known and not fixed here: fourteen of the sixteen languages are about half untranslated English — that predates all of this and is too big for a beta window. Discord's /pin, /autoupdate, /protect, /trustrunning and /askmajor still cannot list what is set.