Documentation
¶
Overview ¶
Package userprefs stores how one person reads the console: how a number is written, which instant a timestamp means, which words the translated screens use (ADR-0459), and whether the console is drawn light or dark (ADR-0507).
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 ¶
var Appearances = []string{"light", "dark", "system", "cockpit"}
Appearances are how the console may be drawn (ADR-0507): light, dark, following the operating system's scheme, or the cockpit skin (ADR-0509), a playful dark variant nobody gets without choosing it. "" is the unset value and draws today's light console; "light" is the same picture chosen on purpose, which matters once the default changes. The list is the stylesheet's — app.css answers exactly these.
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. The api package replaces it at start-up with the message files the embedded console carries (ADR-0497); this default is what the package's own tests and a console-less build see.
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"`
// Appearance is whether the console is drawn light or dark. One of
// Appearances, or "". It is here rather than in the browser for the reason
// the time zone is: it belongs to the person, and `system` already covers a
// person who wants each device to follow its own scheme.
Appearance string `json:"appearance"`
// 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 four 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.
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 ¶
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 its fields together and sends all of them, so a field left out is a field cleared — which is how "back to the defaults" works without a second endpoint. The top bar's light/dark switch sends the whole record too, with only the appearance changed.
type Store ¶
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 ¶
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.