Skip to content

Make the conversation window and locale writable - #78

Merged
ananthb merged 1 commit into
mainfrom
operator-ui-and-settings
Nov 1, 2026
Merged

ananthb merged 1 commit into
mainfrom
operator-ui-and-settings

Conversation

@ananthb

@ananthb ananthb commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Two of the gaps #71 listed: the conversation window and the locale pair were
read by the worker and writable by nobody, so every tenant ran on the
prompt::DEFAULT_* constants and whatever Accept-Language said at signup.

GET/PUT /api/settings now read and write both, and the dashboard's
Settings tab is the UI.

The three-valued wire format

Each knob is Option<u32>, and "no override" is the state most accounts
should be in — so the body distinguishes three cases:

  • absent — leave what's stored
  • null — clear the override, back to the default
  • a number — set it

Without the null case a form could only ever set values, never return one to
its default. The form sends all three knobs every save, and an empty box
encodes as null, so clearing a box is how you go back to the default. The
inputs show the default as a placeholder rather than pre-filling it, so saving
an untouched form doesn't quietly pin today's default forever.

The invariant

idle_gap_mins > handoff_cooldown_mins, checked against the resolved
pair rather than the submitted one. A cooldown that outlives the idle gap
would keep answering in the holding voice for a conversation the pipeline has
already ended.

Resolving first is what makes it correct: lowering only the gap to 45 is
refused because the default cooldown is 60, even though the request never
mentioned the cooldown. My own first test hit exactly that and had to be
fixed, which is the case a submitted-values-only check would have let through.

Why the validation is a pure function

Everything interesting — the three-way read, the bounds, the invariant — sits
behind require_tenant, so an API test can't reach it without a session.
apply_conversation, parse_locale and parse_currency are pure and tested
directly, the same shape as risk.rs. 12 unit tests cover clearing, absent
fields, string numbers (the form posts text), both ends of each range, and the
invariant against an unmentioned default.

api.spec.ts picks up /api/settings in the signed-out 401 list and the
X-Concierge-Request list, which is what it can prove without a session.

Removed: PUT /api/billing/currency

It already existed, nothing in the frontend called it, and it wrote the same
Tenant.currency field — a second write path waiting to drift. It also passed
unknown codes through Currency::parse, which answers INR for anything it
doesn't recognise, so {"currency":"EUR"} returned 204 and silently meant
INR. The settings endpoint rejects instead, and parse_currency's test covers
that specific case.

Docs

doc/configuration.html claimed these had no endpoint and that "nothing
enforces this at present" for the invariant. Both are now false, so all three
passages are rewritten, including the removed billing endpoint.

Checks

cargo check, clippy -D warnings, cargo fmt --check, elm-format --validate and 117 unit tests all pass. Full Playwright suite: 259 passed, exit 0
(captured explicitly). doc/screenshots/dashboard-settings.png regenerated,
and the prerendered pages with it — rebuilding app.js changes its hash, so
the committed snapshots go stale otherwise and SRI blocks the bundle.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
concierge bcdbbd9 Nov 01 2026, 09:44 PM

Two of the gaps #71 listed. The worker read `ConversationConfig` and
`Tenant.locale` and nothing could write either, so every tenant ran on the
`prompt::DEFAULT_*` constants and whatever Accept-Language said at signup.

`GET`/`PUT /api/settings` covers both, with the dashboard's Settings tab as
the UI. Each knob is three-valued on the wire — absent leaves the stored
value, `null` clears the override, a number sets it — because a form that
can only set values can never return one to its default.

The `idle_gap > handoff_cooldown` invariant is checked against the resolved
pair, not the submitted one: lowering only the gap can collide with a default
cooldown the request never mentioned.

Validation is pure and tested directly (`apply_conversation`, `parse_locale`,
`parse_currency`), since behind `require_tenant` an API test can't reach it.

Also drops `PUT /api/billing/currency`: unused, and a second write path to
the same field that passed unknown codes through `Currency::parse`, which
answers INR for anything it doesn't know.
@ananthb
ananthb force-pushed the operator-ui-and-settings branch from 32a3ed1 to bcdbbd9 Compare November 1, 2026 21:41
@ananthb
ananthb merged commit 7db7966 into main Nov 1, 2026
8 checks passed
@ananthb
ananthb deleted the operator-ui-and-settings branch November 1, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant