Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Decision records

Explanation

Server-side UI state

Decision record for the rule that a Cluster ERP user's layout is stored on the server per account, while device preferences stay in the browser or desktop settings.

Status: Accepted, stated in the project's written engineering rules. This record covers where Cluster ERP keeps interface state. The arrangement a user owns (home screen, dock, widgets) belongs to the user account on the server; preferences of one device (theme, language, time zone, accessibility, floating button position) stay on that device. The client and the routes are explained on user UI state.

Context

The client module states the problem that led to the rule: the home layout once lived only in the browser's local storage, so it "silently differed per browser, per device and even per host name", and a cleared cache looked like a reset (webapp/js/ui-state.js:1-7). The model docstring states the goal: the arrangement follows the person across devices and browsers and survives a restart, a cleared cache, or a different host name (src/cluster_erp/modules/auth/ui_state/models.py:13-21). The project's written engineering rules also require browser tests to use the ui-state guard.

Decision

  • Per-user layout values live in the table auth_user_ui_state, one live row per user and key (src/cluster_erp/modules/auth/ui_state/models.py:23-35).
  • The routes GET /api/v1/auth/ui-state and PUT /api/v1/auth/ui-state/{key} read and write the signed-in user's own values. The user id comes from the token, never from the request (src/cluster_erp/modules/auth/ui_state/routes.py:15-39, src/cluster_erp/modules/auth/module.py:58, src/cluster_erp/modules/auth/module.py:68).
  • Device preferences stay in localStorage: theme and time zone (webapp/js/prefs.js:5-6), language (webapp/js/i18n.js:9), accessibility (webapp/js/a11y.js:33), and the floating button position (webapp/js/home.js:2965). The desktop client keeps its window and pane layout in QSettings (src/cluster_erp/desktop/core/settings_store.py:10).

The sequence diagram shows a layout change in the web app.

sequenceDiagram
 participant UI as home screen
 participant St as ui-state.js
 participant API as auth ui-state routes
 participant Svc as UiStateService
 UI->>St: saveUiState(key, value)
 St->>St: queue, wait 600 ms
 St->>API: PUT /auth/ui-state/key with keepalive
 API->>Svc: put(user.id, key, value)
 Svc->>Svc: insert or update the live row

In prose: saveUiState updates the local cache, queues the value, and starts a 600 ms timer, so many saves during a drag become one write (webapp/js/ui-state.js:68-73). flush sends one PUT per queued key with keepalive, and the queue is also flushed when the page is hidden or closed (webapp/js/ui-state.js:41-65). The service inserts a row or updates the existing live row of that user and key (src/cluster_erp/modules/auth/ui_state/services.py:30-44).

Consequences

  • One person can neither read nor overwrite another person's layout; authentication alone guards the routes (src/cluster_erp/modules/auth/ui_state/routes.py:15-18).
  • A value is limited to 256 KiB of JSON (src/cluster_erp/modules/auth/ui_state/schemas.py:8-29), and a key to 60 characters (src/cluster_erp/modules/auth/ui_state/routes.py:32-33).
  • Each device class keeps its own home arrangement under its own key, for example springboard_phone (webapp/js/device-class.js:22-26).
  • When the first read fails, the client keeps its local copy instead of wiping the arrangement (webapp/js/ui-state.js:29-34).
  • Browser tests change server-side state of a real account, so they need a guard that restores it.

Where the code enforces it

Mechanism What it checks Source
UiStateService Every read and write is scoped to one user id src/cluster_erp/modules/auth/ui_state/services.py:12-44
UiStateWrite._bounded The stored value is JSON and at most 256 KiB src/cluster_erp/modules/auth/ui_state/schemas.py:13-29
guardUiState Browser suites run as the test account, on a known board, and restore the original web-client/_ui_state_guard.mjs:114-195
test_board_touching_suites_use_the_ui_state_guard Every suite that touches the layout uses the guard tests/test_e2e_db_isolation.py:177-200

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13