ADR 0262: one listing of every preference, and research 037 surveying the literals to convert

The operator asked for one place that lists every module's preferences and each machine's value, and
for a list of the values the catalogues hard-code that should become preferences.
This commit is contained in:
jochen
2026-10-08 17:55:04 +02:00
parent 0e5a5e2085
commit de7aca4fa6
3 changed files with 164 additions and 0 deletions
@@ -0,0 +1,39 @@
---
status: active
initiated: 2026-10-08
touches:
- 02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md
- 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
- the catalogue's module manifests
- the media catalogue's module manifests
became: []
---
# 037 — The catalogue's hard-coded preferences
## What is investigated
[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)
lets a manifest give a **preference** a default, which the mesh-wide settings and a node's settings
override. Before it, a value a person might want to tune per machine could only be written into a
module's file as a literal. This effort lists those literals in both catalogues: the value, where it
is, the key and default it would take as a preference, and why a person would change it.
## Why
The first case was the notifier's font size, which is less than half as tall on a laptop panel of
about 240 DPI as on a desktop monitor of about 110 DPI, while X is told 96 on both. The same is true of
every size, font and gap on the desktop, and of retention counts, timeouts and thresholds in the
services. A list says which modules to convert, in what order, and which values are shared between
modules and should keep one default.
## What it touches
The manifests of both catalogues, and through ADR 0262 the controller's `settings` verb, which lists
every declared preference. Nothing is changed here: each conversion is a change to its module, made on
the operator's yes.
## Documents
- [01 — the survey](01-the-survey.md): every candidate, by module, strongest first, with the modules
that already take settings and those that have nothing to convert.
@@ -0,0 +1,115 @@
# 01 — The survey
Both catalogues were read on 2026-10-08: every manifest's file contents and the sources they are
copied from, the shell code modules contribute to the login session, and the contributions to the
display session. Nothing was changed. Each row is a literal that is a preference in the sense of
[ADR 0262](../../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md):
true of the software, and only tuned per machine or per mesh. The proposed default is always today's
value, so converting a module changes no machine until a value is set.
Values that are the operator's own (names, addresses, accounts, secrets) are left out, as are values the
mesh assigns (`${port:…}`, `${dir:…}`, `${machine:…}`, `${bound:…}`, `${secret:…}`) and structure nobody
tunes. Every proposed key passes ADR 0262's whole-word check.
**Where a literal sits decides what converting it needs.** A row marked *(shell)* is in code a module
contributes to the login shell, and one marked *(contribution)* is in a contribution to the display
session. ADR 0262 fills `${setting:…}` in files, contributions and served facts, but not in shell code,
so the *(shell)* rows need the controller to fill shell code too before they can convert. A row marked
*(compiled)* is in a module's own program and needs the program to read the value from a file first.
## The desktop, strongest first
| Module | Where | Today | Key | Why a person changes it |
|---|---|---|---|---|
| xorg | the X resources file | `Xft.dpi: 96` | `dpi` | A laptop panel of about 240 DPI and a desktop monitor of about 110 DPI want different values; the file's own comment already expects a setting |
| xterm | X resources *(shell)* | `faceSize: 12`, face JetBrainsMono Nerd Font, `saveLines: 10000` | `font-size`, `font`, `scrollback-lines` | Terminal text size per screen; the face and history are taste |
| i3 | its configuration | `font pango:JetBrainsMono Nerd Font 11` | `font-size` | Title text size follows the screen's density |
| i3 | its configuration | `gaps inner 0`, `gaps outer 0`, `default_border pixel 1`, resize step 10 px | `inner-gap`, `outer-gap`, `border-width`, `resize-step` | Taste and density; a 1-pixel border is hard to see on a dense panel |
| i3 | its configuration | focused colour `#de5200` | `accent-colour` | Taste; the same colour is in the bar and the launcher (see below) |
| i3status-rust | the display-session contribution *(contribution)* | bar font size 11 | `font-size` | The bar's height follows it |
| i3status-rust | the top bar | window title cut at 50 characters | `title-width` | How much title fits depends on the screen's width |
| i3status-rust | the bottom bar | disk warning at 850 GB, critical at 920 GB | `disk-warning`, `disk-critical` | Thresholds written for one disk; wrong on a smaller laptop drive |
| i3status-rust | the bottom bar | updates warn at 10, polled every 1800 s; clock format with seconds, every 1 s; container block every 2 s, disk every 20 s | `updates-threshold`, `updates-interval`, `clock-format`, `refresh-interval` | Polling costs battery; the clock is taste |
| rofi | its theme | font size 11, width 600 px, 8 lines, icon 24 px, padding 10 px, message font size 11 | `font-size`, `width`, `lines`, `icon-size`, `padding`, `message-font-size` | The launcher's size per screen |
| rofi | the power menu theme | width 800 px, font size 32 | `power-menu-width`, `power-menu-font-size` | Too large on a small panel. The power menu script also passes 350 px, which overrides 800: one of the two is dead |
| rofi | its configuration | history of 25 | `history-size` | History length |
| screen-lock | the session's X settings *(shell)* | lock after 1800 s; display standby 1800 s, off 3600 s | `lock-after`, `display-standby-after`, `display-off-after` | The clearest laptop and desktop difference |
| screen-lock | its wrapper script | blur 5, ring width 7.0, accent `#ca4a00`, time and date formats | `blur`, `ring-width`, `accent-colour`, `clock-format`, `date-format` | Look, density and locale |
| picom | its configuration | terminal opacity 0.95 focused and 0.75 not; tooltip 0.95; fade step 0.05; corner radius 0 | `terminal-opacity`, `inactive-terminal-opacity`, `tooltip-opacity`, `fade-step`, `corner-radius` | Taste |
| picom | its configuration | `backend = "glx"` with vsync | `backend` | Depends on each machine's graphics driver |
| adwaita | GTK 3 and 4 settings, Qt settings, and the GNOME settings *(shell)* | interface font Inter 11, monospace 11; cursor size 24 (also in X resources and GNOME settings); double-click 400 ms, 3 scroll lines | `font-size`, `cursor-size`, `double-click-interval`, `scroll-lines` | Interface size on a dense panel; the cursor size must change in all four places together |
| fonts | the fontconfig defaults | monospace JetBrainsMono Nerd Font, sans Inter, serif Noto Serif | `monospace-face`, `sans-face`, `serif-face` | Taste; every desktop module names these faces again |
| the laptop's hardware module | touchpad configuration and its script | acceleration 0.15, tapping on, natural scrolling on | `touchpad-speed`, `tap-to-click`, `natural-scrolling` | Pointer feel; the script repeats 0.15 |
| the laptop's hardware module | the power daemon's battery levels | low 15 %, critical 10 %, act at 7 %, action suspend | `battery-low`, `battery-critical`, `battery-action-at`, `battery-action` | Thresholds move as a battery wears |
| the laptop's hardware module | its scripts | brightness step 5; on-screen display for 1500 ms | `brightness-step`, `notification-timeout` | Feel |
| feh | the wallpaper command | `--bg-fill` | `wallpaper-mode` | Fill, scale or centre is taste |
| zsh | its rc file | `HISTSIZE` and `SAVEHIST` 100000 | `history-size` | History length |
| lemurs | its configuration | background black, border white | `background-colour` | Low value; only the start of the file was read |
## Services
| Module | Where | Today | Key | Why a person changes it |
|---|---|---|---|---|
| restic | its program *(compiled)* | keep 14 daily, 8 weekly, 6 monthly | `keep-daily`, `keep-weekly`, `keep-monthly` | Retention is the clearest service preference, and today it cannot be changed without a new build |
| logrotate | its configuration | weekly, keep 4; the login record monthly, keep 1 | `rotate-period`, `rotate-count`, `wtmp-rotate-count` | Retention per machine and disk |
| docker | the prune unit and timer | prune what is older than 168 h, weekly | `prune-age`, `prune-schedule` | How long images and build cache stay |
| docker | the daemon's configuration | no log rotation at all | `log-size`, `log-files` | **A new value, not a conversion:** container logs grow without bound today. A default such as 10 MB and 3 files changes every machine, so it is a decision of its own |
| memory-pressure | the zram generator | `min(ram / 2, 16384)` | `zram-size` | RAM and workload |
| memory-pressure | the out-of-memory daemon and the session's limit | swap used 90 %, pressure 60 % for 20 s; the session 80 % | `swap-used-limit`, `pressure-limit`, `pressure-duration`, `session-pressure-limit` | How early processes are killed |
| time-sync | its servers | a regional pool | `pool-region` | The region is the mesh's |
| pacman | its configuration and the mirror list's options | 5 parallel downloads; a country list, the latest 20, sorted by rate | `parallel-downloads`, `mirror-countries`, `mirror-count` | Bandwidth and region |
| fail2ban | its jails | ban 1 day after 3 failures in 1 day; repeat offenders 4 weeks after 2 in 2 weeks | `ban-time`, `find-time`, `max-retries`, `repeat-ban-time`, `repeat-find-time`, `repeat-max-retries` | Strictness |
| sshd | its configuration | port 22 | `listen-port` | A port people move. The filter and the jail name the port too, and must follow it |
| dnsmasq | its configuration | two public resolvers | `upstream-resolvers` | The choice of resolver |
| icecast | its configuration | client timeout 30 s, source 10 s, burst 65535, queue 524288, log level 3, log size 10000 | `client-timeout`, `source-timeout`, `burst-size`, `log-size` | Streaming tuning |
| mailu | its environment file | message size 50 MB, 200 messages a day, sign-in rate limits, fetch every 600 s, log level | `message-size-limit`, `message-rate-limit`, `sign-in-rate-limit`, `fetch-delay`, `log-level` | Real limits a person sets |
| matrix | the homeserver's configuration | request size 20 MB | `upload-size-limit` | The upload cap |
| supabase | three files | a sign-in lifetime of 3600 s, written three times | `session-lifetime` | One value in three places, which a setting keeps equal |
| supabase | the functions' main | 150 MB, 60 s | `function-memory-limit`, `function-time-limit` | Function limits |
| power | its stop unit and script | 120 s, 30 s | (weak) | Bounds on shutdown work, rarely changed |
## The media catalogue
| Module | Where | Today | Key | Why |
|---|---|---|---|---|
| qbittorrent | its web login script | ban for 60 s | `ban-duration` | How long a failed login is locked out |
| tautulli | its start script | wait 60 s for the media server | `media-server-wait` | Low value |
The other media modules carry only identities from settings, key-provisioning scripts and mergeable
files. Nothing to convert.
## Already taking settings
- **`${setting:…}`:** the notifier (font size and width, the first preferences), power, hostname, xdg,
keycloak, mailu, lab, and every media module.
- **A mergeable file:** baserow, claude-code, the licence manager, confluence, gitea, gitlab, grafana,
home-assistant, icecast, influxdb, jira, keycloak, letta, mailu, matrix, mesh-watcher, messenger,
nextcloud, nodered, records, route-adapter, searxng, step-ca, supabase, unifi; in the media
catalogue bazarr, bookshelf, jackett, kometa, lidarr, nzbget, ombi, qbittorrent, radarr, sonarr and
tautulli. A mergeable file's content already is its defaults; ADR 0164's declaration would list its
keys.
## Nothing to convert
avahi, blueman, bluetooth, build-agent, ca-trust, clipmenu (its 1-second timeout is a safety bound),
cups, dbus, de-spiegel, disk-load, distribution, dmenu, gnome-keyring, hello-web, invoicing, letta,
local-model-consumer, localization (the language and keymap are the operator's, not a preference),
mesh-catalog, minio, model-usage, mongodb, mosquitto, mssql, n8n, nats, networkmanager, nftables,
node-env, only-office, redis, route-proxy, ssh-client, sudo, systemd, systemd-networkd,
systemd-resolved, triggerhappy, umami, xclip. A container's internal port is structure, not a
preference.
## What follows from it
- **Shared values want one default each.** The accent colour is `#de5200` in the window manager, the
bar and the launcher, and `#ca4a00` in the lock screen. The interface font size is 11 in the window
manager, the bar, the launcher and the GTK, Qt and GNOME settings, and 12 in the terminal. As one
setting per module they would drift. Either each module declares the same default and a person sets
them together, or the mesh grows a preference several modules read. That is a decision, not a
conversion.
- **Three prerequisites in the controller and the modules:** filling `${setting:…}` in shell code, for
the *(shell)* rows; the restic program reading its retention from a file; and, for the container
runtime's log rotation, a decision on the new default.
- **Order:** the density rows first (xorg's DPI, then the font sizes of the window manager, the bar,
the launcher, the terminal and the interface, and the cursor size), because the laptop panel is the
case that started this. Then the lock and display timeouts, then retention and thresholds.
@@ -87,6 +87,13 @@ reads before replacing them ([ADR 0217](0217-no-change-to-a-machine-takes-effect
each key the module gives a default or that is set, its effective value, and its source: `default`,
the mesh, or the node. Where the mesh or the node overrides a default, the default is shown beside it.
**One listing of every preference, in the controller's `settings` verb.** Asked with `list:
"preferences"`, or with no module, `settings` lists every module that declares a preference: each key,
its default and why, and, for every machine the module is assigned to, the value there and where it
comes from (the default, the mesh, or the node). A module and a node narrow it. No module builds a
settings tool of its own; this verb is the interface for all of them, so a person or an agent finds every
preference in one place.
**A default must be read by something.** A declared key that no file, contribution or served fact asks
for is refused, because a default that reaches nothing is the same mistake as a stray setting.
@@ -139,6 +146,7 @@ rolled back. The same holds for every field a later record adds.
| A default is read by the installation check | `TestTheInstallationCheckReadsADefault`, including a setting called `description` |
| A default fills a contribution and a served fact, and never replaces a contribution's own value | `TestADefaultFillsAContributionAndAServedFactButNotALiteral` |
| A node called `default` is a node | `TestANodeCalledDefaultIsANodesLayer` |
| One listing of every preference, per machine with its source | `TestSettingsListPreferences` (the verb's arguments, a listing other than preferences refused) and `TestPreferencesSayEachMachinesValueAndItsSource`; live, `mesh-controller.settings {list: "preferences"}` lists the notifier on both machines |
| A stored manifest with an unknown key is read, and registration refuses it | `TestAStoredManifestWithAnUnknownKeyIsReadAndRegistrationRefusesIt` |
| Nothing changes where no machine sets a value | The notifier's manifest test fills the file with the defaults and finds today's lines; live, `mesh-controller.plan` for the workstation shows no change to the notifier's file after the catalogue change is sent |
@@ -154,4 +162,6 @@ rolled back. The same holds for every field a later record adds.
before replacing them
- [Design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md): the operator as a
provider
- [Research 037](../01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md): the literals in
both catalogues that would become preferences
- mesh-controller `internal/catalogue/setting_defaults.go`; mesh-catalog `modules/dunst`