Skip to content

a failed settings read arms the board's persist writes, and they overwrite with defaults #313

Description

@kensac

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

  1. A failed settings read must not arm the layout or prefs persist paths.
  2. The board must still load when settings are unreachable — the datasource attach stays on both paths.
  3. persistFilters must not write prefs from a ref that was never populated by a successful read.
  4. Separate the two responsibilities currently carried by layoutReadyRef.

Passing criteria

  • With GET /user/settings failing, moving or resizing a column does not write column_layout.
  • With it failing, changing a board filter does not write prefs, or writes it without dropping unrelated keys.
  • With it failing, the board still loads and shows rows.
  • With it succeeding, layout and filter persistence behave exactly as they do today.
  • A prefs key written by another surface survives a board filter change.

Not verified

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions