Objective
On the board, a failed GET /user/settings must not lead to the saved column layout and the prefs jsonb being overwritten. Today it can.
Reason
app/job-tracker/components/jobs-grid.tsx (personal-portfolio), onGridReady:
getSettings()
.then((settings) => { /* applies column_layout, sets prefsRef.current */ })
.catch(() => {})
.finally(() => {
layoutReadyRef.current = true
e.api.setGridOption('datasource', buildDatasource())
})
.finally runs on rejection too, so the ready flag flips whether or not the read succeeded. After a failed read the grid is showing DEFAULT column state and prefsRef.current is still {}, but every persist path is now armed:
- Any
onColumnMoved / onColumnResized / onColumnVisible / onColumnPinned / onSortChanged calls persistLayout(), which writes api.getColumnState() — the defaults on screen — over the saved arrangement.
- Any filter change calls
persistFilters(), which writes {...prefsRef.current, tracker_filters}. With prefsRef.current still {} that is a whole-object replace that drops every other prefs key.
The server does not soften either one. PUT /user/settings (src/api/routers/users.py) does prefs = COALESCE(%(prefs)s, user_settings.prefs) and column_layout = COALESCE(EXCLUDED.column_layout, user_settings.column_layout). A provided value replaces; there is no merge. This is the same full-replace shape as the criteria defect fixed in personal-portfolio#343, on a different field.
This is one flag doing two jobs. The datasource attach genuinely must happen on both paths, or the board never loads when settings are unreachable. Arming the persist writes must not.
Measured, 2026-09-04 04:12 UTC
Production, the only user:
column_layout: INTACT and hand-arranged, not defaults. 18 columns; company pinned left, row_actions pinned right; 8 hidden (size, source, terms, recruiter, connection1, connection2, documents, notes); title width 221.328125, a fractional value that only a manual drag-resize produces.
prefs: {"tracker_filters": {...}}. One key.
updated_at was 35 seconds old when read, so this path executes in normal use.
Whether prefs ever held more keys cannot be determined. user_settings has no history table (the only one in the database is user_job_history, which covers jobs). The absence of onboarding:dismissed and onboarding:sponsorship-answered is equally consistent with "never written" and "written, then replaced". Recorded as cannot-tell rather than counted either way.
So: the mechanism is confirmed by reading both halves, and there is no evidence it has fired. Not an incident.
Requirements
- A failed settings read must not arm the layout or prefs persist paths.
- The board must still load when settings are unreachable — the datasource attach stays on both paths.
persistFilters must not write prefs from a ref that was never populated by a successful read.
- Separate the two responsibilities currently carried by
layoutReadyRef.
Passing criteria
Not verified
Objective
On the board, a failed
GET /user/settingsmust not lead to the saved column layout and the prefs jsonb being overwritten. Today it can.Reason
app/job-tracker/components/jobs-grid.tsx(personal-portfolio),onGridReady:.finallyruns on rejection too, so the ready flag flips whether or not the read succeeded. After a failed read the grid is showing DEFAULT column state andprefsRef.currentis still{}, but every persist path is now armed:onColumnMoved/onColumnResized/onColumnVisible/onColumnPinned/onSortChangedcallspersistLayout(), which writesapi.getColumnState()— the defaults on screen — over the saved arrangement.persistFilters(), which writes{...prefsRef.current, tracker_filters}. WithprefsRef.currentstill{}that is a whole-object replace that drops every other prefs key.The server does not soften either one.
PUT /user/settings(src/api/routers/users.py) doesprefs = COALESCE(%(prefs)s, user_settings.prefs)andcolumn_layout = COALESCE(EXCLUDED.column_layout, user_settings.column_layout). A provided value replaces; there is no merge. This is the same full-replace shape as the criteria defect fixed in personal-portfolio#343, on a different field.This is one flag doing two jobs. The datasource attach genuinely must happen on both paths, or the board never loads when settings are unreachable. Arming the persist writes must not.
Measured, 2026-09-04 04:12 UTC
Production, the only user:
column_layout: INTACT and hand-arranged, not defaults. 18 columns;companypinned left,row_actionspinned right; 8 hidden (size, source, terms, recruiter, connection1, connection2, documents, notes);titlewidth221.328125, a fractional value that only a manual drag-resize produces.prefs:{"tracker_filters": {...}}. One key.updated_atwas 35 seconds old when read, so this path executes in normal use.Whether prefs ever held more keys cannot be determined.
user_settingshas no history table (the only one in the database isuser_job_history, which covers jobs). The absence ofonboarding:dismissedandonboarding:sponsorship-answeredis equally consistent with "never written" and "written, then replaced". Recorded as cannot-tell rather than counted either way.So: the mechanism is confirmed by reading both halves, and there is no evidence it has fired. Not an incident.
Requirements
persistFiltersmust not write prefs from a ref that was never populated by a successful read.layoutReadyRef.Passing criteria
GET /user/settingsfailing, moving or resizing a column does not writecolumn_layout.prefs, or writes it without dropping unrelated keys.Not verified
prefsand would race this one.jobs-grid.tsx. It was misclassified as "correct as-is" in ci: commit mechanical fixes and re-run instead of failing #343's own sweep table, on the reasoning that a failure only means an arrangement does not persist. That reasoning missed the ready flag.