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.
New reference.md 5.4 pins the user-visible contract: notifications are three
metadata lines only (From / Subject / Received), never the body and never an
LLM summary; the mailbox display form and its fallback literals; the
timezone field, its 16-zone whitelist, the 422 on anything else, and why DST
is not tracked. Registered as REQ-TIMEZONE-DISPLAY in charter 8.
deployment 5 gains the TIMEZONE row, including the SPA section and the
reason DST zones cannot be added.
Opengaps 1 drops two blockers that are verified done and moves the remaining
two to a "what you do / what I do" checklist.
Corrected doc claims that the line-number checker cannot reach:
- reference 6.2 and the Redis key table cited notify.rs 240/253 and
252/253 for lock TTL and heartbeat; the lock is acquired at :289 and
renewed at :302.
- The Redis key table's dedup:tg / dedup:jmap / ratelimit / stream call-site
anchors all pointed into the ready() handler and into unrelated config
helpers. Re-pointed at the real statement lines.
- Both Retry-After helpers were anchored at error_response 321-329 and
error_response_with_id 1088-1094; the real definitions are 370-378 and
1368-1375, and the readiness path passes retry at :210, not :162.
- design claimed summarize() is also called by notify.rs for new-mail
notifications. There is exactly one call site, the /summary branch in
worker.rs; notifications never call the LLM.
- design described list_emails as requesting an explicit property list and
returning Vec<EmailSummary>; the code passes no properties and returns
Vec<EmailMetadata>.
- retired listed 6 intents but only 5; worker.rs:533 for /search is :553.
- Config is 7 fields now that timezone is mirrored into it.
The anchor audit now flags any citation that lands on a bare brace, paren or
comma - the shape these drifted references had.
- opengaps: drop the Telegram inbound callback and Stalwart PushSubscription
blockers, both verified live; 2 items remain (newState semantics, TTL
measurement). The public-callback mapping question is also resolved -- the
integration env has no Worker in front and Stalwart hits the backend origin
directly, while /push/jmap is already in the Worker safelist for production
- deployment 4.1 (new): full setWebhook and /api/push/register procedure,
expected replies, failure symptoms, and the setWebHook rotation trap (the
deployed secret must be re-registered after any TG_WEBHOOK_SECRET change)
- deployment 6.2: the worker is neither a background task nor a separate
container; POST /worker is the only consumer entry point
- deployment 6.3.1: add the /worker drain step to the cron and k8s CronJob
examples, plus a two-endpoint comparison table, ordering requirements, the
10-message batch cap, and the "204 is not a success signal" warning
- deployment 10.4: describe /worker as scheduler-driven, not manually invoked
- reference, retired: /worker anchor notify.rs:331 -> 380; the old value fell
inside the reconcile function body, so the anchor audit passed while being
semantically wrong
- charter 6.6: narrow the blocker list; code baseline b2dbe7c -> fc686ab
- design 11.2: PushSubscription permission now verified; the external Cron
must call both /reconcile and /worker
GATE-P0: cargo fmt / check / clippy clean, 69 passed, 1 ignored
GATE-DOCS: 6/6 clean (ANCHOR 299, SECTION-REF 234, PATH 428)
The PM accepts the residual on the strength of the two gates that are
already the documented production posture, so the item leaves the gap
file and docs/opengaps.md no longer has a 代码缺口 section at all.
Both gates verified in code before agreeing to the premise:
- closing debug is total: the dual-factor gate in src/main.rs:98-105
feeds src/notify.rs:1340-1342, which only then merges debug_router();
unset and the 7 routes do not exist, axum falls through to a generic
404, so it is not a 401 and does not disclose the path exists
- the Worker refuses rather than forwards: cloudflare-worker/src/index.js
:81-82 returns 404 route not forwarded for any path outside LB_ROUTES,
and LB_ROUTES = SAFE_ROUTES holds exactly 15 entries with no /debug
path. Confirmed by reading the list, not by trusting the README
So either gate alone is sufficient, which is stronger than the assumption
recorded in the gap entry, which treated the allowlist as the thing
keeping the other two apart.
The residual is already on paper and is the only real one: direct access
to the backend origin bypasses both. docs/deployment.md:103 says exactly
this, so nothing new had to be written, and design.md:349 now points
there and to docs/design.md §7.6 instead of at a section that no longer
exists.
- drop the allowlist row from the 你要做的事 table as well, or the table
would advertise a task whose gap no longer exists
- docs/design.md:349 would have dangled on 「代码缺口」, so it now records
the closure and its basis rather than a count of a deleted section
- docs/retired.md:81 says 「代码缺口」4 → 3. That is the recorded effect
of the deletion it documents, so it stays as a past-tense fact rather
than being rewritten to the current state of 0
- the behavioral fact stays where it belongs: docs/deployment.md:102 still
documents that an empty allowlist does not block, and charter.md:279
still registers the ID with its 启用须确认白名单已配置 condition.
Only the gap status was removed
- GATE-DOCS 6/6 pass: ANCHOR 270 (was 272), SECTION-REF 226 (was 224,
the two new cross-refs I added), PATH 394 (was 393, net +1 swap)
Every blocker now states the concrete action required, the owner, and the
expected result, instead of only explaining why it is blocked. The file
was answering 'what is broken'; nobody could tell from it what to run.
- new 你要做的事 table up front: 6 rows splitting each item between
operator action and my action, so the reader sees at a glance which two
items need a curl, which one needs an answer, which needs a downtime
window, and which need nothing
- item 1 Telegram: setWebhook + getWebhookInfo commands, allowed_updates
narrowed to message to match the only field the service parses
(message.text), and the rotation trap spelled out -- changing the SPA
secret without re-running setWebhook leaves Telegram holding the old
value, so real callbacks start failing with 401 silently, no error
and no alert
- item 2 PushSubscription: POST /api/push/register command plus the
split of responsibility. Registration is performed by this service
against JMAP, not in the Stalwart admin UI, and the verification code
comes back over the callback and is verified automatically, so the
operator never needs to obtain a code. Also documents the observable,
the failure-code decode table, and the disable endpoint
- item 3 rewritten: the operator action collapses to a single question
(which component does the public mapping), because no Worker is in
front of this deployment. The five TTL keys moved into a table with
exact keys, purposes, TTLs and line anchors, and the pending 900s /
verified 300s statuses are documented where they are set
- item 4 spelled out as a concrete downtime window: stop the service
while mail is still arriving, longer than Stalwart's delta retention,
without running POST /reconcile during it, then I reconcile and check
the cursor re-baselines instead of erroring
- corrected getWebhookInfo field: it returns last_error_date, not
last_error_time
- anchor audit caught two of my own bad anchors on the first run -- the
SPA-verification comment is at notify.rs:1163 and the callback URL
validation at :1174, I had guessed :1151 and :1163, both off by 12.
ANCHOR is now 272 refs / 0 errors, PATH 393
- GATE-DOCS 6/6 pass
- remove the 真实 Redis TLS 连接 blocker: user confirms the managed
Redis is connected; the redis crate has tokio-rustls-comp enabled so
rediss:// is a supported scheme, and /debug/redis + /ready both report
reachable/true on the live deployment (checked just before this commit)
- the old item bundled three things: rediss:// handshake, password URL
encoding, ACL password. The URL-encoding half was only ever covered by
a parse-only unit test pointing at example.invalid, which never
connects, so the handshake itself was the unverified part; the live
connection covers all three
- blockers now 5 -> 4; no cross-reference lost: docs/deployment.md:231
already documents the rediss://default:<url-encoded-password>@ form,
and :220 already draws the startup-env vs business-config hot-reload
line, so deleting the item drops no surviving fact
- docs/charter.md 6.6: the blocker enumeration mirrored opengaps.md and
must move with it, and the credential preamble ("无 Stalwart /
Telegram / 托管 Redis 凭据") was already stale since the Stalwart and
Telegram creds both verified live, so it now states what is actually
unverified, which is the inbound callback direction
- 代码缺口 has nothing to remove: its only remaining item is
SAF-DEBUG-ALLOWLIST, which is deliberately unimplemented by product
decision and src/debug.rs:233-235 still shows the empty-allowlist
short-circuit
- GATE-DOCS 6/6 pass: ANCHOR 267 (was 269, the item carried 2 refs),
SECTION-REF 224, PATH 390 (was 392)