Repository navigation
fix(settings): stop a failed settings write from saying it was saved - #736
JDProfresh wants to merge 1 commit into
Conversation
App Settings' save handled only the multi-user admin-only 403 and the tunnel's no-password 403, so a 409 for a settings.json that does not parse, a 400, a 500 or a dropped connection (`_apiPut` answers null) still toasted "Settings saved" and closed the modal, and the run mode and respawn preset writes failed with no word at all. Those failures now warn with the server's reason (else the HTTP status, else that the server could not be reached), the modal stays open, and nothing that assumes the server stored the save runs: Apply's refresh, the tunnel-starting toast and the gesture-control reload. Since the modal now invites a second Save, the plan-usage collection flip and the gesture reload are judged against the settings the failed save started from until a save lands, so the retry still sends the flip and still reloads. A save with the tunnel switch on is read as a tunnel refusal only on a 403, so another failure no longer turns the switch off or shows the no-password message, and an accepted no-password retry now resends the whole save instead of the tunnel keys alone, which had left every other setting unsaved behind "Settings saved". Run mode and respawn presets stay saved on this device as before and now say they were not saved on the server. Stopping the tunnel from the welcome screen or the tunnel panel no longer says "Tunnel stopped" when the server refused the write, since the tunnel is then still running.
|
Thank you @JDProfresh, this is a careful follow-up to #719, and the write-up made it quick to review. The PR makes every browser write to One small thing I will apply at merge:
Two notes that need nothing from you:
I will merge with the two-line change above on top. Thank you again for following this one through every write path. |
|
Thanks for the careful read, and for reproducing the abandoned-tunnel baseline in the harness. Setting the baseline to |
App Settings' save only recognises the two 403s it already handles, so any other failed
PUT /api/settings(including the new409 CONFLICTfrom #719) still ends with "Settings saved" and a closed modal.Follow-up to #719, the App Settings part you said could come as its own PR.
Problem
With a
settings.jsonthat does not parse, the server now refuses the write with409 CONFLICTand a message that says how to fix it. The browser never shows that message:Cause
_apiPutanswers a dropped connection withnulland an HTTP error with a non-ok response; it never throws (api-client.js).saveAppSettings()checks the response only through_noteSettingsWriteRefusal(403FORBIDDEN) and_handleTunnelEnableRefusal, then reachesshowToast('Settings saved')(settings-ui.js:3419on master) andcloseAppSettings()(:3439)._handleTunnelEnableRefusalis called for any failure while the tunnel switch is on (:3393), so a 409 with the switch on shows the bare 409 text as a tunnel error, turns the switch off on this device and closes the modal. A 500 with no JSON body shows "Tunnel refused: set CODEMAN_PASSWORD...".session-ui.js:742) and respawn preset (respawn-ui.js:609) writes only pass the response to_noteSettingsWriteRefusal.settings-ui.js:2309and:2759).What it does
Saved on this device, not on the server: <reason>, where the reason is the server'serror, elseHTTP <status>, elsethe server could not be reached. The modal stays open. Nothing that assumes the server stored the save runs: Apply's refresh, the "Tunnel starting" toast, and the gesture-control reload. The toast stays until dismissed (duration: 0), since the 409 message is long and tells you what to do.tunnelEnabled. Before, every other setting stayed unsaved while the toast said "Settings saved".Run mode saved on this device, not on the server: <reason>/Respawn presets saved on this device, not on the server: <reason>. They stay saved on this device, as before.Tunnel not stopped: <reason>, and the welcome button stays on.Two small helpers in
settings-ui.jsnext to_noteSettingsWriteRefusal:_settingsWriteError(res)returns the reason or'', and_noteSettingsWriteResult(res, what)is what the run mode and preset writes now call.Two cases I left out:
settings-ui.js:5098on master) andsaveLastUsedCase(session-ui.js:644). Neither tells the user something was saved: the migration runs again on the next load, and quick-start already writeslastUsedCaseon the server.saveModelConfigFromSettingsstill ignores its own response. It writes the same file through the same queue right after the settings PUT, so when the file is broken the settings PUT fails first and that warning shows. Making the model config fail on its own would need a schema 400 the form can't produce, or the file getting torn between two back-to-back writes.Tests
test/settings-save-failure-ui.test.tsruns the realsettings-ui.js,session-ui.js,respawn-ui.jsandapi-client.jsagainst the realindex.htmlform; onlyfetchis mocked. It covers 409, 400, 500 with no JSON body, and a dropped connection for the save. It also covers Apply, the tunnel switch on, the gesture reload, a failed Save followed by a successful one, the accepted no-password retry (Save and Apply), run mode and presets, both tunnel stop paths, and the zh-CN strings. Negative cases: a successful save, the 403FORBIDDENsave, the no-password 403 cancelled, a successful background write, aFORBIDDENbackground write, and a successful tunnel stop.On master, 18 of its 24 tests fail; the 6 negative cases pass. I removed each guard in turn, and every removal turns at least one test red.
test/settings-admin-only-ui.test.tsnow runs_settingsWriteErrorfor real instead of stubbing it, which catches a known non-admin's save (which sends nothing) being read as a dropped connection.test/plan-usage-collection-flip.test.tspins the flip's argument by source text, so its pattern now names_prevServer.Gate: typecheck, lint, format:check, check:frontend-syntax, check:browser-excludes and
npm testpass, excepttest/tui/tui-switch-guard.test.ts, which also fails on master on this machine (tmux 3.2a).Verified
On a throwaway instance with a torn
settings.json, in Chromium:On this branch
settings.jsonwas left untouched by every failed write.