settings

package
v0.1.23 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package settings is the single description of everything about a Perfuse server that an operator may change.

Why this exists

Before this package, roughly thirty things were command-line flags and nothing else. Changing how long messages are kept meant editing a service definition and restarting, which in a hospital means a change ticket - so in practice nobody changed them, and a default chosen on the first afternoon became the setting forever.

Why one registry rather than a form

The obvious way to build a settings page is to write the form. That produces two descriptions of every setting - one in the server that validates it and one in the interface that renders it - and they drift. The drift is quiet: a field the server stopped accepting still has a control, so somebody sets it, sees it saved, and it does nothing.

So a setting is declared once, here, with everything needed to both check it and draw it: what kind of value it is, what control suits it, which group it belongs in, what it means, and whether changing it takes effect now. The interface asks for that description and renders whatever it is told. Adding a setting is one entry in this package and no interface work at all.

TestEverySettingIsFullyDescribed enforces the parts a human has to supply, because a setting with no help text is a setting somebody guesses at.

Why a file rather than the database

Settings live in a YAML file the interface edits in place, matching how channels and alert rules already work. Three reasons, in order of weight: a hospital's change control wants configuration that can be diffed and put in version control; an operator with the machine but not the application can read a file and cannot read a database; and restoring yesterday's settings should not require the application to be working.

Why flags only seed it

The file wins once it exists, and flags supply the initial contents. The alternative - flags always winning - would make the interface a liar, since anything ever passed on a command line could be edited and saved and silently ignored.

A flag that disagrees with the file is reported at startup, naming both values, because otherwise the flag stays in the service definition for years and the next person to read it believes it.

Index

Constants

View Source
const (
	GroupData     = "Data"
	GroupSecurity = "Security"
	GroupIdentity = "Sign-in"
	GroupAlerts   = "Alerts"
	GroupObserve  = "Monitoring"
	GroupEngine   = "Engine"
	GroupFHIR     = "FHIR"
	GroupFleet    = "Fleet"
	GroupBranding = "Branding"

	// GroupTEFCA is national exchange. Its own group rather than a corner of FHIR, because the questions are about
	// agreements, identifiers and audit obligations rather than about a wire format.
	GroupTEFCA = "TEFCA"
)

Groups, named as they appear in the interface.

Constants rather than literals so a typo becomes a duplicate group in the navigation rather than a compile error nobody gets.

Variables

This section is empty.

Functions

This section is empty.

Types

type Choice

type Choice struct {
	// Value is what is stored.
	Value string `json:"value"`

	// Label is what is shown.
	Label string `json:"label"`

	// Help explains the consequence of this option, not what it is called.
	//
	// Optional, but the reason the type has it: "strict" tells somebody nothing, and "refuse a file whose segment count
	// disagrees" tells them whether they want it.
	Help string `json:"help,omitempty"`
}

Choice is one option of a KindChoice setting.

type Effect

type Effect string

Effect says when a change takes hold.

const (
	// EffectLive takes hold on save.
	EffectLive Effect = "live"

	// EffectRestart needs the process restarted.
	//
	// Shown in the interface rather than left to be discovered. A settings page that silently needs a restart is how
	// somebody changes retention, sees that it saved, and finds out three weeks later that nothing was ever deleted.
	EffectRestart Effect = "restart"

	// EffectReconnect takes hold for connections made after the change, leaving existing ones alone.
	//
	// Its own value rather than being folded into live or restart, because both would be wrong in a way that matters: an
	// operator who tightens a credential needs to know the session already open is still open.
	EffectReconnect Effect = "reconnect"
)

type Kind

type Kind string

Kind is what sort of value a setting holds.

Deliberately small. Every kind here needs different validation and a different control, and a kind that needed neither would be a kind nobody could see the point of.

const (
	// KindBool is on or off.
	KindBool Kind = "bool"

	// KindInt is a whole number, usually with a range.
	KindInt Kind = "int"

	// KindString is a single line.
	KindString Kind = "string"

	// KindSecret is a single line that is never sent back.
	//
	// The interface is told whether one is set, not what it is. A settings page that renders a password into an input is a
	// settings page that puts it in the browser's memory, in a screenshot, and in whatever the browser saved.
	KindSecret Kind = "secret"

	// KindChoice is one of a fixed set.
	KindChoice Kind = "choice"

	// KindDuration is a length of time, held as a Go duration string.
	KindDuration Kind = "duration"

	// KindList is an ordered list of single lines.
	KindList Kind = "list"

	// KindCode is a multi-line value with a syntax - a YAML document or a script.
	KindCode Kind = "code"
)

type Registry

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

Registry is the full set of settings, in a stable order.

func NewRegistry

func NewRegistry(all []Setting) (*Registry, error)

NewRegistry builds a registry, refusing anything malformed.

Refuses rather than skips, and at construction rather than at first use: every caller of this is a program starting up, and a setting silently missing from the interface is worse than a server that will not start with a clear message.

func (*Registry) All

func (r *Registry) All() []Setting

All returns every setting in display order.

func (*Registry) Groups

func (r *Registry) Groups() []string

Groups returns the group names in display order, without duplicates.

func (*Registry) Lookup

func (r *Registry) Lookup(key string) (Setting, bool)

Lookup finds a setting by key.

type Setting

type Setting struct {
	// Key identifies it, dotted and stable: "data.retentionDays".
	//
	// Stable because it is what the file is keyed by. Renaming one silently abandons whatever an operator had set, so the
	// rename path is a migration rather than an edit.
	Key string `json:"key"`

	// Group and Subgroup place it in the interface.
	//
	// Two levels and no more. A third level is how a settings page becomes something you navigate rather than read.
	Group    string `json:"group"`
	Subgroup string `json:"subgroup"`

	// Label is the control's caption, in sentence case and without a trailing colon.
	Label string `json:"label"`

	// Help says what the setting does and what happens if it is wrong.
	//
	// Required. The whole reason settings were flags for so long is that a flag comes with a line of help text and a form
	// field does not, so the form is the thing that has to be made to carry it.
	Help string `json:"help"`

	// Kind is what sort of value it is.
	Kind Kind `json:"kind"`

	// Widget is how to draw it.
	Widget Widget `json:"widget"`

	// Effect says when a change takes hold.
	Effect Effect `json:"effect"`

	// Default is the value when nothing has been set.
	//
	// Typed to match Kind - bool, int, string, or []string - and checked by the drift test rather than trusted.
	Default any `json:"default"`

	// Min and Max bound a number. Both nil means unbounded.
	Min *int `json:"min,omitempty"`
	Max *int `json:"max,omitempty"`

	// Unit is what the number counts: "days", "percent", "MB".
	//
	// On the control rather than only in the help, because a box containing 30 is ambiguous and a box containing 30 next to
	// the word "days" is not.
	Unit string `json:"unit,omitempty"`

	// Choices are the options for KindChoice.
	Choices []Choice `json:"choices,omitempty"`

	// Language is the syntax for KindCode: "yaml" or "javascript".
	Language string `json:"language,omitempty"`

	// Flag names the command-line flag that seeds this setting, without its dash.
	//
	// Recorded so the interface can say where a value came from, and so a flag that disagrees with the file can be reported
	// by name at startup instead of as a mystery.
	Flag string `json:"flag,omitempty"`

	// Advanced hides it behind a disclosure.
	//
	// For settings that are real but that most installations should not touch. Hidden rather than absent, because a setting
	// nobody can find is one they will instead ask for as a feature.
	Advanced bool `json:"advanced,omitempty"`

	// PerServer marks a setting that names something on one machine (a certificate's path, an address to listen on, its
	// own name). In a cluster every server shares one settings file, so these are left out of it and each server takes
	// them from its own command line.
	PerServer bool `json:"perServer,omitempty"`

	// Sensitive marks a setting whose change is worth an audit entry beyond the ordinary.
	//
	// Anything that alters who can get in or what leaves the building.
	Sensitive bool `json:"sensitive,omitempty"`

	// Validate checks a value beyond its kind and bounds. Nil when kind and bounds are the whole story.
	//
	// Not serialised: the interface cannot run it, and pretending otherwise would mean writing the same rule twice. The
	// interface catches what it can and the server is what refuses.
	Validate func(any) error `json:"-"`
}

Setting is one thing an operator may change.

func Default

func Default() []Setting

Default is the registry of everything an operator may change.

Ordered by group here for reading; the registry sorts for display. Grouped by what somebody is trying to achieve rather than by which part of the code reads the value - "I want to stop keeping message bodies" is a thought about data, and it should not require knowing that payload storage is implemented in the message store.

Deliberately absent: the listen address, the database path and the channel directory. All three decide where the process finds the thing it is editing, so changing them through the interface would mean the interface saving a change to a file it would then stop being able to read. They stay flags and are shown read-only.

type Store

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

Store holds the current values and writes them to a file.

Safe for concurrent use: settings are read on nearly every request and written from the interface, so a mutex is cheaper than reasoning about which reads happen to be safe.

func NewStore

func NewStore(registry *Registry, path string) (*Store, error)

NewStore builds a store over a file, creating nothing.

The file is read if it exists. Every absent key falls back to its declared default, so a partial file is normal rather than an error - an operator who wants to set one thing should be able to write a file with one line in it.

func (*Store) Bool

func (s *Store) Bool(key string) bool

Bool returns a boolean setting.

func (*Store) Check

func (s *Store) Check(key string, value any) error

Check reports whether a value would be accepted for a key, without saving it.

Exists so the interface can explain a refusal as somebody types rather than only when they submit. It runs the same coercion and the same validator that Set does, so a value this accepts is one Set will accept - a preview that disagreed with the save would be worse than no preview.

func (*Store) Conflicts

func (s *Store) Conflicts(flagValues map[string]any) []string

Conflicts reports flags whose values disagree with the file.

The file wins, so a disagreement means the flag does nothing. Reported at startup by name and with both values, because otherwise the flag stays in the service definition for years and everybody who reads it believes it.

func (*Store) Get

func (s *Store) Get(key string) any

Get returns a setting's value, or its default when unset.

Panics on an unknown key, deliberately. Every caller passes a constant, so an unknown key is a programming error that should fail on the first run rather than return a zero value that behaves like a deliberate choice - a Get of a mistyped retention key returning 0 would mean "keep forever".

func (*Store) Int

func (s *Store) Int(key string) int

Int returns a numeric setting.

func (*Store) IsSet

func (s *Store) IsSet(key string) bool

IsSet reports whether a value was set rather than defaulted.

What the interface needs to distinguish "nobody has chosen" from "somebody chose the default", which for a secret is the difference between blank and configured.

func (*Store) List

func (s *Store) List(key string) []string

List returns a list setting.

func (*Store) Path

func (s *Store) Path() string

Path is where settings are written, empty when there is no file.

func (*Store) Registry

func (s *Store) Registry() *Registry

Registry is the description of every setting.

func (*Store) Reload added in v0.1.17

func (s *Store) Reload() ([]string, error)

Reload reads the file again, for a server in a cluster whose settings another server changed. A file that does not parse leaves the values as they were. It reports the keys whose values changed.

func (*Store) SeedFromFlags

func (s *Store) SeedFromFlags(flagValues map[string]any) error

SeedFromFlags writes an initial file from values the process was started with.

Only when the file does not exist. Existing installations keep working with their flags, get a file on first start, and can then be managed from the interface - without anybody editing a service definition to migrate.

func (*Store) SeedMissing added in v0.1.10

func (s *Store) SeedMissing(flagValues map[string]any) error

SeedMissing records the flags the process was started with that the file has no value for yet. A setting added after the file was first written (or a flag added to the service later) would otherwise read as its default in the console while the server ran with the flag's value.

func (*Store) Set

func (s *Store) Set(changes map[string]any) error

Set validates and applies changes, then writes the file.

All or nothing. A partial application would leave a server in a state neither the operator nor the file describes, and the interface sends a whole form - so one bad field must not save the others.

func (*Store) ShareAcrossServers added in v0.1.17

func (s *Store) ShareAcrossServers()

ShareAcrossServers says the file is shared by the servers of a cluster. Per-server settings (PerServer) are dropped from it, refused when saved, and never seeded from flags, so each server takes them from its own command line: one server's certificate path in the shared file would otherwise win on every server.

func (*Store) SharedAcrossServers added in v0.1.17

func (s *Store) SharedAcrossServers() bool

SharedAcrossServers reports whether ShareAcrossServers was called.

func (*Store) String

func (s *Store) String(key string) string

String returns a text setting.

type Widget

type Widget string

Widget is the control the interface should draw.

Separate from Kind because one kind suits several controls and the choice is about how the value behaves, not what it is. A number with a known range and a number without are both integers, but a slider for a port number is useless and a slider for a sampling percentage is exactly right.

const (
	// WidgetToggle is a switch, for a boolean.
	WidgetToggle Widget = "toggle"

	// WidgetSlider is for a bounded number where the useful thing is the relative position.
	WidgetSlider Widget = "slider"

	// WidgetNumber is a spinner, for a number where the exact value matters.
	WidgetNumber Widget = "number"

	// WidgetText is a single-line box.
	WidgetText Widget = "text"

	// WidgetSecret is a single-line box that never shows an existing value.
	WidgetSecret Widget = "secret"

	// WidgetRadio is for a small set of choices that should all be visible.
	//
	// Used where the options have consequences worth reading rather than picking blind - an acknowledgement level, a
	// retention policy. A dropdown hides them until somebody opens it.
	WidgetRadio Widget = "radio"

	// WidgetSelect is for a set too long to show at once.
	WidgetSelect Widget = "select"

	// WidgetList is a repeated single-line editor.
	WidgetList Widget = "list"

	// WidgetCode is a CodeMirror editor.
	WidgetCode Widget = "code"
)

Jump to

Keyboard shortcuts

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