Skip to content

fix(providers): clear the base-URL field when the gateway stops returning one - #52

Merged
sulthannauval merged 1 commit into
mainfrom
fix/provider-api-url-field
Aug 10, 2026
Merged

fix(providers): clear the base-URL field when the gateway stops returning one#52
sulthannauval merged 1 commit into
mainfrom
fix/provider-api-url-field

Conversation

@sulthannauval

Copy link
Copy Markdown
Member

Summary

Two problems in the Providers form, both of which kept an API key on screen.

1. The base-URL field never cleared. The effect mirroring GET /secrets into the field was guarded on truthiness:

if (secrets.data?.api_url) setUrl(secrets.data.api_url);

so it only ever filled the field and never emptied it. A value the gateway stopped returning stayed rendered until a page reload — and that is exactly what happens when the gateway starts withholding an api_url that holds an API key (RantAIClaw#444): the server returns null, the effect re-runs because the dependency changed, the guard skips the write, and the key stays visible in the console.

2. Both inputs were placeholder-only. They sit next to each other, one takes a URL and one takes a credential, and a placeholder disappears the moment either is focused — so the field that has just been cleared is also the one whose purpose is least visible while you type into it. Pasting a key into the base-URL field is what put a credential into config.toml in plaintext to begin with.

What changed

  • setUrl(secrets.data?.api_url ?? "") — mirror the server's value even when it is absent.
  • Both inputs get a real <label htmlFor> + id, using the pattern already in channels-panel.tsx. The base URL is named "API base URL — optional, not your API key"; the "leave blank to keep current" hint moves into the key field's placeholder where it no longer competes with a label. The base-URL placeholder becomes an example URL, which is what a placeholder is for.

What did not change

No API calls, no state shape, no save logic, no other panel. SecretsInfo.api_url was already typed string | null — this only stops the client ignoring the null.

Related

Validation

npx tsc --noEmit    # 0 errors
npx next build      # compiled successfully
npx vitest run      # 20 files, 156 tests passed

Component tests were not added: vitest.config.mts runs in the node environment and includes only src/**/*.test.ts, so there is no DOM/testing-library setup to hang a component test on. Adding one would mean pulling in jsdom and a rendering library for a one-line state fix. Verified by driving the real console instead.

Browser verification, against a live gateway on an isolated config dir:

step fixed build control (guard reverted)
server returns api_url: "https://probe.example.com/v1", page loaded field shows the URL field shows the URL
server switched to api_url: null, panel Refresh clicked, no page reload field is empty field still shows the old URL

Same sequence, same gateway, same browser. The control is the point: reverting only the changed line reproduces the stale value, so this is not a test that would pass either way.

Build provenance was checked rather than assumed — the served bundle was grepped for the new label before any conclusion was drawn from it. Worth noting for anyone reproducing: next start does not work with this project's output: standalone config and will silently serve a stale build; use node .next/standalone/server.js with .next/static and public copied in.

Risk

Low. One state assignment and markup around two existing inputs.

  • The effect now writes on every secrets.data change rather than only on truthy ones, so a refresh overwrites whatever is typed in the base-URL field. That was already true for non-empty server values; this extends it to the empty case, which is the intended behaviour — refresh means "show me what the server has".
  • useAsync never clears data during a refresh (it only assigns on success), so an in-flight or failed refresh cannot blank the field.

Rollback

git revert. Nothing depends on this and no data shape changed.

…ning one

Two problems in the same form, both of which kept an API key on screen.

The effect that mirrors `GET /secrets` into the base-URL field was guarded
on truthiness:

    if (secrets.data?.api_url) setUrl(secrets.data.api_url);

so it only ever filled the field and never emptied it. A value the
gateway stopped returning stayed rendered until a page reload. That is
exactly what happens the moment the gateway starts withholding an
`api_url` that holds an API key: the server returns null, the effect
re-runs because the dependency changed, the guard skips the write, and
the key remains visible in the console.

Verified against a live gateway, with the panel's own Refresh button and
no page reload. Before: the field still showed the value the server had
already stopped returning. After: it clears. Same sequence, same server,
same browser.

Both inputs were also placeholder-only. They sit next to each other, one
takes a URL and one takes a credential, and a placeholder disappears the
moment either is focused — so the field that has just been cleared is
also the one whose purpose is least visible while typing into it. Pasting
a key into the base-URL field is what put a credential into config.toml
in plaintext in the first place. Give both a real `<label htmlFor>` using
the pattern already used in the channels panel, name the base URL as
"not your API key", and move the "leave blank to keep current" hint into
the key field's placeholder where it is not competing with a label.
@sulthannauval
sulthannauval merged commit be56a1c into main Aug 10, 2026
1 check passed
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