Four copy fixes on the SPA configuration page (web/index.html +
web/config.js). All string-only except one 1-line status override
in the runtime save handler.
1. Callout: "Business configuration is not read from the server" ->
"Business configuration is encrypted in Redis". The old text
contradicted GET /api/business-config, which reads non-secret
fields back. New copy states secrets are not echoed but non-secret
fields are read back; the first save must include all required
secrets.
2. Save feedback: after saving runtime parameters the status line
reused runtime.loaded ("read from Redis"), which is wrong for a
save. Added runtime.savedState ("Saved and applied successfully.")
and set it in the save handler. Dropped the stale "business
config still needs manual filling" sentence from runtime.readToast,
which also contradicted readback.
3. LLM toggle: "Allow network calls" -> "Enable LLM outbound
requests"; the hint now states no LLM client is initialized and
the bot calls no external model service when off.
4. All fixes applied to both zh and en dictionaries and to the HTML
English fallback defaults for consistency.
GATE-DOCS 6/6 PASS. No src/, cloudflare-worker/, or docs/ changes.
The value rendered was nogit+<UTC build time>, which carried no
information for the operator, so remove all three display sites
(topbar next to the secure-channel note, setup card, page footer).
Clears the three querySelectors, renderVersion, and the two dead i18n
keys. /api/status is untouched and still returns version - the API is
now the only way to confirm a deploy landed.
The <footer> element is removed with its span rather than left empty:
it had no other child, and an empty footer would still render a
visible separator band (border-top plus 41px padding).
The callback section's field was the only one in the SPA sitting
directly on the panel, with no padded ancestor. It spanned the full
card width with zero side padding and pressed the button bar, while
the copy above it was indented 25px. Wrap it in .field-grid and pad it
to match its card siblings on both desktop and the 720px breakpoint;
.field-wide's grid-column now has a grid ancestor.
Rewrite the callback copy to name the Cloudflare Worker front proxy
explicitly instead of the bare word Worker, and state that registering
a new address retires the previous subscription.
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.
PUT /api/business-config and POST /api/bootstrap now treat validation as the only
write gate: a valid wire is always persisted, and only then are the clients built
and the running worker swapped. A failed build returns 200 with persisted=true,
runtime_applied=false and a warnings array of {component, step, detail} instead of
503, so an unreachable JMAP/LLM no longer turns a valid configuration into a silent
data loss. The previous worker keeps serving.
POST /api/business-config/preflight runs the same validation and client build
against the submitted wire without writing anything and without touching the
running worker. It answers 200 even when the configuration is bad, because that
is the answer the caller asked for; components is null after a validation
failure, and llm is null rather than vacuously ok when LLM is disabled.
Authentication is the admin-session gate, not DEBUG_TOKEN.
The SPA drops the account_id field (the backend still accepts it; omitted falls
back to the session primary account), adds a Test connection button, and shows
the build fingerprint in the footer, the topbar and the setup card. The
503 message no longer claims JMAP/LLM build failure, which is not what produces
it.
BusinessConfigWire.account_id gains #[serde(default)]; without it, omitting the
field in the wire body made deserialization fail outright.
build.rs bakes BUILD_VERSION (git sha or nogit, plus a UTC build timestamp) which
GET /api/status exposes as version.
Every member's audit is now read and each real finding fixed, not just reported.
AGENTS.md §2.1
- The 500-line soft cap now has a machine-checkable exception: an over-limit
file must carry a one-line `SPLIT-EVAL:` marker stating the split decision.
- Six files were over the cap with no justification: notify.rs 2036, state.rs
1586, worker.rs 1045, config.js 1079, client.rs 601, config.rs 521. All now
carry the marker. client.rs was missed by my own earlier line count, which
did not descend into src/domain/jmap/.
- New validator scripts/docs_check/check_file_size.py; GATE-DOCS 4/4 -> 5/5.
It proves the marker exists, not that the written reason is sound.
- Re-based 144 line anchors across six docs for the +1 line each file gained;
every shifted anchor verified against the code, not just for non-blankness.
audit-arch.md
- design.md §6.1 module matrix omitted the `debug` module entirely; added.
- Seven numeric duplications in design.md now defer to reference.md §6.1 as
the single authority instead of restating the values.
- reference.md §5.1 was missing DEBUG_TOKEN; §5.3 listed six recognised names
and guessed `JMAP_*`. It now lists all 20 exactly as config.rs reads them.
- Corrected two wrong claims in the audit itself: debug.rs is compiled
unconditionally, not under #[cfg(debug_assertions)] (there are no cargo
features at all), and /debug/* exposes 7 routes, not the invented session
list / resend / rebuild surface.
audit-fileplanner.md
- reference.md cited a nonexistent `jmap_service.rs`; replaced with the real
adapter chain worker.rs -> domain/jmap.rs -> domain/jmap/client.rs.
- cloudflare-worker/README.md was outside every validator's scope; all four
doc validators now include it (208 section refs, 250 line refs).
- README.zh-CN.md stated only half of what README.md says about RUN_MODE; the
forward-placeholder sentence is now bilingual.
- charter.md §7 boundary table was missing rows for itself and for web/.
audit-rust.md / audit-frontend.md
- Error envelope corrected: `error` is a bare machine code, no `message`.
- Status table, AI-consent labels and ~15 stale or phantom facts fixed against
notify.rs; `TelegramClient::SendMessage` and `web_config` phantoms removed.
- GET|PUT /api/enabled forwarded by the worker so the SPA kill switch works.
- Ten UI strings that called the admin credential the Redis ACL password now
say CONFIG_ENCRYPTION_KEY.
- POST /healthz returns 405; dead HTTP_STATUS removed.
- audit_anchors.py now flags identifiers that do not exist in the file they
are anchored to.
charter.md
- `SAF-ENABLE-FLAG` registered (85 IDs); `SAF-ADMIN-SESSION` row corrected;
- `REQ-PUSH-TYPES` registered; the stale `admin_session_digest` removed.
Gates: GATE-DOCS PASS 5/5 (250 line-refs, 208 section refs, 0 errors);
GATE-P0 62 passed / 0 failed / 1 ignored, baseline unchanged; worker and SPA
36 tests, 0 failures.
Real Stalwart / Telegram / managed Redis verification stays blocked on missing
credentials and remains labelled as unverified.
- 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