mesh/merge-gate head c9a711c259ed merged into trunk 827b89dbf109 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head c9a711c259ed merged into trunk 827b89dbf109 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
writeAccountLists checked only for a running Betterbird and wrote without a sync; it now waits for the profile's lock too and writes atomically. readLayer read any answer that was not JSON as an empty layer, and a proposal replaces the layer whole, so a changed wording would have dropped accounts; it now refuses. The README says the rollout order that leaves no node on Betterbird's defaults.
350 lines
29 KiB
Markdown
350 lines
29 KiB
Markdown
# betterbird
|
||
|
||
Betterbird, the mail and calendar client of g14 and shanks (a Thunderbird fork, with the operator's
|
||
Exchange calendar through the Owl add-on), as a module (novox/hq issue 345, issue 586, ADR 0208, ADR 0316,
|
||
ADR 0317, ADR 0329, to-be 56). It requires `x11-display`, so it is assigned only where a display server is held
|
||
on the same machine. It installs Betterbird from a pinned upstream release, ships Owl with it through
|
||
Betterbird's enterprise policy, and owns Betterbird's whole configuration as settings: the preferences, the
|
||
mail accounts without their credentials, the identities with their signatures, the CalDAV calendars, the
|
||
CardDAV address books, the filters and the tags.
|
||
|
||
It exists because a symptom had no tools: on a workstation, a calendar reminder appeared as a
|
||
transparent window with nothing in it (2026-10-09). Nothing in the mesh could say which window that
|
||
was, how the compositor treats it, or which preferences Betterbird ran with. The diagnosis is in hq
|
||
issue 345.
|
||
|
||
## Owns
|
||
|
||
| what | where | class |
|
||
|---|---|---|
|
||
| Betterbird, the upstream release (`archive`, pinned by URL and sha256) | `/opt/betterbird` | owned, whole |
|
||
| the enterprise policy (`install/distribution/policies.json`) | `/opt/betterbird/distribution` | owned, whole |
|
||
| Owl's xpi (`owl`, pinned by the vendor's sha256) | `/opt/betterbird/owl` | owned, whole |
|
||
| the module's defaults (`install/defaults-pref/mesh-defaults.js`: the preferences kept at Betterbird's own default, no value of the operator's) and the spelling dictionaries' default (`vendor-prefs.js`) | `/opt/betterbird/defaults/pref` | owned: its two files |
|
||
| the tools' program, for the start's preparation | `/usr/local/lib/mesh-betterbird` | owned, whole |
|
||
| the start | `/usr/local/bin/betterbird` | owned, whole |
|
||
| the launcher entry | `/usr/local/share/applications/eu.betterbird.Betterbird.desktop` | owned, whole |
|
||
| the module's directory, with the settings as the node-engine writes them (`prefs.json`, `accounts.json`, `configuration.json`, `tags.json`), the records of what was adopted or created, and the filter files it replaced (`kept/`) | `~/.config/betterbird/` | owned, whole |
|
||
| the profile's `user.js`, placed by the tools | the profile | the module's, or never touched |
|
||
| the lists in the profile's `prefs.js` (accounts, outgoing servers, an adopted account's identities, the calendars' order), and the preferences of what the module created and the settings no longer hold | the profile | written into: the module's keys only, only while Betterbird is closed |
|
||
| each declared account's `msgFilterRules.dat` (setting `filters`) | the account's directory in the profile | owned, whole, only while Betterbird is closed |
|
||
| the profile's own values of the preferences the module manages, in `prefs.js` | the profile | cleared only by `betterbird_config_apply` with apply, Betterbird quit |
|
||
| the profile: mail, address books, calendars, logins | `~/.thunderbird/` | found (ADR 0182) |
|
||
|
||
**The community package `betterbird-bin` is declared absent** (ADR 0180), before anything is placed in
|
||
`/opt/betterbird`: the node-engine removes it with `pacman -R`, then unpacks the release where the package
|
||
had it. Betterbird chooses its profile by its installation's path, so the same profile opens after the
|
||
switch. A package left in place (its removal failed) holds the path, and the release is refused there by
|
||
the node-engine rather than mixed in. The module creates no link: the start, a launcher entry naming the
|
||
icon in the installation, and the dictionaries as a default preference replace the package's links.
|
||
|
||
**Every new build waits for the operator** (`upgrade: record`): the first push to a node removes the
|
||
package and replaces what a running Betterbird has open, so the operator quits Betterbird, then pushes.
|
||
An upgrade is a change to the pins, reviewed and delivered like any other (ADR 0316 §5).
|
||
|
||
**The pins.** The release is `betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz` (81,620,684 bytes) and
|
||
Owl is 1.5.6.1 (430,201 bytes), each pinned by sha256 in `module.json`. The build seat fetches each once,
|
||
refuses it when the digest differs, keeps it under its sha256, and packs it into the artifact store; a node
|
||
receives the mesh's archive by digest and never asks the vendor. **Owl is never in this repository**: its
|
||
licence forbids copying it to third parties, and its vendor serves only its newest version, at one
|
||
address. Keeping the pinned copy in the mesh's private artifact store for the operator's own machines was
|
||
the operator's call (2026-10-12, ADR 0316 §6).
|
||
|
||
**Owl's range.** Owl declares which Betterbird versions it supports (`strict_min_version` to
|
||
`strict_max_version`: 115 to 156 for 1.5.6.1). `owlSupports` in `manifest_test.go` holds the range read
|
||
from each pinned xpi, and the module's tests refuse a Betterbird pin outside it: moving Owl's pin means
|
||
reading the new range there (ADR 0316 §8).
|
||
|
||
**The policy** turns Betterbird's own updater off (`DisableAppUpdate`, which Betterbird's update check
|
||
honours) and installs Owl into every profile from `file:///opt/betterbird/owl/owl.xpi` as
|
||
`force_installed`, with `updates_disabled`. A profile that already runs another Owl keeps it: the policy
|
||
installs the pinned one where Owl is missing. A newer Owl in the profile (g14 runs 1.5.7) is said as a
|
||
fact by `betterbird_status`; Owl installed by policy cannot be removed in Betterbird, so the remedy is
|
||
moving the pin. An older one is a finding of `betterbird_check`, with the same remedy.
|
||
|
||
**Betterbird never asks to be the default client.** The policy locks `mail.shell.checkDefaultClient` off
|
||
(`Preferences`, `Status: locked`): Betterbird's own default (`all-thunderbird.js`) is to ask at every start,
|
||
with a window to make it the default for mail, news, feeds and calendar files, while that preference is true
|
||
(`messenger.js`). Betterbird 153 has no policy of its own for it (its `policies-schema.json`), and its
|
||
`Preferences` policy accepts the `mail.` prefix. A lock, not a default: the window's own "always check" box
|
||
writes the preference into the profile, which wins over a default; a lock wins over the profile and greys the
|
||
box out in the settings. `betterbird_check` says when Betterbird would ask (novox/hq issue 345). **Owl's licence stays the person's**: the mesh holds no licence, key or payment (ADR 0316 §9).
|
||
|
||
**No start of its own.** Betterbird is started by the operator from the launcher. The start
|
||
(`/usr/local/bin/betterbird`) first runs `betterbird-tools prepare`, which, with no Betterbird of the
|
||
account running, places the user.js and writes what Betterbird rewrites itself (the lists, the removals, the
|
||
filter files); then it
|
||
starts Betterbird either way. `betterbird_restart` starts it as the transient user unit
|
||
`mesh-betterbird`, so its output then goes to the journal.
|
||
|
||
|
||
## The preferences: locks, the setting prefs, and Betterbird's own defaults
|
||
|
||
Betterbird's preferences come in three layers (novox/hq issue 345, issue 586). **The operator's own choices
|
||
live in the module's configuration, never in the catalogue**: the module ships only its policy's locks and the
|
||
names it keeps at Betterbird's own default.
|
||
|
||
| layer | where | who changes it | holds |
|
||
|---|---|---|---|
|
||
| **locked** | `/opt/betterbird/distribution/policies.json` (`install/distribution/`) | only a reviewed catalogue change | what must never differ on a node; Betterbird's settings show it greyed out |
|
||
| **declared** | the setting `prefs`, mesh-wide with a node's layer over it, written to `~/.config/betterbird/prefs.json` and placed in the profile's `user.js` | the operator, through the settings layers (trusted: the terminal or the operator's warrant) | the operator's setup, and a node's exceptions, such as a font size for a larger screen |
|
||
| **Betterbird's default** | Betterbird's own defaults; `mesh-defaults.js` names those the module manages there | a catalogue change | the preferences kept at Betterbird's default |
|
||
|
||
A locked value wins over everything; a declared one (in `user.js`) wins over the profile's `prefs.js` at every
|
||
start; `prefs.js` wins over a default.
|
||
|
||
**The setting `prefs` is one entry per preference** (issue 586): `{"prefs": {"mail.uifontsize": 14, …}}`, the
|
||
value true, false, a whole number or a string. The node-engine merges the layers key by key, so a node's layer
|
||
overrides one preference, or takes one out with `null`. The operator's setup approved on 2026-10-12 (compact
|
||
density, 14-point text, the vertical view with multi-line cards and threads, no new-mail pop-up, sound or tray
|
||
icon, the calendar's day from 09:00 to 20:00, reminders missed while asleep shown, chat off, remote content
|
||
blocked, Do Not Track; 21 preferences) moved from the catalogue's defaults file into the mesh-wide layer of
|
||
`prefs`, set on the operator's Approve.
|
||
|
||
**The order that leaves no node on Betterbird's own defaults** (issue 586): merge this build (the `record`
|
||
upgrade policy holds it back from the nodes); propose the mesh-wide layer through the controller's `settings`
|
||
verb with `propose`, carrying the current `accounts` verbatim and `prefs` with the 21 values; the operator
|
||
approves; only then push the desktop nodes. Caveat: between that approval and a node's push, any other send to a
|
||
desktop node writes the map's text into the old build's user.js, and `betterbird_check` there says the user.js is
|
||
refused until the push.
|
||
|
||
**Locked** (the policy): Betterbird's updater off (`DisableAppUpdate`); telemetry off (`DisableTelemetry`);
|
||
add-ons change only through the mesh's pins (`ExtensionUpdate` false); and three preferences locked off:
|
||
`mail.shell.checkDefaultClient` (the ask to be the default client), `mail.inappnotifications.enabled` (the
|
||
vendor's in-app notices), and `mailnews.start_page.enabled` (the start page, which otherwise loads from the
|
||
vendor at every start).
|
||
|
||
**Kept at Betterbird's default** (`// keeps Betterbird's default: <name>` in `mesh-defaults.js`): the theme,
|
||
system colours, always-underlined links, manual spam marking, and the composer's attachment pane. A value the
|
||
profile saved for one of them is drift.
|
||
|
||
**Drift.** `betterbird_config` shows each managed preference (locked, declared, kept) through the layers, and
|
||
flags **drift**: a declared value the profile's `user.js` does not hold yet (remedy: `betterbird_user_js` with
|
||
apply, then `betterbird_restart`), or a value the profile saved where the module keeps Betterbird's default.
|
||
`betterbird_check` reports it.
|
||
|
||
- `betterbird_config_apply` with `apply` takes a kept preference's saved value out of `prefs.js`, so Betterbird's
|
||
default holds at the next start. It acts only while Betterbird is quit (no main process, and no `lock` in the
|
||
profile), and says what it cleared.
|
||
- **The theme is the exception.** Betterbird keeps the active theme in its add-on database (`extensions.json`),
|
||
and `extensions.activeThemeID` only mirrors it. Another theme is drift whose remedy is to enable the default
|
||
theme in the add-on manager. `betterbird_config_apply` never clears it, and `keep` refuses it.
|
||
- `betterbird_config` with `keep` proposes this node's layer of `prefs` holding the profile's values of the named
|
||
preferences beside what the layer holds, through the controller's settings verb, for the operator's Approve.
|
||
|
||
**One owner per value.** `prefs` refuses a preference whose name can hold a credential, an account's
|
||
(`mail.account.`, `mail.identity.`, `mail.server.`, `mail.smtpserver.` and the account lists, ADR 0316 §11), and
|
||
those the settings below own: `calendar.registry.`, `calendar.list.`, `ldap_2.servers.` and `mailnews.tags.`
|
||
(ADR 0329 §6).
|
||
|
||
## The user.js
|
||
|
||
Betterbird reads `user.js` in its profile when it starts, and a preference set there wins over the one it saved
|
||
in `prefs.js`. The profile's directory has a random name per machine (`<random>.default-default`), which a
|
||
manifest cannot name. So the tools compose the user.js from the settings — `prefs`, then the declared accounts,
|
||
then the declared identities, calendars, address books and tags under the keys each resolved to — and place it
|
||
in the profile: at the start from the launcher, `betterbird_user_js` with `apply`, or `betterbird_restart`.
|
||
|
||
**The profile is found** as the one the running Betterbird holds open; with none running, as the
|
||
installation's default in `profiles.ini` (`[Install…] Default`), else the profile marked `Default=1`.
|
||
|
||
**A user.js the module did not write is never overwritten.** The module's begins with the line
|
||
`// Betterbird's user.js (module betterbird`; any other is the operator's, and `apply` refuses with what to do.
|
||
|
||
## The accounts: declared without credentials (ADR 0316 §10–§16, ADR 0317)
|
||
|
||
The setting `accounts` reaches the machine as `~/.config/betterbird/accounts.json`, a trusted mergeable
|
||
file: a map of accounts by name, each with `address`, `from-name` and `label` (optional), `incoming` (`kind`
|
||
imap or pop3, `host`, `port`, `security` tls, starttls or none, `sign-in` password, encrypted-password,
|
||
oauth, kerberos, ntlm or tls-certificate, `user`) and `outgoing` (the same, without a kind; its sign-in may
|
||
be none), and, only in a node's own layer, `key`. **Never a password or a token**: the whole file is
|
||
refused when any key in it is shaped like one, and an account signs in at its first use, in Betterbird's
|
||
own window, where Betterbird keeps the result in its login store. The work account through Owl is never
|
||
declared: the person creates it in Owl's setup (ADR 0316 §13).
|
||
|
||
At every apply (the start from the launcher, `betterbird_user_js` with apply, `betterbird_restart`), each
|
||
declared account, in its place in the setting, is (ADR 0317 §1):
|
||
|
||
1. **kept by its recorded keys**, when the module recorded it on this node for this profile;
|
||
2. **its creation**, when it is under the module's own keys (`account_mesh_<name>`, `id_mesh_<name>`,
|
||
`server_mesh_<name>`, `smtp_mesh_<name>`);
|
||
3. **adopted by the key a node's layer names** (`key`), when that account is in the list, its default
|
||
identity has the address and it is not Owl's;
|
||
4. **adopted by its address**, when exactly one account's default identity has it: its keys (account,
|
||
default identity, incoming server, and the outgoing server that identity names) become the module's, and
|
||
the account lists are left as they are;
|
||
5. **created** under the module's own keys when no account has it: its keys are added at the end of
|
||
`mail.accountmanager.accounts` and `mail.smtpservers` in `prefs.js`, every other key kept in its place,
|
||
only while Betterbird is not running.
|
||
|
||
**Refused, for that account alone and with the remedy** (§7): two accounts with the address (remove the
|
||
duplicate in Betterbird, or name the `key`), Owl's account at the address, a named key that does not
|
||
qualify, an account of another kind than declared (never converted), an identity that names no outgoing
|
||
server, and an outgoing server that another identity, neither adopted nor the module's, also uses
|
||
(Betterbird lets identities share one, and the declared values would change that other account's sending;
|
||
ADR 0317 §2 leaves the identity's server and the lists as they are, so the module refuses rather than give
|
||
it a server of its own). A recorded adoption follows the identity's current outgoing server at every apply,
|
||
under the same rule. The other accounts go ahead; the start and `betterbird_check` say it.
|
||
`betterbird_user_js` answers an account's preferences by name with their values withheld.
|
||
|
||
The declared fields are written in the user.js under the account's keys, so they win at every start; only
|
||
the fields the setting names, never the account's directory, folders, filters, caches, mail or kept
|
||
sign-in, so an adopted account keeps its sign-in (§5). The apply names, by preference name only, each one
|
||
whose value it changed (§4). What was adopted or created is recorded in
|
||
`~/.config/betterbird/accounts-record.json`. **Leaving the setting** releases an adopted account (its
|
||
lines and its record go, the account stays the person's) and takes a created account's keys out of the
|
||
lists (its mail stays in the profile).
|
||
|
||
**The value is derived, not retyped** (§9): `betterbird_accounts` with `propose` builds `accounts` from the
|
||
profile (every account but Owl's, no credential) and proposes it in the mesh-wide layer as it stands, through
|
||
the controller's settings verb; the operator approves on a channel that proves who answers. Its answer shows
|
||
the value it proposed (ADR 0329 §10).
|
||
|
||
**A node with no profile yet.** The preparation needs the profile Betterbird makes at its first start: on a
|
||
node set up from nothing, the declared accounts are written at the second start from the launcher (or at
|
||
`betterbird_restart`).
|
||
|
||
## Identities, calendars, address books, filters and tags (ADR 0329)
|
||
|
||
Five more settings, mesh-wide with a node's layer over each (an entry set to `null` in a node's layer is not on
|
||
that node). `identities`, `calendars`, `address-books` and `filters` reach the machine as
|
||
`~/.config/betterbird/configuration.json`, trusted as `accounts` is; `tags` as `tags.json`, not trusted (§5).
|
||
**No value holds a password, a token or a cookie**: a key shaped like one is refused, and so is an address whose
|
||
user part carries a password (§2). A setting holds no line break, so a signature is a list of its lines.
|
||
|
||
| setting | an entry | matched in the profile by |
|
||
|---|---|---|
|
||
| `identities` | by a name: `account` (its name in `accounts`), and for another identity than the account's default `address` and `name`; `reply-to`, `organization`, `signature` (lines), `signature-html`, `signature-above-quote`; `key` in a node's layer | the account's default (no `address`), or an identity of that account with the address |
|
||
| `calendars` | by a name: `name`, `url` (CalDAV), `color`, `read-only`, `reminders`, `user`, `invitations` (an identities entry); `key` in a node's layer | its address |
|
||
| `address-books` | by a name: `name`, `url` (CardDAV), `user`; `key` in a node's layer | its address |
|
||
| `tags` | by its key: `name`, `color`, `order` | its key |
|
||
| `filters` | by an account's name: the ordered list, each with `name`, `enabled`, `when` (new-mail, manual, after-junk, sending, archiving, periodic), `match` (all, any, every-message), `conditions` (`field`, `op`, `value`, in Betterbird's words) and `actions` (`action` in Betterbird's words; `folder` as a path within the account, or `value`) | owned whole per account |
|
||
|
||
**Adopted as accounts are** (§7): an entry recorded on this node is kept by its key; one under the module's own
|
||
key (`ident_mesh_<name>`, `mesh_<name>` for a calendar or an address book) is its creation; a `key` in the
|
||
node's layer is adopted when the profile holds it and it matches; else one match is adopted, none is created
|
||
under the module's key, and several are refused with the remedy. The declared fields go into the user.js under
|
||
that key. An entry leaving the setting releases what was adopted (its lines go) and removes the preferences of
|
||
what was created, never a calendar's events, an address book's contacts or a cache. An entry no setting declares
|
||
stays the person's. Owl's accounts and calendars, an ICS calendar (its address is often the secret), local
|
||
calendars and address books are never declared (§13).
|
||
|
||
**Filters are owned whole per declared account** (§8): the module writes that account's `msgFilterRules.dat`
|
||
from the list, each folder's path turned into its address on this node, so a filter added in Betterbird is gone
|
||
at the next apply (drift shows it first). Before its first write it keeps the replaced file in
|
||
`~/.config/betterbird/kept/<profile>/`; when the account leaves `filters`, the module stops writing and the file
|
||
stays the person's; `betterbird_filters` says where the kept copy is. A filter a setting cannot hold as it is (a
|
||
folder of another account, an add-on's action, a news filter) is named and not derived.
|
||
|
||
**Written only while Betterbird is closed** (§9): the lists in `prefs.js` and the filter files, because
|
||
Betterbird rewrites them as it runs and quits; the user.js is placed as before. `betterbird_check` counts per
|
||
kind the changes that wait.
|
||
|
||
**Derived, then proposed one layer at a time** (§10): `betterbird_derive` asks every desktop node holding
|
||
Betterbird for what its profile holds. An entry on every such node goes into the mesh-wide layer with the value of
|
||
the node the derive was called on, and the answer lists each field another node holds differently (which then
|
||
shows as drift there); one on some nodes only goes into those nodes' own layers. An account whose filter file
|
||
holds no filter is not put in `filters`. With `propose`, the next layer (the mesh-wide one, then each node's) is
|
||
proposed with the layer as it stands beside the derived kinds; never while a proposal of the module's waits, and
|
||
never while a desktop node does not answer. `prefs` on the call carries values of the setting `prefs` into the
|
||
mesh-wide layer. A `key` is never proposed for the mesh-wide layer.
|
||
|
||
## Tools
|
||
|
||
They are served by the node's runtime as the operator account (ADR 0175). **They show the operator's own
|
||
configuration with its values, and never a credential** (ADR 0329 §1, §2): every answer passes the module's
|
||
credential check, which withholds a field whose name says password, token, secret or cookie and the password
|
||
part of any address. No answer carries mail, a calendar entry or a contact.
|
||
|
||
| tool | does |
|
||
|---|---|
|
||
| `betterbird_status` (r) | whether it runs (pid, since, scope, children); what is installed and from where, its version beside the newest published build; the policy; Owl's version against the pin and whether it is active; where its output goes; the profile and how it was found; the user.js (declared, placed, in effect, with every declared entry as it resolved); the declared accounts by count; the add-ons; the preferences that decide how its windows and notifications look |
|
||
| `betterbird_prefs` (r) | named preferences or prefixes: value and source, and the default with the file that gives it. Withheld without its value: a name that can hold a credential, one that names an account, server, calendar or person, and every `extensions.*` but the theme's id. Values masked (the home as `~`, addresses and hosts) |
|
||
| `betterbird_config` (r; a with `keep`) | each managed preference: its lock, the declared value, Betterbird's default where the module keeps that, the profile's saved value, the value in effect, and drift; with `keep`, this node's layer of `prefs` proposed with the profile's values |
|
||
| `betterbird_config_apply` (r; a with `apply`) | a kept preference's drifting values listed; with `apply`, while Betterbird is quit, taken out of `prefs.js` |
|
||
| `betterbird_user_js` (r; a with `apply`) | the user.js composed from the settings against the profile's; `apply` places it, writes what Betterbird rewrites while it is closed, and counts what waits |
|
||
| `betterbird_log` (r) | the last lines of the journal of `mesh-betterbird` (or of the reminder test), addresses and credentials masked |
|
||
| `betterbird_restart` (a) | ends Betterbird (asked to quit, forced after 8 s), places the user.js and writes what waited, starts it as `mesh-betterbird`. **Refused while a composer window is open**, unless `discard_unsent` is true, which only the operator's yes justifies |
|
||
| `betterbird_check` (r) | installed from the mesh's release; the policy and the module's defaults placed; no managed preference drifting; no ask to be the default mail client; Owl at its pin and active; one running; profile found; the user.js valid, placed, in effect; the declared accounts in the list, none refused, each signed in once; per kind, the declared entries drifting or refused and the changes waiting for Betterbird to close; its output read |
|
||
| `betterbird_accounts` (r; a with `propose`) | the profile's accounts with their values and the declared account each is, Owl's by name and address and marked; with `propose`, `accounts` built from them and proposed in the mesh-wide layer as it stands |
|
||
| `betterbird_identities`, `betterbird_calendars`, `betterbird_address_books`, `betterbird_filters`, `betterbird_tags` (r) | the profile's entries with their keys and values beside the declared ones and how each resolved (adopted, created or refused with why); an ICS calendar by its name only, Owl's marked; `betterbird_filters` also where the kept filter files are |
|
||
| `betterbird_derive` (r; a with `propose`) | the five kinds derived from the desktop nodes' profiles, per layer, with the fields another node holds differently and what was left out; with `propose`, the next layer proposed; `here` answers this node's profile, as another node asks it |
|
||
| `betterbird_drift` (r) | per kind and declared entry: holds, differs (each field with both values), lacks, or refused; and the profile's entries nothing declares |
|
||
| `betterbird_reminder_test` (a) | a reminder on demand without the operator's calendar (below) |
|
||
|
||
### A reminder on demand
|
||
|
||
**It opens windows on the operator's screen**, and its description says so first, so a caller tells the
|
||
operator before calling it. On 2026-10-12 at 12:05 a test Betterbird with an empty profile carrying the
|
||
operator's theme looked like the operator's configuration lost; so the test is **visibly a test**: its
|
||
profile carries a `userChrome.css` that frames every window of the test Betterbird in red and runs a red
|
||
band reading MESH TEST across its top, and its event is called "MESH TEST reminder (a throw-away
|
||
Betterbird of the mesh, not your calendar)". The reminder window itself is left unstyled, because how
|
||
Betterbird draws it is what the test looks at; its title is the event's. A window title cannot be set by a
|
||
preference, so the marking is the band, the frame and the event's name.
|
||
|
||
`betterbird_reminder_test` starts a **second** Betterbird with `--no-remote` in a throw-away profile
|
||
under `/run/user/<uid>/mesh-betterbird-reminder-test/`, as the user unit
|
||
`mesh-betterbird-reminder-test` (ended after 10 minutes at most). The profile holds the module's
|
||
user.js (the setting `prefs`; never a declared account) and one calendar: a local `.ics` file with one
|
||
event, "MESH TEST reminder", whose reminder is
|
||
due `after` seconds from the call (default 40). `carry` copies named preferences of the operator's
|
||
profile into it (a theme, a colour switch), under the same limits as `betterbird_prefs`. The test's own
|
||
preferences come last in its user.js, so nothing declared or carried overrides them; one of them turns
|
||
Betterbird's update check off (`betterbird.update.checkIntervalDays` 0), because a fresh profile checks
|
||
at its start and, when a newer build is published, opens the About window on the operator's screen
|
||
(seen 2026-10-12). Then:
|
||
|
||
- `xorg_windows` with class `Calendar` answers the reminder window: role `AlarmWindow`, type `Dialog`,
|
||
its depth (32: a visual with alpha, which the compositor blends), its map state;
|
||
- `xorg_screenshot` with that window shows it as the screen shows it.
|
||
|
||
The unit removes its profile when it ends, however it ends (`ExecStopPost`): at its 10 minutes, on
|
||
`stop`, or when the test window is closed. The test Betterbird is never counted as the operator's: its
|
||
command line names its own profile, and `status`, `check`, `restart` and the user.js leave it out.
|
||
|
||
The running Betterbird, its profile and its calendars are not touched: `--no-remote` keeps the two
|
||
apart. **`profiles.ini` and `installs.ini` are not rewritten either**, measured on the workstation
|
||
(2026-10-09): their content and modification time (June 2023) were the same before a `--profile` start,
|
||
while it ran, and after it ended.
|
||
|
||
## The reminder window, as measured (2026-10-09, workstation, i3 and picom with glx)
|
||
|
||
| | |
|
||
|---|---|
|
||
| X class / instance | `eu.betterbird.Betterbird` / `Calendar` |
|
||
| role, type | `AlarmWindow`, `_NET_WM_WINDOW_TYPE_DIALOG` (i3 floats it) |
|
||
| depth | 32 (ARGB visual), like every Betterbird window there |
|
||
| size | 600×300, kept in the profile's `xulstore.json` |
|
||
|
||
A window of depth 32 that its program has not drawn is transparent under a compositor, not black:
|
||
that is why the symptom looks the way it does. Why Betterbird did not draw it is hq issue 345's
|
||
question.
|
||
|
||
## What changes when it is pushed
|
||
|
||
The module is assigned to shanks today (look-only, from issue 345's first part); g14 runs Betterbird
|
||
outside the mesh. With this version, a push to a node (each waits for the operator: `upgrade: record`):
|
||
|
||
| | on a node with the community package (g14, shanks) |
|
||
|---|---|
|
||
| the package | `betterbird-bin` removed (quit Betterbird first) |
|
||
| `/opt/betterbird` | the mesh's 153.4.0esr-bb10, in the same place, so the same profile opens |
|
||
| the policy, Owl | placed; Owl installed into the profile by Betterbird at its next start where it is missing |
|
||
| `/usr/local` | the start, the launcher entry, the tools' program |
|
||
| `~/.config/betterbird/` | `prefs.json`, `accounts.json`, `configuration.json`, `tags.json` (each empty until its setting is approved) |
|
||
| the profile | untouched until the next start from the launcher, `betterbird_user_js` with apply, or `betterbird_restart` |
|
||
| the configuration | the policy's locks at Betterbird's next start; the settings in the user.js from the next start. Pushed in the order above, the operator's 21 preferences are in `prefs` before the build arrives; pushed before that layer is approved, Betterbird runs with its own defaults for them until it is |
|
||
|
||
## Relies on
|
||
|
||
- A display server on the same machine (`x11-display`, ADR 0208 §3).
|
||
- **`xorg`** for `xorg_windows` and `xorg_screenshot`, which look at Betterbird's windows. This module
|
||
carries no window code of its own.
|
||
- `systemd-run` and `journalctl` of the account's user manager.
|
||
- The build seat's archive pinned by URL and sha256 (mesh-controller, ADR 0316 §2): a controller that does
|
||
not know it refuses this manifest.
|
||
- The controller's `settings` verb, for every proposal and to read a layer as it stands; its `nodes` and `node`
|
||
verbs, for which machines hold the module; and the module's own `betterbird_derive` on those machines
|
||
(`invokes`).
|