userprefs

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package userprefs stores how one person reads the console: how a number is written, which instant a timestamp means, and which words the translated screens use (ADR-0459).

It is per user rather than per browser because of the time zone. A language is how somebody reads at one desk, and ADR-0267 kept it in the browser for that reason; a time zone is where somebody *is*, and a person who answers tasks on a laptop and checks a stuck instance on a tablet has one of those and two of these. A preference they must set again per device is one they will have set wrong somewhere, and a timestamp read in the wrong zone is an operational error rather than an ugly one.

The server stores and validates; it does not format. Formatting happens in the browser, which is why validation here is strict: an unknown zone stored would come back as an exception from `Intl.DateTimeFormat` and break every timestamp on the screen instead of this one field.

Index

Constants

This section is empty.

Variables

View Source
var Languages = []string{"de", "en"}

Languages are the locales the console's catalogue actually holds (ADR-0267). A language without a catalogue would render every translated screen as its own message keys, so the list is the catalogue's and is checked, not trusted.

View Source
var Regions = []string{"international", "ch", "de", "us"}

Regions are the number and date formats on offer. It is a fixed list rather than a locale picker, and that is the decision: each entry's output is a table in the console's own formatter (api/web/numfmt.js), so the preview on the settings screen promises exactly what the badge will show. A locale handed to `toLocaleString` would render whatever the browser's data says this year, which is neither previewable nor testable.

"international" is the default and is the unset value, not a fifth row: the console formats that way today, so an instance where nobody opens the screen keeps the behaviour it has.

Functions

This section is empty.

Types

type Prefs

type Prefs struct {
	// UserID is the authenticated principal's id. It is the store's key and is
	// never read from a request body.
	UserID string `json:"userId"`
	// Region is how a number and a date are written. One of Regions, or "".
	Region string `json:"region"`
	// TimeZone is which instant a timestamp means: an IANA name, or "" for the
	// device's own zone.
	TimeZone string `json:"timeZone"`
	// Language is which words the translated screens use. One of Languages, or "".
	Language string `json:"language"`
	// UpdatedAt is when this record was last written, in unix nanos. It is here so
	// a support question ("since when has this been set?") has an answer.
	UpdatedAt int64 `json:"updatedAt,omitempty"`
}

Prefs is one person's answer to the three questions. Each field's zero value is "not chosen", which is why none of them is a pointer and why "international" and "" both mean the default: a person who clears the screen gets the console they had before it existed.

func (Prefs) Validate

func (p Prefs) Validate() error

Validate reports why a record may not be stored, or nil.

Every refusal names the field, because this validates a form somebody filled in and "invalid request" would send them looking at all three.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service serves one person's display preferences: GET and PUT on /api/v1/me/preferences, and nothing else. There is no listing and no admin view — a preference is read by the person it belongs to, and by nobody else.

func New

func New(loop *runloop.Loop, store *Store) *Service

New builds the service over the store, serializing every access on the loop: a sidecar store does no locking of its own, and two tabs saving at once is the ordinary case for a setting a person edits on one screen.

func (*Service) HandleGet

func (s *Service) HandleGet(w http.ResponseWriter, r *http.Request)

HandleGet answers this person's preferences.

Nothing stored is not a miss: it is the state every account starts in, and the answer is the unset record, which is the console's behaviour before anybody opened the screen.

func (*Service) HandleSave

func (s *Service) HandleSave(w http.ResponseWriter, r *http.Request)

HandleSave rewrites this person's preferences and answers the stored record.

The whole record is written rather than patched: the screen edits three fields together and sends all three, so a field left out is a field cleared — which is how "back to the defaults" works without a second endpoint.

type Store

type Store struct {
	*sidecar.Store[Prefs]
}

Store is the durable home of the display preferences: one JSON file per user id, with the atomic-write discipline every design-time store shares. It is owned by the run loop, like the stores it sits beside, and does no locking of its own.

func NewStore

func NewStore(dir string) (*Store, error)

NewStore opens (creating if needed) the preferences directory.

The key is the user id, and the filename scheme is the default: user ids come from whichever directory signs people in — an email address, an OIDC subject, a UUID — and hex-encoding is what makes any of those a safe, unique, deterministic filename. The encoding also guards the key on the way in, so an id carrying a slash cannot address a file outside the store.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL