feat: give every room the broker, one scale, and its own panel back

Little Waders took the Plasma build and reported what a container
cannot. A disc player on a machine with no optical drive, a clock nobody
at the back of the room could read, a browser at half the size of the
panel around it, and Parent Call with no broker to call.

The broker is the same for every classroom, so it is a default now and
not something to fill in room by room. classroom-migrate brings an
existing room to the same place without touching its name, its username
or its password, and Parent Call distinguishes a broker it cannot reach
from one that was never set up: those send somebody to look at
completely different things.

The disc player's detection was right and ran too late — alongside the
session rather than before it, and the panel is built from the layout the
moment a session starts. It runs before the display manager now, and
again from udev when a drive is plugged in.

UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a
clock pinned at 15 points, which is the same size at every scale; and a
browser that was never told the number at all. All three are fixed and
the room doubles together at 200%.

This also brings 0.1.47's work into the repository, where it had only
ever existed in a build directory under /tmp: the hidden display dialog,
its authenticated writer, the polkit action, and the church's own
wallpaper. One piece of it is deliberately left out. The custom volume
display needed a shell package of our own, and with one selected
plasmashell builds no panel at all — measured twice, with that build's
own files, on a classroom that deletes its panel configuration at every
login by design.

The lab connected to the real church broker the moment the package
defaulted to it. Nothing was left behind, which was checked, but the
guard that should have stopped it had never worked: it used iptables,
which the lab image does not have, and probed with /dev/tcp under dash,
where that always fails. Both reported success.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-12 19:05:09 -05:00
co-authored by Claude Opus 5
parent 926668dc9b
commit 34fd02e08f
59 changed files with 1335 additions and 198 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/bin/bash
set -uo pipefail
# What an upgrade does to a room's own settings file.
#
# device.conf is the one file in the classroom that belongs to the church
# rather than to the package: the room's name, and the broker password.
# An upgrade edits it, so an upgrade can also ruin it. These are the cases
# that matter, run against the real code — the migration's own Python is
# lifted out of the script rather than copied here, so a change to one
# cannot pass by disagreeing with the other.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT" <<'PY'
import re
import subprocess
import sys
import tempfile
from pathlib import Path
root = Path(sys.argv[1])
script = (root / "scripts/classroom-migrate").read_text()
block = re.search(r'python3 - "\$DEVICE_CONFIG" <<\'PY\'\n(.*?)\nPY\n',
script, re.S)
passed = failed = 0
def check(label, condition):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
failed += 1
check("the migration's device.conf code was found", block is not None)
if block is None:
sys.exit(1)
migration = block.group(1)
def migrate(text):
"""Run the real migration over a device.conf and return it."""
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "migrate.py"
source.write_text(migration)
config = Path(directory) / "device.conf"
config.write_text(text)
result = subprocess.run(
[sys.executable, str(source), str(config)],
capture_output=True, text=True, check=False,
)
return config.read_text(), result.stdout + result.stderr
print("1. a room from the Hyprland experiment")
BEFORE = """# Ekklesia Classroom device configuration
UPDATE_URL=https://updates.riverofglorymn.org
CHANNEL=hyprland
LIVE_ENABLED=false
AUDIO_OUTPUT=auto
UI_SCALE=2.0
ROOM=little-waders
MQTT_ENABLED=false
MQTT_HOST=
MQTT_PORT=1883
MQTT_USERNAME=classroom
MQTT_PASSWORD=hunter2-not-a-real-one
COMPOSITOR=hyprland
HYPR_APPEARANCE=visual
"""
after, output = migrate(BEFORE)
check("the broker is switched on", "MQTT_ENABLED=true" in after)
check("and given the church address", "MQTT_HOST=10.40.0.2" in after)
check("the port is left as it was", "MQTT_PORT=1883" in after)
check("the room keeps its name", "ROOM=little-waders" in after)
check("the username is untouched", "MQTT_USERNAME=classroom" in after)
check("the password is untouched",
"MQTT_PASSWORD=hunter2-not-a-real-one" in after)
check("and is never printed", "hunter2" not in output)
check("the compositor setting is gone", "COMPOSITOR" not in after)
check("and the appearance setting with it", "HYPR_APPEARANCE" not in after)
check("the channel moves off the experiment",
"CHANNEL=classroom-v2" in after)
check("the room's scale is preserved", "UI_SCALE=2.0" in after)
check("and everything else is left alone",
"UPDATE_URL=https://updates.riverofglorymn.org" in after
and "AUDIO_OUTPUT=auto" in after)
print()
print("2. a room already pointed at a broker of its own")
OWN = """CHANNEL=a-channel-somebody-named
MQTT_ENABLED=true
MQTT_HOST=broker.example.invalid
MQTT_PORT=8883
MQTT_PASSWORD=leave-me
"""
after, output = migrate(OWN)
check("a deliberate address is not overwritten",
"MQTT_HOST=broker.example.invalid" in after)
check("nor a deliberate port", "MQTT_PORT=8883" in after)
check("a channel nobody here named is left alone",
"CHANNEL=a-channel-somebody-named" in after)
check("and its password is still there", "MQTT_PASSWORD=leave-me" in after)
print()
print("3. a room old enough to have no broker settings at all")
OLD = """CHANNEL=canary
UI_SCALE=1.0
"""
after, output = migrate(OLD)
check("the settings are added", "MQTT_ENABLED=true" in after
and "MQTT_HOST=10.40.0.2" in after and "MQTT_PORT=1883" in after)
check("no credentials are invented",
"MQTT_USERNAME" not in after and "MQTT_PASSWORD" not in after)
# stable and canary are both the sway line. A machine that has just taken
# the Plasma package and is left pointed at one of them is handed the old
# desktop back by the next release there: the upgrade undoing itself.
check("canary moves onto the v2 channel", "CHANNEL=classroom-v2" in after)
after, _ = migrate("CHANNEL=stable\nUI_SCALE=1.0\n")
check("and so does stable", "CHANNEL=classroom-v2" in after)
print()
print("4. running it twice changes nothing the second time")
once, _ = migrate(BEFORE)
twice, _ = migrate(once)
check("the second run is a no-op", once == twice)
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY