- CachedStaticFiles matched the substring `v=`, so `?rev=1` looked
fingerprinted and earned the 180-day immutable lifetime. It parses the
query now and looks for a parameter actually named `v`.
- saveRenameProtocol() restored the button label from a server-rendered
`'{{ _("save") }}'` string literal; a locale whose translation carries a
quote would break the whole inline script. Uses the runtime helper and
textContent instead.
- The add-user modal kept the "password optional" hint up after the role
changed to one that requires a password. It is hidden with the label now.
- app.py handed every /static response `max-age=15552000, immutable`, but
only the assets built through static_version() carry ?v=<mtime>. The
favicon, the icons, qrcode.min.js, searchable-select.js, the vendored
CodeMirror bundles and ReDoc keep one URL forever, so a redeploy could not
reach a browser that had already cached them. Immutable is now limited to
fingerprinted URLs; the rest get an hour plus a revalidation.
- WireGuardManager._apply_bw_limits ran `docker cp ... && docker exec ...`
through run_sudo_command, which only privileges the head of the chain, so
the exec ran as the unprivileged SSH user. Wrapped in one `sh -c`, the
pattern tests/test_sudo_command_chains.py enforces.
- toggleConnection() restored the row only in the error path. When
loadConnections() fails after a successful toggle, the row it was going to
rebuild stays disabled with a spinning bulb. Restore now runs in `finally`.
- The add-user modal starts on role "none", where the password is optional,
but the label said "required" until the select fired its change event.
Tests: the bandwidth chain joins the sudo-chain suite, and a new
tests/test_static_cache.py pins both cache lifetimes.