From de7aca4fa62b6c08c8d28f1fd63fb00d49ad3587 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 17:10:15 +0200 Subject: [PATCH] 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. --- .../00-overview.md | 39 ++++++ .../01-the-survey.md | 115 ++++++++++++++++++ ...st-and-the-operators-own-value-has-none.md | 10 ++ 3 files changed, 164 insertions(+) create mode 100644 01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md create mode 100644 01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md diff --git a/01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md b/01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md new file mode 100644 index 00000000..cc6e01f4 --- /dev/null +++ b/01-RESEARCH/037-the-catalogues-hard-coded-preferences/00-overview.md @@ -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. diff --git a/01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md b/01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md new file mode 100644 index 00000000..d94ade01 --- /dev/null +++ b/01-RESEARCH/037-the-catalogues-hard-coded-preferences/01-the-survey.md @@ -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. diff --git a/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md b/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md index 66791f23..5cee9c70 100644 --- a/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md +++ b/02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md @@ -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`