GET /api/business-config returns the stored values with presence-only secrets, and PUT now merges a patch against the stored config instead of replacing it, so the SPA can show what is already set and send back only the fields that changed.
The SPA marks secrets as set-but-unreadable, edits chat_allowlist as an unordered set, and omits fields it did not touch; blank required fields keep the stored value, blank optional fields clear it. The read-back is Serialize-only, so a client cannot inject secrets_present.
Forward both paths at the gateway: /api/business-config accepted PUT only, so the read-back returned 405, and the preflight path was missing from the safelist entirely, so it returned 404. The SPA is served from the Worker domain, so the whole edit experience was unreachable behind the production gateway. Route methods are now asserted per path, which the safelist test never checked.
Also: /debug/config reports timezone again, the timezone select is styled like the other fields, and the docs follow the new contract.
Section 04 "Notification display" holds a single select populated from a
frozen catalogue of the 16 zones the server accepts, with Asia/Shanghai
selected by default. Labels are per-zone (`label.en` / `label.zh`) so they
follow the existing browser-language switch.
readBusinessConfig now submits the selection under the `timezone` key.
- detect locale from navigator.language (zh* -> zh, otherwise en) at startup;
<html lang> switches accordingly; English is the default
- index.html carries English default text plus data-i18n /
data-i18n-placeholder / data-i18n-aria attributes
- config.js holds inline en/zh dictionaries, t(key, vars) and detectLocale();
dynamic Chinese strings become t() calls with {var} interpolation
- no new asset files (src/web.rs serves only config.js and styles.css);
stays a build-less vanilla-JS single page
- config.test.mjs updated to the English defaults; adds dictionary key-parity
and markup key-coverage tests