mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +02:00
fix(statusline): GET /api/settings never writes, and a save sends the collection switch only on a flip
Two follow-ups to #361's sticky telemetry switch. GET /api/settings reconciled an absent showPlanUsageLimits by persisting true, but readJsonConfig() answers {} for ANY read failure (a parse error, EACCES, EMFILE, a read landing inside PUT's non-atomic write), not only ENOENT, and every page load calls this route, so one unlucky read replaced the whole settings file with a one-key file. The route is a plain read again and the default moved into the reader: readPlanUsageTelemetryEnabled() treats an absent key as ON, the same way readWorkspaceHooksEnabled() does, which is what the desktop chip already shows for an install that never touched the setting. saveAppSettings() sent showPlanUsageLimits on every save. The chip defaults OFF on handhelds, so a phone saving its font size persisted false and switched collection off for every desktop, whose chip then went stale with no error anywhere. The key is now stripped like the other per-device display keys and re-added only when the save FLIPS the chip relative to what the device had (planUsageCollectionFlip), so an explicit toggle on any device still writes it in either direction. Tests pin both: the GET route with a mocked filesystem (absent, missing, EACCES, garbage, explicit), the reader default, and the flip helper plus its wiring in saveAppSettings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -936,47 +936,18 @@ export function registerSystemRoutes(
|
||||
// ========== Settings ==========
|
||||
|
||||
app.get('/api/settings', async () => {
|
||||
const settings = await readJsonConfig<Record<string, unknown>>(SETTINGS_PATH, 'settings', {});
|
||||
|
||||
// Plan-usage chip default reconciliation (PR #361 follow-up): the client's
|
||||
// own default resolution (planUsageChipEnabled() in settings-ui.js) shows
|
||||
// the header chip and the App Settings checkbox as already ON whenever this
|
||||
// key has never been set — a discoverability default from 1.9.3, unrelated
|
||||
// to consent. Meanwhile readPlanUsageTelemetryEnabled() (hooks-config.ts)
|
||||
// deliberately treats an absent key as "no telemetry" (privacy: never POST
|
||||
// usage data without an explicit persisted yes, pinned by its own unit
|
||||
// tests). Nothing ever reconciled those two independent guesses, so a
|
||||
// fresh install showed a checked box that silently did nothing until the
|
||||
// user opened Settings and hit Save at least once — verified live: an
|
||||
// install that had never touched this setting had NO showPlanUsageLimits
|
||||
// key in settings.json, and its running Claude process's argv carried no
|
||||
// --settings flag at all, i.e. zero telemetry ever collected.
|
||||
//
|
||||
// Resolve it ONCE, here, the first time anything reads settings: if the
|
||||
// key is truly ABSENT (never explicit true or false), persist the same
|
||||
// desktop-default-ON resolution the client already shows, so "chip visible"
|
||||
// and "telemetry collected" become the same fact instead of two defaults
|
||||
// that happen to disagree. readPlanUsageTelemetryEnabled()'s own
|
||||
// absent-means-false contract is untouched — after this runs once the key
|
||||
// is never absent again, so that branch stays correct in isolation (its
|
||||
// unit tests keep passing unmodified) while being unreachable in practice
|
||||
// for any install that has ever called this route. An explicit false the
|
||||
// user sets afterward is respected forever; this only fires on true absence.
|
||||
if (!('showPlanUsageLimits' in settings)) {
|
||||
settings.showPlanUsageLimits = true;
|
||||
try {
|
||||
const dir = dirname(SETTINGS_PATH);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
await fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
||||
} catch {
|
||||
// Best-effort: the resolved default still reaches this response even
|
||||
// if the write fails, so the caller sees consistent data either way.
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
// A plain read. This route must NEVER write settings.json: readJsonConfig()
|
||||
// answers `{}` for ANY read failure (a parse error, EACCES, EMFILE, a read
|
||||
// that lands inside PUT's non-atomic write), not only for a missing file,
|
||||
// and every page load calls this route, so a "persist the default when the
|
||||
// key is absent" reconcile here replaced a whole settings file with one key
|
||||
// on the first unlucky read. The plan-usage default is resolved by the
|
||||
// READERS instead: an absent `showPlanUsageLimits` means ON to
|
||||
// readPlanUsageTelemetryEnabled() (hooks-config.ts), the same way an absent
|
||||
// `workspaceHooksEnabled` means ON, and the client resolves its own display
|
||||
// default through planUsageChipEnabled(). Pinned by
|
||||
// test/routes/system-routes-settings-get-plan-usage-default.test.ts.
|
||||
return readJsonConfig(SETTINGS_PATH, 'settings', {});
|
||||
});
|
||||
|
||||
app.put('/api/settings', async (req) => {
|
||||
|
||||
Reference in New Issue
Block a user