🚑 fix: Stop Configuration Page Data Loss (Wave 0) - #151
Merged
Merged
Conversation
Implements HF-1 to HF-11 of the configuration-page plan, plus HF-12 as
limited by the product decisions (no data-provider bump; unknown keys per D8).
- HF-1 Enter key: every click-ui Button/IconButton now sets an explicit
htmlType (new ESLint rule click-ui/require-button-html-type); the tab form
gets a disabled hidden default submit button and blocks implicit submission
from inputs. Enter can no longer delete an endpoint/social login or add "".
- HF-2 Delete buttons: TrashButton/EditButton are native buttons named
"Delete <item>"/"Edit <item>"; an entry card ignores keydown from nested
controls, so Enter on a card's trash button deletes instead of toggling.
- HF-3 Blank values: buildSavePayload normalizes every value (blank list items
dropped, emptied lists/records become "no value"), drops edits equal to the
saved value, and turns an emptied override into a reset. Blank allowlist
rows, empty header lists and stray [] / {} are never written.
- HF-4 beforeunload: only prompts while there are unsaved edits.
- HF-5 YAML import: validates a copy (version optional) but imports the raw
YAML (no Zod defaults, unknown keys kept and listed), merges through the
normal validated save path into the target picked in the dialog (base or a
profile; the open profile is preselected). The full-replace PUT is gone.
- HF-6 Dotted / $ keys: inline errors in key/value rows, record add-key and
rename inputs; the review blocks the save and names each key; import and the
server pre-flight reject them (BLOCK_UNSAFE_CONFIG_KEYS flag for BE-1).
- HF-7 Atomic saves: validateConfigChangesFn validates every save (indexed
array entries against the element schema, process-backed MCP fields, unsafe
keys, capabilities) before any reset is written; queries always refetch.
- HF-8 Base-only sections (filters) and YAML-only sections (cloudfront,
rateLimits, turnstile, verified in LibreChat) render read-only with a banner,
are stripped from saves, and a backend "No actionable" reply is reported as
a warning instead of "Changes saved".
- HF-9 Azure groups and Anthropic Vertex are read-only with a banner, group
API keys are masked (also in the review dialog), and a stored override gets
a "Remove stale override" action.
- HF-10 The save error is cleared on open, cancel, discard and every edit.
- HF-11 Scope mode reads collections from the merged tree when entries have
pending edits, so sequential edits and "Add item" rows are kept; review
BEFORE values come from the profile being edited, and the title names it.
- HF-12 (D8) Settings unknown to the bundled schema are listed read-only per
section and at top level on the System tab; unmapped sections get a
readable title; test asserts every schema section has SECTION_META.
Saves and imports send at most 100 entries per PATCH (the admin API limit).
Tests: normalizeForSave, findUnsafeKeys, raw YAML import (3 keys -> 3 leaves),
pre-flight checks, import target routing, HF-11 sequential scope edits, secret
masking; Playwright spec for Enter on every tab and delete-button names (axe).
- Sanitize the aria-describedby ids of key/value and rename errors (entry names can contain spaces) and give each add-key input a unique error id. - Document that the tab form's Enter guard also covers portaled create dialogs, so Enter commits the field being typed instead of submitting a half-filled dialog.
…, Reverts)
Write rules now apply to every write path:
- One shared rule, getWriteSkipReason (src/utils/writes.ts), now drives the
review, the server pre-flight and every write path: import, base saves,
bulk profile saves and saveFieldProfileValueFn. Imports can no longer
store Azure groups (plaintext keys) or Anthropic Vertex overrides. A save
at an ancestor path that carries one of these fields is rejected. Removing
a stale override is still allowed.
- Langfuse (BASE_PRINCIPAL_CONFIG_SECTIONS) is skipped on generic writes as
`dedicated`. mcpAppSandbox is an interim base-only section until the
data-provider bump.
- A PATCH reply whose stored overrides lack a section that was sent is
reported as `notStored`, so partial strips are no longer called saved.
- When a later batch fails, the error says how many entries were already
saved.
Import:
- Import leaves are normalized like edits (normalizeForSave), so
`allowedDomains: [""]` and blank or null values are reported as `empty`
instead of being stored.
- AppService alias sections are merged leaf by leaf with their canonical
section, using APP_SERVICE_KEY_ALIASES, which is shared with
normalizeAppServiceKeys.
- A numeric `version` no longer fails validation.
Review and messages:
- Emptying a value that only librechat.yaml or the defaults set is now
reported as `yamlValue` ("can't be cleared here") instead of "nothing
to save".
- Resets show the stored override as BEFORE and the value the field falls
back to as REVERTS TO. This uses librechat.yaml for base (the new
yamlConfig from getBaseConfigFn) and the base value for a profile.
- Warning toasts have a short title and a wrapped list of paths and
reasons. When nothing was written they say "Nothing was stored", and the
import success banner is not shown.
Keyboard:
- The tab form's Enter guard only applies to inputs in its own DOM, so the
portaled Create MCP server and Create endpoint dialogs submit on Enter
again. Covered by a new e2e test.
Cleanup:
- Removed the dead normalizeImportConfig.
- ConfigRecord and PatchFieldsResponse moved to src/types.
- Write helpers are typed with t.SaveEntry instead of unknown.
- ImportTarget is renamed to WriteTarget.
- Server utils are imported through the @/utils barrel.
- Fixed the prettier issues this branch introduced.
normalizeForSave trimmed every string and list item, so saving any sibling
field silently rewrote whitespace-significant values such as stop sequences
("\n\nHuman:" -> "Human:", ["\n"] dropped). Only drop exactly-empty
strings and list items; keep everything else verbatim.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stops silent data loss and instance-wide misconfiguration on the Configuration page. These issues came from a hands-on audit: 10 isolated live stacks (a real LibreChat backend, MongoDB and a production-like
librechat.yaml), with every finding independently re-reproduced. This PR is "Wave 0" of a larger overhaul. It is deliberately surgical: the UX/visual rework follows in separate PRs.Data-loss and corruption fixes
Button/IconButtondefaulted totype="submit"inside the tab<form>. Pressing Enter in any field "clicked" the first Trash/Add button: it deleted the first custom endpoint or social login, or added""toactions.allowedDomains/modelSpecs.addedEndpoints, which blocks every domain or hides every endpoint. Fixes:htmlType;click-ui/require-button-html-typeenforces this.[]/{}used to become overrides. For example,[""]puts LibreChat into allowlist mode, and an empty MIME list rejects every upload.normalizeForSavedrops empty items. An emptied override becomes a reset, and edits equal to the saved value are dropped. Whitespace is preserved verbatim (stop sequences).balance.startBalanceturnedbalance.enabledoff for everyone.__base__.us.anthropic.*,gpt-4.1, MIME types) are blocked with inline errors and a save/import pre-flight. LibreChat'sexpress-mongo-sanitizesilently strips them, while the panel used to report "Changes saved".filters) and YAML-only sections (cloudfront,rateLimits,turnstile; verified in LibreChat source) are now read-only with a banner and stripped from saves. Backend partial strips and "No actionable" replies are reported as warnings ("Nothing was stored…"), never as success.Smaller fixes
Change Type
Testing
normalizeForSave(including whitespace-significant strings)findUnsafeKeyse2e/config.spec.ts: Enter on every tab marks nothing dirty; delete-button names (axe); create dialogs still submit on Enter. These tests need a live LibreChat backend (not the mock).librechat-data-provider0.8.527, which is not published yet.npx tsc --noEmit,npx vitest run(1174 passed),npx eslint src/Known follow-ups (next PR, low severity)
e2e/config.spec.tsshould be gated behind a live-backend flag.interfaceConfig.*) are listed as unknown in the import report.LibreChat backend changes recommended (separate repo)
mongoSanitize({ allowDots: true })for/api/admin/config*, so dotted keys can be stored.azureConfigSetup/vertexConfigSetupon merged overrides, so Azure groups become editable again.endpoints.customentries.Test Configuration:
dev@ a4f6763 (data-provider 0.8.527) with MongoDB 8.2Checklist