A real room upgraded to 0.1.45 and came up as stock Debian Plasma with the old Waybar across the top, mako still catching notifications, and a lock screen on an account that has no password. The cause was one line, and it was in the lab: the harness handed the session under test XDG_CONFIG_DIRS by hand, so every result it had ever produced was a result about a machine somebody had already configured. On a machine SDDM starts the session through PAM, PAM does not read /etc/environment.d, and Debian's own plasma-workspace env.sh then set the variable from scratch — so the classroom's settings directory was not in the cascade at all. No layout, no lock policy, no Kiosk restrictions. The settings now arrive the way Plasma delivers them, through a file in /etc/xdg/plasma-workspace/env. Around that: the look and feel is immutable on its own key so a stale profile cannot answer for it; the palette is composed into the package, because naming a colour scheme does not produce one and the dark bar had been coming from a technician's hand; the desktop reset converges another desktop's appearance state and keeps the room's own; and classroom-migrate stops, deactivates and masks the old shell, including the D-Bus activation that brought mako back while the panel was down — found only by killing the panel on a migrated machine. device.conf loses COMPOSITOR and comes off the hyprland channel, which would otherwise have handed the room the experiment back. The lab stops flattering the package: it restarts the machine, lets SDDM actually autologin, and asks a fresh install and a machine upgraded from the Hyprland package the same sixty questions. Both answer identically, twice, and the panel still comes back from eight SIGKILLs. Co-Authored-By: Claude Opus 5 <[email protected]>
1289 lines
62 KiB
Markdown
1289 lines
62 KiB
Markdown
# Classroom v2 architecture experiment
|
||
|
||
An open re-evaluation of the whole Ekklesia Classroom desktop stack, with
|
||
reliability as the first requirement. Nothing here reaches a stable, canary
|
||
or hyprland room, and nothing here changes the development host.
|
||
|
||
## Base
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| branch | `experiment/classroom-v2` |
|
||
| base commit | `aadb1b881977b0eb25e1ca10e060977bed31a48f` |
|
||
| base subject | *fix: keep the system print dialog in front of the window that rendered it* |
|
||
| `VERSION` at base | `0.1.44` |
|
||
|
||
That commit is the published **stable** build, verified rather than assumed:
|
||
`repo/stable/manifest.json` names `ekklesia-classroom_0.1.44_amd64.deb` with
|
||
sha256 `7de568…81041`, and the copy in `repo/packages/` hashes to exactly
|
||
that. The branch is not built on `experiment/hyprland`; `git ls-files` on it
|
||
contains no Hyprland file.
|
||
|
||
## The answer, in one paragraph
|
||
|
||
Every user-visible failure the church reported is a piece of desktop
|
||
infrastructure the classroom wrote itself. The shell that disappeared had no
|
||
supervisor. The minimised window with no feedback exists because wlroots has
|
||
no minimise and the classroom invented one. The secondary window that fights
|
||
the browser needed a 314-line arbiter because sway tiles. The unrecognisable
|
||
browser icon was hand-drawn to match a monochrome set. Debian 13 already
|
||
ships a desktop that solves all four, so **the recommendation is Plasma 6.3
|
||
on the Debian the classroom already runs**, configured rather than written,
|
||
and it was built and tested here rather than argued for.
|
||
|
||
## The development host, as found
|
||
|
||
Recorded because it bounds what could be tested.
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| OS | Debian 13 trixie (13.5), kernel 6.12.90 |
|
||
| machine | already a VM — root is `/dev/vda2`, GPU is `bochs-drm`, CPU is `QEMU Virtual CPU` |
|
||
| nested virtualisation | **none** — `/dev/kvm` does not exist |
|
||
| libvirt / qemu / VirtualBox | not installed |
|
||
| containers | Docker 26.1.5, cgroup v2 |
|
||
| resources | 6 vCPU, 11 GiB RAM, 160 GiB free |
|
||
|
||
**This session could not create a virtual machine**, so the candidate desktop
|
||
was given a systemd container instead. Nothing outside Docker was changed on
|
||
the host: no packages installed, no configuration touched, no service
|
||
altered. See *Alternate operating systems* for what still needs a real VM.
|
||
|
||
## The lab
|
||
|
||
`tools/lab/` builds a Debian 13 container with a full Plasma 6.3 desktop in
|
||
it, runs systemd as PID 1 so the session is supervised the way a real machine
|
||
supervises it, and nests the session inside a headless sway so that `grim`
|
||
can photograph it and a real pointer can be aimed at it.
|
||
|
||
To resume: the lab is left running, built from the package, with Parent
|
||
Call's test rig **not** started — so the classroom's broker settings are
|
||
off and nothing can reach anything. `tools/lab/parent-call-rig up` brings
|
||
the coordinator back, and it re-establishes and re-verifies the block on
|
||
the church's networks before it will start.
|
||
|
||
```
|
||
tools/lab/lab build # the image
|
||
tools/lab/from-package # a fresh machine, the .deb, and nothing else
|
||
tools/lab/cold-start 3 # three complete cold starts, from nothing
|
||
tools/lab/lab session # compositor + Plasma session
|
||
tools/lab/lab shot NAME # docs/screenshots/NAME.png
|
||
tools/lab/lab click X Y # a real pointer press
|
||
tools/lab/kwin-script FILE # ask KWin what it believes about a window
|
||
tools/lab/recovery-check # every recovery property, measured
|
||
tools/lab/parent-call-rig # the real coordinator, safely walled off
|
||
```
|
||
|
||
Every window measurement in this document came from `kwin-script`, not from
|
||
looking at a screenshot. Every click came through libinput.
|
||
|
||
Three lab-only deviations, recorded because they bound what was proven:
|
||
|
||
* **Software rendering.** llvmpipe throughout, with KWin nested in sway. This
|
||
proves behaviour and layout, not performance on classroom hardware.
|
||
* **No sound card and no optical drive.** The audio policy was therefore
|
||
tested as a pure function against the real room's reported hardware state
|
||
rather than end to end, and DVD playback was not exercised.
|
||
* **Video did not decode.** VLC came up fullscreen at exactly 1920×1080 with
|
||
the panel covered, which is the window-management result being tested, but
|
||
the picture was black under software XWayland.
|
||
|
||
## What was measured
|
||
|
||
Each of these changed a decision.
|
||
|
||
### The panel gives up after three crashes
|
||
|
||
Plasma's own unit restarts a crashed `plasmashell`, but with
|
||
`StartLimitBurst=3` inside sixty seconds. Measured:
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| kill 1 | back in 0.3 s |
|
||
| kill 2 | back in 0.4 s |
|
||
| kill 3 | **never came back** |
|
||
|
||
That is the production failure the church already had, reproduced on stock
|
||
Plasma. A four-line systemd drop-in
|
||
(`systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf`) makes
|
||
the restart unconditional. Re-measured: **eight consecutive `SIGKILL`s one
|
||
second apart, back every time in about 2.4 s**, with the open application
|
||
window untouched.
|
||
|
||
This is the single most important line in the whole experiment, and it is a
|
||
supported override rather than a supervisor of our own.
|
||
|
||
### KWin already recovers, and takes nothing with it
|
||
|
||
`kill -9 kwin_wayland` twice: back in **0.1 s** each time, via
|
||
`kwin_wayland_wrapper`, with the running application still on screen and its
|
||
Wayland connection intact.
|
||
|
||
### A deleted panel rebuilds itself
|
||
|
||
`plasma-org.kde.plasma.desktop-appletsrc` was deleted and the shell
|
||
restarted. The classroom bar came back exactly as designed, from the layout
|
||
script in the look-and-feel package. So the recovery procedure for a damaged
|
||
desktop is one file removed and one service restarted — no reflash, and no
|
||
technician in the room.
|
||
|
||
### Plasma cannot show all five application states
|
||
|
||
The brief asks for closed, running, active, minimised and behind. Measured by
|
||
sampling the pixels of the indicator strip rather than by eye:
|
||
|
||
| state | indicator | icon |
|
||
| --- | --- | --- |
|
||
| closed | none — panel background `(26, 29, 32)` | full colour |
|
||
| running, behind | grey `(42, 45, 48)` | full colour |
|
||
| running, active | teal `(65, 127, 121)` | full colour |
|
||
| **minimised** | grey `(42, 45, 48)` — **identical to behind** | full colour |
|
||
|
||
Three marks for four states. Rather than write a task manager that draws a
|
||
fourth — which is exactly the kind of custom desktop code this experiment
|
||
exists to remove — **the classroom removes the state**: clicking a launcher
|
||
always means *show me this*, never *hide it*, and the window decoration has
|
||
no minimise button. There is nothing on the idle desktop a teacher could want
|
||
to see, so minimising in this room only ever loses the lesson.
|
||
|
||
A window minimised by some other route is still shown as running and comes
|
||
back with one click, so it can never be lost even then.
|
||
|
||
### A window rule cannot tell a browser from its popup
|
||
|
||
The first attempt maximised the browser with a KWin rule on `google-chrome`.
|
||
Measured: the window a lesson page opened was maximised too, because Chrome
|
||
gives both the same class — the very behaviour the current architecture
|
||
spends 314 lines preventing.
|
||
|
||
Chrome can tell them apart, so `--start-maximized` in the launcher does it.
|
||
Measured afterwards:
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| main browser | `[0,0] 1920×1004` — the whole desktop above the bar |
|
||
| window the page opened | `[509,160] 902×683` — centred, the size the page asked for |
|
||
| main browser afterwards | unchanged |
|
||
|
||
The secondary window carries an ordinary title bar with a close button, so
|
||
the dock's sticky Close button has nothing left to do.
|
||
|
||
### Printing
|
||
|
||
A page's own Print button produced a **native KDE print dialog** through
|
||
`xdg-desktop-portal-kde`, `transient=true` on the browser, centred over it,
|
||
in the classroom's colours, offering only **Elmer**. Stacking is the window
|
||
manager's ordinary behaviour and needed no rule.
|
||
|
||
### An immutable colour scheme cannot be toggled
|
||
|
||
Marking `[General]` immutable in `/etc/xdg/kdeglobals` froze the palette so
|
||
hard that `classroom-theme light` changed the running session and lost the
|
||
change at the next start. Immutability now covers the Kiosk restrictions and
|
||
nothing else. Measured both ways afterwards.
|
||
|
||
### Two packages cannot own one file
|
||
|
||
`dpkg -i` refused the first package outright: `plasma-workspace` already
|
||
owns `/etc/xdg/plasmanotifyrc`, and `kdeglobals`, `kwinrc` and the rest are
|
||
the same story. The room's settings live in
|
||
`/etc/ekklesia-classroom/xdg/` instead, with
|
||
`/etc/environment.d/90-ekklesia-classroom.conf` putting that directory in
|
||
front of `/etc/xdg` in `XDG_CONFIG_DIRS`. That is KConfig's own cascade
|
||
rather than a fight over a path, and it was checked at runtime:
|
||
`systemctl --user show-environment` carries it, and `kreadconfig6` in the
|
||
session reads the classroom's values.
|
||
|
||
The `postinst` then failed on a `chmod` for a command that had been
|
||
removed. It walks what the package actually installed now.
|
||
|
||
### VLC calls itself `Vlc`
|
||
|
||
KWin's window matching is case sensitive, and a lower-case substring rule
|
||
matched nothing while reporting nothing. The rule is a regular expression
|
||
now.
|
||
|
||
## The architecture
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| distribution | **Debian 13 trixie** — unchanged, so the `.deb` and OTA work survives |
|
||
| desktop | **KDE Plasma 6.3.6** (Debian's own packages) |
|
||
| compositor | **KWin 6.3.6**, Wayland |
|
||
| panel | **Plasma panel**, one full-width bar, built by a look-and-feel layout script |
|
||
| task feedback | **Icons-only Task Manager** |
|
||
| notifications | **Plasma's own** |
|
||
| browser | **Google Chrome**, unchanged |
|
||
| Wonder Ink | Chrome app mode, **its own profile** |
|
||
| session start | **SDDM autologin** (recommended; not yet tested — see risks) |
|
||
| lockdown | **KDE Kiosk** action restrictions |
|
||
| audio | one small classroom service, event driven |
|
||
|
||
### The bar
|
||
|
||
One continuous full-width bar, 76 px, the height the room already has.
|
||
|
||
* **Left** — Parent Call, drawn as a line mark in the accent rather than a
|
||
colour tile, so it reads as a different kind of thing from the applications
|
||
and is visibly not one of them.
|
||
* **Centre** — the applications: Wonder Ink, and the browser wearing
|
||
**Chrome's own icon, in colour**. That is the direct answer to the teacher
|
||
who asked whether the old monochrome glyph meant "the Internet".
|
||
* **Right** — network, volume, removable media, notifications, clock. Four
|
||
applets placed directly on the bar rather than a system tray: the tray
|
||
discovers whatever is installed and hides most of it behind an arrow, and
|
||
in the lab it collected battery, brightness, clipboard, keyboard layout,
|
||
camera, vaults and screen management on a machine with none of those.
|
||
|
||
The bar launches applications, shows their state, and shows a little status.
|
||
It is not the Live View, not Parent Call, and not a shell framework.
|
||
|
||
### Wonder Ink as an application
|
||
|
||
```
|
||
Wonder Ink chrome --app=https://app.wonderink.org/ --user-data-dir=…/wonderink
|
||
Browser chrome the room's own profile
|
||
```
|
||
|
||
Its window class is `chrome-app.wonderink.org__-Default` — measured on a
|
||
running session, and the launcher's `StartupWMClass` matches it exactly, so
|
||
the panel shows one button rather than a launcher and a window side by side.
|
||
|
||
The separate profile buys something the current single-profile design cannot:
|
||
if a teacher signs into Wonder Ink through Google, that Google session is in
|
||
the Wonder Ink profile and closing the window ends it, while the church
|
||
YouTube sign-in lives in the browser's profile and is untouched. The
|
||
documented conflict in `browser.md` — that clearing `wonderink.org` leaves
|
||
the next teacher one click from the previous teacher's account — does not
|
||
arise.
|
||
|
||
The ordinary browser therefore no longer opens Wonder Ink. Its home page is
|
||
YouTube, because a second way in through that window would put a teacher's
|
||
lesson session in the room's shared profile, which is the leak the split
|
||
exists to prevent.
|
||
|
||
### Windows
|
||
|
||
KWin's ordinary behaviour, with one rule.
|
||
|
||
* Main windows fill the desktop above the bar, edge to edge, because Chrome
|
||
is told to start maximized.
|
||
* Windows a page opens float over the main one, centred, at the size the page
|
||
asked for, with a title bar and a close button.
|
||
* Dialogs are transient on their parent and stack correctly with no help.
|
||
* Fullscreen video covers the whole display: `[0,0] 1920×1080`, panel
|
||
included, no rounding, no wallpaper.
|
||
* One rule, and it is `noborder` for VLC.
|
||
|
||
**The outer margin and the 10 px rounding were dropped.** Chrome squares its
|
||
own corners when maximised, so margin and rounding are the same decision, and
|
||
expressing "the screen minus 16 px" needs a rule generated per resolution at
|
||
every boot. That is glue in exchange for appearance, and the brief says
|
||
reliability wins. Full-bleed above the bar is also what every other appliance
|
||
does.
|
||
|
||
### Theme
|
||
|
||
One set of design tokens in `EkklesiaDark.colors`; the light scheme is
|
||
**generated** from it by `tools/build-light-scheme`, so a colour can only be
|
||
changed in one place. Plasma applies either to the panel, the notifications,
|
||
the decorations and every dialog at once. `classroom-theme light|dark|toggle`
|
||
is the whole switch. Accent teal `#4FB3A8`, taken from the wallpaper.
|
||
|
||
### Wallpaper
|
||
|
||
`config/wallpapers/classroom-calm.png` is a **stand-in**, generated by
|
||
`tools/build-wallpaper` in the sage/teal/blue/lavender palette with a river
|
||
leading into the picture and a quiet upper middle. The illustrated wallpaper
|
||
the church selected is not in this repository; when it arrives it replaces
|
||
that file and nothing else changes.
|
||
|
||
### Lockdown
|
||
|
||
`/etc/xdg/kdeglobals`, `[KDE Action Restrictions][$i]`. Verified by clicking,
|
||
not by reading the file: right-clicking the wallpaper produced no menu,
|
||
right-clicking the bar produced no menu, and KRunner was not running and
|
||
could not be summoned. `run_command`, `shell_access`, `logout`,
|
||
`action/lock_screen`, `action/kwin_rmb` and `plasma/plasmashell/unlockedDesktop`
|
||
all read back `false`. There is no lock screen and no session restore.
|
||
|
||
Nothing was achieved by deleting a binary. Lifting a restriction for
|
||
maintenance is one line and a session restart.
|
||
|
||
### Audio
|
||
|
||
The one place custom code is still right, because no desktop expresses this
|
||
policy. `lib/classroom_audio.py` is a pure function so it can be tested
|
||
against the room that failed rather than against a machine:
|
||
|
||
* a port that reports itself unavailable is not a candidate;
|
||
* an analog port existing is not evidence that a cable is in it;
|
||
* an explicit `AUDIO_OUTPUT=hdmi|analog` overrides everything.
|
||
|
||
`scripts/classroom-audio` follows PipeWire's own event stream — no polling —
|
||
and on every change sets the card profile, sets the default sink, and
|
||
**moves streams that are already playing**. That last step is the fix for the
|
||
teacher who had to restart the browser after changing the output.
|
||
|
||
Twelve test cases in `tests/test-classroom-audio.sh`, the real room among
|
||
them. The service is supervised the same way the panel is; measured, it came
|
||
back from four consecutive kills.
|
||
|
||
### DVD
|
||
|
||
**How much of the DVD player survives this change was measured, not
|
||
assumed**, because the room's disc playback works and breaking it for
|
||
tidiness would be a bad trade.
|
||
|
||
* Its loading cover and control bar are `gtk-layer-shell` surfaces. KWin
|
||
implements `zwlr_layer_shell_v1` — the Plasma panel is one — and a probe
|
||
overlay came up full-screen under it. So the DVD surfaces keep working
|
||
and `python3-gi` and `gir1.2-gtklayershell-0.1` stay in the package.
|
||
Removing them broke `classroom-dvd toggle` immediately, which is how this
|
||
was found.
|
||
* Its stylesheet was the dock's. The dock is gone; the template survives as
|
||
`config/overlay/style.css.in`, renamed to say what still uses it.
|
||
* `classroom-dvd` calls `classroom-window`, which speaks sway's IPC.
|
||
Measured with no sway running: it exits 0 and does nothing, so it
|
||
degrades rather than hanging — but the DVD player's focus handling is not
|
||
yet ported, and that is the largest piece of work this change leaves.
|
||
* One behavioural difference: under sway the loading cover hid the dock;
|
||
under KWin the panel draws above it, so the bar stays visible while a
|
||
disc spins up. Fullscreen VLC still covers everything, which is the state
|
||
that matters.
|
||
|
||
`/usr/share/solid/actions/ekklesia-dvd.desktop` with an
|
||
`X-KDE-Solid-Predicate` for a video DVD. Plasma's device notifier offers
|
||
"Play the DVD" while a disc is in the drive and withdraws it when the disc
|
||
leaves, so a room without a drive shows nothing and no room needs
|
||
`DVD_ENABLED` edited. Nothing polls, and `classroom_optical`'s two-second
|
||
loop goes away. **Not exercised** — the lab has no optical drive.
|
||
|
||
|
||
## Parent Call
|
||
|
||
The coordinator already existed and its contract is frozen. It was read
|
||
from `/home/media/docker/parent-call/README.md` and its source on the
|
||
control computer, not guessed, and nothing here changes it.
|
||
|
||
### What the teacher does
|
||
|
||
Click the mark on the left of the bar. A small card appears in the middle
|
||
of the screen, above whatever they were reading. It already knows the
|
||
room. They type a child's name and press **Call parent**. The card says
|
||
what happened, in the coordinator's own words, and closes itself four
|
||
seconds after a call is on its way.
|
||
|
||
Four steps, one of which is typing a name. The room is never asked for:
|
||
this machine knows which room it is, and offering a list is a chance to
|
||
pick the wrong one.
|
||
|
||
### The mark
|
||
|
||
Three were put on a real panel and judged at the size the bar draws them.
|
||
|
||
| tried | why it lost |
|
||
| --- | --- |
|
||
| a bell | sat inches from Plasma's own notification bell in the same bar |
|
||
| `call-outgoing`, a handset with an arrow | read as a telephone feature, not as fetching somebody |
|
||
| **an adult, a child, and one speech bubble** | kept |
|
||
|
||
Breeze has people and it has telephones and the two never meet, so the
|
||
kept mark is drawn rather than picked — on Breeze's 22-unit grid, with its
|
||
`ColorScheme-Text` class, so it recolours with the session and needs no
|
||
second file for the light theme. `docs/screenshots/pc-icon-vs-notifications.png`
|
||
is it beside the notification bell.
|
||
|
||
It is carried by **Quick Launch**, on the left, apart from the
|
||
applications. Two other widgets were tried first and both failed in ways
|
||
worth writing down:
|
||
|
||
* the **Icon** widget copies the desktop entry into the user's home and
|
||
uses the copy, so an update can never change the icon or the command —
|
||
it kept showing a bell out of a package that no longer contained one;
|
||
* a **task manager** reads the entry live, which fixes that, but a task
|
||
manager also shows every running window, so the auxiliary corner filled
|
||
up with the lesson's tasks.
|
||
|
||
### The states a teacher sees
|
||
|
||
Every word comes back from the coordinator in the `message` field. The
|
||
classroom never composes one for a call, because the coordinator is the
|
||
only thing that knows what happened.
|
||
|
||
| coordinator status | on screen | what the card does |
|
||
| --- | --- | --- |
|
||
| `submitted` | Sending | waits |
|
||
| `waiting_approval` | Waiting for approval | waits, amber |
|
||
| `sending` | Sending to sanctuary | waits |
|
||
| `sent_to_sanctuary` | Sent to sanctuary | green, closes after 4s |
|
||
| `rejected` | Not approved | red, offers Try again |
|
||
| `expired` | Expired before it was sent | red, offers Try again |
|
||
| `unavailable` | Parent Call is unavailable | red, no way to send |
|
||
| `error` | Could not reach the sanctuary | red, offers Try again |
|
||
|
||
**Nothing says "displayed".** A ProPresenter call that succeeded means the
|
||
message was accepted, not that anyone saw it, and *Sent to sanctuary* is
|
||
the strongest claim the room is entitled to make.
|
||
|
||
Before a name can be typed at all, the card reads the two retained topics
|
||
and refuses in four different ways: no broker (*Cannot reach the church
|
||
network*), no coordinator (*The sanctuary system is not running*), mode
|
||
`off` (*Parent Call is unavailable*, the coordinator's own wording), and
|
||
no retained mode at all (*Parent Call has not been set up yet*). A teacher
|
||
is never left believing a call went out.
|
||
|
||
### How it is built
|
||
|
||
```
|
||
Quick Launch -> classroom-parent-call a separate process
|
||
|
|
||
+-- classroom_parent_call topics, payloads, states
|
||
+-- paho, one short-lived clean session
|
||
```
|
||
|
||
Not a Plasma applet. A broker library, a network timeout and a text entry
|
||
have no business inside the only way a teacher has of getting anywhere.
|
||
Measured: killing the card leaves the panel untouched, and clicking the
|
||
mark again starts a new one.
|
||
|
||
Three properties the contract asks for and this keeps:
|
||
|
||
* the status topic is subscribed **before** the request is published, so
|
||
an answer cannot arrive before anyone is listening;
|
||
* the session is **clean** and nothing is retained, so a call from last
|
||
Sunday cannot replay into this one;
|
||
* listening stops the moment a status is terminal.
|
||
|
||
Bounded everywhere: 5s to reach the broker, 1.5s for retained state, 8s
|
||
for the coordinator's first answer, and 210s overall — its own
|
||
`REQUEST_TTL` is 180. Silence is reported as *No answer from the
|
||
sanctuary*, which is not the same as a refusal and is not shown as one.
|
||
|
||
Sixty tests cover the contract with no broker at all
|
||
(`tests/test-classroom-parent-call.sh`).
|
||
|
||
### Testing it without touching the sanctuary
|
||
|
||
`tools/lab/parent-call-rig` runs the **real coordinator** — copied from
|
||
the control computer — against a broker on loopback and a ProPresenter
|
||
that is a mock, and it **rejects the church's own networks from inside
|
||
the container before it will start**. That guard is not a convention; the
|
||
rig exits if either address is still reachable. It was needed:
|
||
`10.40.0.2:1883` is reachable from this development host.
|
||
|
||
Verified end to end on the packaged install: request accepted, queued,
|
||
approved by an operator, delivered as
|
||
`Name=Emma, Room=Nursery`, and *Sent to sanctuary* shown. Also `off`,
|
||
`rejected`, and the broker stopped.
|
||
|
||
|
||
## DVD
|
||
|
||
### It is an application now
|
||
|
||
DVD used to sit in the status row beside the clock, as though a disc were
|
||
a machine reading. It is in the row with Wonder Ink and the browser, with
|
||
Breeze's optical-disc mark, and Plasma gives it the same closed / running
|
||
/ active treatment as the others.
|
||
|
||
`DVD_ENABLED` is gone. `classroom-desktop-layout` asks the machine for
|
||
`/dev/sr*` and removes the launcher from the layout on a room that has no
|
||
drive. A computer that moves between rooms takes its drive with it, and
|
||
nobody remembers to edit a file when it does.
|
||
|
||
**Disc presence is not shown on the launcher.** Doing that needs either
|
||
polling or rewriting the panel while it runs, and rewriting the panel
|
||
while it runs was measured to destroy it. The launcher tracks the drive,
|
||
not the disc; a room with a drive and no disc gets a notification saying
|
||
so when it is pressed.
|
||
|
||
### The panel is rebuilt from the package at every login
|
||
|
||
Plasma keeps whatever panel it first built. An update could therefore
|
||
change the bar in the package and never change it in a room — which is
|
||
exactly what happened to an icon that outlived the file it came from.
|
||
|
||
`classroom-desktop-reset` removes the panel configuration before
|
||
plasmashell starts, so the bar is built from the layout every time. The
|
||
desktop is stateless: there is nothing in a teacher's home an update
|
||
cannot reach, and a damaged panel is repaired by the next login. Nothing
|
||
is lost, because Kiosk policy means a teacher never had anything in that
|
||
file.
|
||
|
||
Editing the live configuration instead was tried first and destroyed the
|
||
panel — the shell owns that file and rewrites it, so an edit races with
|
||
it. That is why this runs `Before=plasma-plasmashell.service`.
|
||
|
||
### The last of sway
|
||
|
||
The DVD player was the only thing still speaking a compositor the room no
|
||
longer runs. It asked sway's tree whether VLC's window had really gone
|
||
fullscreen, because VLC reports playing while its window is still the
|
||
black startup one the loading cover exists to hide.
|
||
|
||
**VLC's own status was measured as a replacement and is not one.** It
|
||
reports `<fullscreen>true</fullscreen>` in the same 250ms poll as
|
||
`<state>playing</state>`, because the flag is the option it was started
|
||
with and not a report about its window. So the cover is held for a floor
|
||
(`MINIMUM_COVER_SECONDS`) and still capped by the deadline that was
|
||
already there, and the compositor is not asked anything at all.
|
||
|
||
Focus-or-launch went the same way: the session already takes an exclusive
|
||
lock so a second one cannot start, and bringing an existing one to the
|
||
front is the panel's job.
|
||
|
||
Gone with it: `classroom-window`, `classroom_sway.py`, and
|
||
`classroom_chrome.py`, which turned out to be reachable from nothing.
|
||
**One mention of sway remains in the whole tree**, and it is the upgrade
|
||
step in `postinst` that removes the sway-era login shell from a room
|
||
being upgraded. Nothing executes it, imports it, or asks it anything.
|
||
|
||
Still unproven, and it is the same gap as before: playback end to end.
|
||
The lab has a drive with no readable disc and no libdvdcss, so VLC exits
|
||
on `--key-length=`. The loading cover, the control bar and the session's
|
||
own lifecycle all run; the picture does not.
|
||
|
||
|
||
## Audio: how to test it on the hardware
|
||
|
||
Nothing below can be done in the lab: the container has no sound card, so
|
||
the policy is tested as a function and applying it is not tested at all.
|
||
`classroom-audio show` prints what it can see and what it would choose,
|
||
and changes nothing — start there.
|
||
|
||
1. **Television on before the computer.** Boot with the TV on and an
|
||
input selected. Expected: sound comes out of the TV with nothing
|
||
touched. Check with `classroom-audio show`; the chosen port should be
|
||
an `hdmi-output-*`.
|
||
2. **Television off during boot, switched on afterwards.** Boot with the
|
||
TV off, wait for the desktop, then switch the TV on. Expected: within
|
||
a couple of seconds the output moves to HDMI by itself. This is the
|
||
case the room actually failed on, and the one worth watching.
|
||
3. **Browser already playing.** Start a YouTube video, then switch the TV
|
||
on or change the output. Expected: the sound follows **without
|
||
restarting Chrome**. That is the whole point of moving existing
|
||
streams; if Chrome still has to be restarted, the policy is not doing
|
||
its job and `journalctl --user -u ekklesia-classroom-audio` will say
|
||
how many streams it moved.
|
||
4. **Unplug and replug HDMI** while something is playing. Expected: falls
|
||
back, then returns.
|
||
5. **VLC already playing**, same as 3.
|
||
6. **Overrides.** `AUDIO_OUTPUT=analog` in
|
||
`/etc/ekklesia-classroom/device.conf`, restart the user service, and
|
||
confirm it obeys even with the TV on. Then `hdmi`, then back to
|
||
`auto`.
|
||
|
||
What to capture if something is wrong: `classroom-audio show`,
|
||
`pactl list cards short`, `pactl list sinks short`, and the service
|
||
journal. The decision is a pure function, so a bad choice can be
|
||
reproduced as a test case from what those print.
|
||
|
||
## Custom code this deletes
|
||
|
||
Deleted on this branch across both milestones, and counted rather than
|
||
estimated:
|
||
|
||
| | lines | replaced by |
|
||
| --- | --- | --- |
|
||
| `scripts/classroom-dock` | 754 | the Plasma panel |
|
||
| `scripts/classroom-chrome-windows` | 314 | `--start-maximized`, and a window manager |
|
||
| `tests/test-classroom-chrome-windows.sh` | 280 | — |
|
||
| `config/dock/style.css.in` | 102 | the colour scheme |
|
||
| `scripts/classroom-notifications` | 74 | Plasma notifications |
|
||
| `config/sway/config` | 62 | `kwinrc` |
|
||
| `config/mako/config.in` | 48 | `plasmanotifyrc` |
|
||
| `config/waybar/*` | 55 | the Plasma panel |
|
||
| `config/bash_profile`, getty autologin | 6 | SDDM |
|
||
| `config/icons/browser.svg` | — | Chrome's own icon |
|
||
| `scripts/classroom-window` | 208 | the panel, and a lock the DVD session already took |
|
||
| `lib/classroom_sway.py` | 127 | — |
|
||
| `lib/classroom_chrome.py` | 66 | nothing: it was reachable from nowhere |
|
||
| `tests/test-classroom-window.sh` | 214 | — |
|
||
| `config/icons/*` (dock set) | — | Breeze, and two marks the classroom draws |
|
||
| **total** | **≈ 2 310** | |
|
||
|
||
Rewritten rather than deleted: `classroom-browser` (41 → 30 lines, it no
|
||
longer decides launch-or-focus-or-minimise), `classroom-scale` (sway output
|
||
scale → `kscreen-doctor`), `classroom-notify` (found the session by looking
|
||
for sway and refused to run without mako; now looks for the compositor and
|
||
the shell).
|
||
|
||
**Nothing in the classroom speaks to a compositor any more.**
|
||
`classroom_icons.py`,
|
||
`classroom_style.py` and `classroom_optical.py` remain, reachable only
|
||
from the DVD player and the MQTT notifier: the DVD overlay draws its own
|
||
layer-shell surfaces and still needs the icon and stylesheet helpers the
|
||
dock shared, and disc detection is still ours.
|
||
|
||
Added since: the audio policy and its tests, the theme switch, Parent
|
||
Call and its contract module, the layout tailor, the panel reset, and
|
||
configuration.
|
||
|
||
Kept because they are the classroom's own subject matter: the DVD session
|
||
and CSS setup, printer setup, provisioning, MQTT, the updater, shutdown.
|
||
Added: the audio policy and its test, the theme switch, and configuration.
|
||
|
||
The package was built and its contents checked: every `/usr/local` path any
|
||
classroom command names is in it.
|
||
|
||
## Alternatives, and why they lost
|
||
|
||
**Stay on sway, add Waybar.** Cheapest migration, and it loses on the thing
|
||
that matters. wlroots has no minimise, so the invented hidden workspace
|
||
stays; sway tiles, so the 314-line window arbiter stays; there is no
|
||
window-manager decoration, so the dock's sticky Close button stays; and
|
||
Waybar is one more unsupervised process to lose. It removes none of the four
|
||
reported problems.
|
||
|
||
**Hyprland** (`experiment/hyprland`). Already measured on this repository and
|
||
already carrying two findings against it: the legacy config silently matches
|
||
nothing, and a Lua config changes the IPC language so the classroom would be
|
||
adopting a brand-new format early. For critical production UI in a room
|
||
nobody attends on a Sunday morning, that is the wrong end of the maturity
|
||
curve.
|
||
|
||
**GNOME.** Task state, a bottom bar and application pinning all need
|
||
extensions, and extensions break on GNOME updates. That is precisely the
|
||
"several fragile independently-maintained components" the brief rules out.
|
||
|
||
**Fedora KDE / Arch.** Both would supply the same Plasma. Neither supplies
|
||
anything Debian 13 does not, and both would cost the entire `.deb` and OTA
|
||
investment. Arch's rolling updates are the wrong risk for an appliance that
|
||
must survive unattended. **Changing distribution buys nothing here**, which
|
||
is the most useful negative result in this document.
|
||
|
||
**Firefox ESR / Chromium instead of Chrome.** No concrete benefit. Firefox
|
||
has no app mode, which is what makes Wonder Ink an application; Debian's
|
||
Chromium ships no Widevine. Chrome is what Wonder Ink is known to work in.
|
||
|
||
|
||
### Recovery, measured against the package
|
||
|
||
`tools/lab/recovery-check`, run after `tools/lab/from-package`, against
|
||
the lab exactly as the `.deb` built it:
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| panel, 8 × SIGKILL one second apart | back every time, 2.2–2.5s |
|
||
| compositor, 2 × SIGKILL | back in 0.1s |
|
||
| audio policy, 3 × SIGKILL | active every time |
|
||
| Parent Call killed | panel untouched |
|
||
| browser killed | panel untouched |
|
||
| afterwards | panel, compositor, 10 applets, audio all standing |
|
||
|
||
The drop-in that makes the panel restart unconditional is in the package,
|
||
not only in a hand-edited session: `/etc/systemd/user/plasma-plasmashell.service.d/`.
|
||
|
||
## Reliability results
|
||
|
||
| test | result |
|
||
| --- | --- |
|
||
| cold start, 3 identical runs | 16, 16, 17 s — panel, compositor and 11 applets every time |
|
||
| `plasmashell` SIGKILL ×8 at 1 s | back every time, ≈2.4 s |
|
||
| `plasmashell` on stock settings | **dead on the third kill** — fixed by the drop-in |
|
||
| `kwin_wayland` SIGKILL ×2 | back in 0.1 s, windows survived |
|
||
| audio service SIGKILL ×4 | active every time |
|
||
| panel configuration deleted | rebuilt itself from the layout script |
|
||
| right-click desktop / panel | no menu |
|
||
| KRunner | not running, not reachable |
|
||
| light ⇄ dark | persists both ways |
|
||
| memory, 3 samples over 6 minutes | flat: plasmashell 399 MB, KWin 141 MB under llvmpipe |
|
||
| package install on a clean machine | clean, and the session came up from it |
|
||
|
||
## The package
|
||
|
||
`tools/build-release.sh` builds it, and it was installed on a fresh machine
|
||
and booted from rather than described. `ekklesia-classroom_0.1.45` is 65
|
||
files: the desktop under `/etc/ekklesia-classroom/xdg`, a look and feel
|
||
package, two colour schemes, the launchers, the Solid action, two supervised
|
||
user services, and the classroom's own commands. Dependencies swap
|
||
`sway, swaybg, waybar, mako-notifier, python3-gi, gir1.2-gtklayershell-0.1`
|
||
for `plasma-desktop, plasma-workspace, kwin-wayland, plasma-nm, plasma-pa,
|
||
sddm`, and it `Conflicts` with the welcome tour, the software centre and the
|
||
phone pairing daemon that `plasma-desktop` would otherwise recommend into a
|
||
classroom.
|
||
|
||
Checked mechanically: every `/usr/local` path any classroom command names is
|
||
in the package.
|
||
|
||
The OTA mechanism is untouched. This is an ordinary version on the ordinary
|
||
channel structure, and an experimental build would go out as
|
||
`0.1.45~plasma1` on a channel of its own, exactly as the Hyprland milestone
|
||
did.
|
||
|
||
## Screenshots
|
||
|
||
Twenty-nine, in `docs/screenshots/`, indexed in
|
||
[screenshots/README.md](screenshots/README.md) — including the two that
|
||
record failures rather than successes.
|
||
|
||
## Not done, and honest about it
|
||
|
||
* **DVD end to end.** The overlay surfaces and disc detection were checked;
|
||
playback was not, and the focus helper is not ported. See *the DVD
|
||
player* above.
|
||
* **Real audio hardware.** Policy tested as a function; no card to apply it to.
|
||
* **Video decode and performance.** Software rendering only.
|
||
* **SDDM.** Recommended over the current `bash_profile` `exec`, because when
|
||
the session ends SDDM starts another one instead of leaving a teacher at a
|
||
shell prompt. Not yet configured or tested.
|
||
* **Packaging.** `tools/build-release.sh` still installs the sway-era file
|
||
list. The desktop is installed by `tools/lab/install-desktop`, which is the
|
||
shape the package needs but is not the package.
|
||
* **Parent Call and Live View.** Placed in the architecture, not built. Live
|
||
View's pin is KWin's Keep Above, which is already a title-bar button.
|
||
* **UI scale.** `classroom-scale` now asks `kscreen-doctor` instead of the
|
||
compositor, and 1.25 was applied and read back correctly. The *look* of it
|
||
could not be judged: a nested session shrinks its own canvas to the
|
||
logical size instead of keeping the display's mode, so everything got
|
||
smaller rather than larger. On a real output it does the opposite. Judge
|
||
this one on hardware.
|
||
* **Network states.** plasma-nm is in the bar and live — it correctly showed
|
||
a problem state throughout, because Docker configures the container's
|
||
interface before NetworkManager sees it and NM reports limited
|
||
connectivity for it. Online, connecting and offline transitions need a
|
||
machine whose network NetworkManager actually owns.
|
||
|
||
|
||
## Live View, and whether this desktop can carry it
|
||
|
||
Not implemented. What the KDE architecture already provides for it:
|
||
|
||
* **A small status widget on the idle desktop.** A Plasma applet, or —
|
||
preferring less custom desktop code — a notification when the sanctuary
|
||
goes live plus a launcher in the bar.
|
||
* **A preview that expands.** A separate application window, not
|
||
something embedded in the panel. The panel may launch it and show that
|
||
something is live; it must not be the player.
|
||
* **Pin above other applications.** This is KWin's *Keep Above*, which is
|
||
already a window state with a title-bar button, and a window rule can
|
||
force it for the preview's class. Nothing has to be invented, which was
|
||
the open question when this experiment started.
|
||
* **Unpin returns it to normal stacking.** The same button.
|
||
|
||
The likely media path (MediaMTX, WHEP) is a property of the player
|
||
application and does not touch the desktop architecture.
|
||
|
||
|
||
## The power button, and the way back in
|
||
|
||
### A press does nothing
|
||
|
||
Two layers, and both are needed. While a graphical session is running,
|
||
PowerDevil takes logind's `handle-power-key` inhibitor and acts on the
|
||
button itself, so a logind drop-in on its own would be overruled.
|
||
|
||
| layer | file | what it says |
|
||
| --- | --- | --- |
|
||
| logind | `/etc/systemd/logind.conf.d/ekklesia-classroom.conf` | `HandlePowerKey`, `HandleSuspendKey`, `HandleHibernateKey`, `HandleRebootKey` and every lid switch set to `ignore`, long-press included |
|
||
| PowerDevil | `/etc/ekklesia-classroom/xdg/powermanagementprofilesrc` | `powerButtonAction=0` and `lidAction=0` in all three profiles, groups marked `[$i]` so the session cannot put an action back |
|
||
|
||
logind covers the machine before the session starts, after it ends, and
|
||
if PowerDevil is not running at all. PowerDevil covers it while a teacher
|
||
is using it.
|
||
|
||
Measured in the lab: with the drop-in installed and logind reloaded,
|
||
logind reports `ignore` for the power, suspend, hibernate and lid keys —
|
||
it reported `poweroff` and `suspend` before — and the running graphical
|
||
session was not disturbed by the reload. The `postinst` therefore
|
||
reloads logind, so the button stops working at install time rather than
|
||
at the next boot.
|
||
|
||
**Holding the button still cuts the power.** That is firmware, below any
|
||
operating system, and it is left alone on purpose: it is the only
|
||
recovery a person standing at a wedged machine has.
|
||
|
||
**Needs the hardware.** Nothing in a container has a power button. What
|
||
is proven is that both layers are configured, packaged, and read back
|
||
the way they should. Pressing the button is a bench test.
|
||
|
||
### One chord opens a terminal
|
||
|
||
`Meta+Ctrl+Alt+Shift+K`.
|
||
|
||
Normal administration is still SSH. This is for the case where somebody
|
||
is standing at a machine and SSH is not available.
|
||
|
||
The chord is declared in the package — `X-KDE-Shortcuts` in
|
||
`ekklesia-admin-terminal.desktop` — rather than seeded into a teacher's
|
||
configuration, where an update could not reach it. KGlobalAccel read the
|
||
entry and registered a component for it; invoking that component's
|
||
`_launch` over D-Bus opened a working shell, which is how the whole path
|
||
was verified without a keyboard.
|
||
|
||
It needed a wrapper, for a reason worth recording. The classroom's Kiosk
|
||
policy sets `shell_access=false` and **Konsole honours it**: with the
|
||
classroom configuration in its cascade, Konsole starts and prints
|
||
*Program to run not set.* rather than opening a shell. That restriction
|
||
is worth keeping for everything else, so `classroom-admin-terminal`
|
||
lifts it for one process by leaving the classroom's configuration
|
||
directory out of that process's `XDG_CONFIG_DIRS`. Nothing is patched,
|
||
no binary is replaced, and the terminal therefore looks like stock KDE
|
||
rather than like the classroom — which is fair, because it is not part
|
||
of the classroom.
|
||
|
||
**It grants no privilege.** The classroom user has no sudo rule, and
|
||
anything needing root still needs the administrator's own account and
|
||
password. `shell_access` was always a barrier in the interface, never a
|
||
privilege boundary.
|
||
|
||
Konsole is a package dependency, so the route exists on every machine.
|
||
Its own entry is shadowed from `/usr/local/share/applications` with
|
||
`NoDisplay=true`, leaving Konsole's file exactly where its package put
|
||
it.
|
||
|
||
One thing found while testing, and kept as a second layer rather than
|
||
removed: **Konsole ships `X-KDE-Shortcuts=Ctrl+Alt+T`**, a chord a great
|
||
many people know. The shadow entry blanks it, but the registration comes
|
||
from Konsole's own file and survives that — so it is still registered.
|
||
It is also inert: Konsole's entry carries
|
||
`X-KDE-AuthorizeAction=shell_access`, and with the classroom's
|
||
`shell_access=false` the launch is refused. Measured: invoking that
|
||
component over D-Bus opened nothing.
|
||
|
||
Nothing is pinned to the panel. A task button appears only while a
|
||
terminal is open, and closing it returns the bar to three launchers.
|
||
|
||
**Needs the hardware.** The chord itself has never been pressed — key
|
||
injection cannot reach the nested session in this lab. What is verified
|
||
is that the shortcut is registered from the packaged entry and that its
|
||
action opens a shell.
|
||
|
||
|
||
## Long-Escape to turn the television off: it should change
|
||
|
||
The gesture asked for was: hold Escape, silence for three seconds, then a
|
||
five-second visible countdown, cancel on release. Deliberate, hard to
|
||
trigger by accident, and a clear intent.
|
||
|
||
### What was measured
|
||
|
||
* **KGlobalAccel accepts a bare `Escape`.** A probe entry declaring
|
||
`X-KDE-Shortcuts=Escape` was registered and given its own component. So
|
||
the binding is possible.
|
||
* **Press *and release* are both observable.** The component's D-Bus
|
||
interface carries `globalShortcutPressed` **and**
|
||
`globalShortcutReleased`. Hold duration and cancel-on-release are
|
||
therefore expressible without any input daemon of our own.
|
||
* **KWin's scripting API has no key events at all** — `registerShortcut`
|
||
and screen edges, and nothing that reports a key going up.
|
||
|
||
### What was not measured, and why it decides the answer
|
||
|
||
Whether a registered global shortcut **takes the key away from
|
||
applications**. Key injection cannot reach the nested session in this
|
||
lab, so this could not be pressed.
|
||
|
||
The mechanism says it does: on Wayland, KWin matches global shortcuts in
|
||
an input filter that runs before the event reaches the focused surface,
|
||
and a match stops propagation. That is how `Meta+Q` never arrives at an
|
||
application. A bare `Escape` bound globally should therefore stop
|
||
reaching anything — no leaving a fullscreen video, no dismissing a Chrome
|
||
dialog, no closing the Parent Call card.
|
||
|
||
**This is inference, not a measurement, and it is a thirty-second bench
|
||
test**: bind the probe entry above, press Escape in a fullscreen video,
|
||
and see whether it leaves fullscreen.
|
||
|
||
### The recommendation
|
||
|
||
**Change the gesture.** Even if Escape were forwarded as well as
|
||
observed, the whole ceremony would rest on a global grab of the key an
|
||
appliance uses most, and a countdown driven from the process holding
|
||
that grab is an input daemon in all but name. That is the fragile thing
|
||
the classroom keeps removing.
|
||
|
||
Two clean alternatives, both using mechanisms already proven here:
|
||
|
||
1. **Hold-to-confirm on screen.** A television control where the volume
|
||
applet is now: press and hold it, a countdown runs, releasing
|
||
cancels. Press, hold and release inside an application's own surface
|
||
are ordinary Qt and need no global input at all. This keeps every
|
||
property the gesture was for — deliberate, visible countdown,
|
||
cancel-on-release — and costs nothing in reliability.
|
||
2. **A modifier chord**, the same mechanism as the maintenance terminal
|
||
and proven to work in this session. Instant rather than ceremonial,
|
||
but impossible to press by accident. Worth adding as a second route
|
||
for somebody who cannot find the mouse.
|
||
|
||
If a keyboard hold is still wanted after the bench test, it should be on
|
||
a key the classroom does not otherwise use — never Escape.
|
||
|
||
## The television control service
|
||
|
||
Unchanged in shape from the previous checkpoint, and now with the
|
||
gesture question answered:
|
||
|
||
```
|
||
panel control / chord presentation only
|
||
|
|
||
v
|
||
ekklesia-classroom-tv.service small, supervised, ours
|
||
|
|
||
+-- backend: CEC over the adapter
|
||
+-- interface: on, off, volume up, volume down, mute, input
|
||
```
|
||
|
||
* The keyboard and the panel request **"TV off"**. They never speak CEC.
|
||
* The service decides how. CEC hardware can change without the desktop
|
||
noticing.
|
||
* The same service will own automatic TV-on at session start, volume,
|
||
mute and input, and it is why the computer's own volume applet is
|
||
expected to leave the bar.
|
||
* Supervised and self-recovering, the same way the audio policy is.
|
||
|
||
**No CEC code has been written and no protocol has been guessed.** The
|
||
adapters arrive today; the commands get measured before anything is
|
||
built on them.
|
||
|
||
## The upgrade that failed
|
||
|
||
The clean-install result was real and the migration was not. A classroom
|
||
machine that had been through the sway build and then the Hyprland
|
||
experiment took the 0.1.45 package and came up as **stock Debian Plasma**:
|
||
kickoff, a pager, a full system tray, the Debian wallpaper, the old Waybar
|
||
still across the top, mako still catching notifications — and then a lock
|
||
screen, on an account that has no password. Somebody had to give the
|
||
appliance account a password over SSH to get back in.
|
||
|
||
None of that was visible here, because the lab had been answering an
|
||
easier question than the one a room asks.
|
||
|
||
### Reproducing it
|
||
|
||
One line of the harness:
|
||
|
||
```
|
||
tools/lab/lab plasma-up: -e XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg
|
||
```
|
||
|
||
The lab had been handing the session under test the very setting whose
|
||
delivery was the thing in doubt. Removing it and starting the session the
|
||
way a machine does reproduced the failure exactly: 23 applets, kickoff,
|
||
pager, system tray, Debian wallpaper — `docs/screenshots/migration-before.png`
|
||
is that screen, and it is the teacher's screen.
|
||
|
||
### Root causes
|
||
|
||
**1. The classroom's settings never reached the session.** Everything the
|
||
room is — the panel layout, the lock screen that must never appear, the
|
||
idle behaviour, every Kiosk restriction — lives in
|
||
`/etc/ekklesia-classroom/xdg`, and reaches Plasma only if that directory
|
||
is in `XDG_CONFIG_DIRS`. It was set in `/etc/environment.d`, which
|
||
systemd's **user manager** reads. SDDM starts the session through PAM, and
|
||
PAM does not read `/etc/environment.d`. Debian's own
|
||
`/etc/xdg/plasma-workspace/env/env.sh` then set the variable from scratch,
|
||
because it was empty, and `startplasma` pushed its own environment back
|
||
into the user manager — so the value was not merely bypassed, it was
|
||
overwritten for every user service too. Measured in the session that came
|
||
up:
|
||
|
||
```
|
||
XDG_CONFIG_DIRS=/home/classroom/.config/kdedefaults:/etc/xdg:/usr/share/desktop-base/kf5-settings
|
||
LookAndFeelPackage = org.debian.desktop
|
||
Autolock = (unset, so on)
|
||
shell_access = (unset, so allowed)
|
||
```
|
||
|
||
The stock panel, the lock screen and the absence of every Kiosk
|
||
restriction are all that one line.
|
||
|
||
**2. Deleting the panel configuration made it worse.** The desktop reset
|
||
removes `plasma-org.kde.plasma.desktop-appletsrc` before the shell starts
|
||
so that the bar always comes from the package. With the look and feel
|
||
unresolvable, Plasma rebuilt the **Debian** default layout instead. The
|
||
reset was correct and was pointed at the wrong answer.
|
||
|
||
**3. Naming a colour scheme does not produce one.** `ColorScheme=EkklesiaDark`
|
||
is a label; KConfig reads the actual colours from `[Colors:*]` groups in
|
||
kdeglobals, and nothing was putting them there except a technician having
|
||
run `classroom-theme` once by hand. The clean-install screenshots looked
|
||
right for a reason the package did not own. A machine with no user
|
||
profile got Breeze light — confirmed here the moment a profile was
|
||
cleared.
|
||
|
||
**4. A stale user profile outranks the package.** `~/.config/kdeglobals`
|
||
sits ahead of every system directory, and `~/.config/kdedefaults` ahead of
|
||
ours. A migrated machine carried a Breeze-light palette in both.
|
||
|
||
**5. The old shell was still being started.** `waybar.service` was
|
||
activated through `graphical-session.target.wants` — and, found only by
|
||
running the crash-recovery suite on a migrated machine, **mako came back
|
||
through D-Bus**: the panel was killed, something sent a notification while
|
||
nothing owned `org.freedesktop.Notifications`, and D-Bus started mako from
|
||
the file `mako-notifier` ships. Deactivating the systemd unit would not
|
||
have been enough.
|
||
|
||
And one more that had not bitten yet: `device.conf` still said
|
||
`CHANNEL=hyprland`, so the next release published on that channel would
|
||
have handed the room the Hyprland experiment back.
|
||
|
||
### The fix
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| `config/plasma/env/zz-ekklesia-classroom.sh` | new. `/etc/xdg/plasma-workspace/env/` is sourced by `startplasma` for every session; the name sorts after Debian's `env.sh`, so it appends rather than replaces, and it is a no-op if the directory is already there. This is the root fix. |
|
||
| `config/plasma/xdg/kdeglobals` | `LookAndFeelPackage[$i]` — per-entry immutability, so no leftover profile and no stray write can answer that question differently. The palette stays writable, because `classroom-theme` may change it. |
|
||
| `tools/build-kdeglobals` | new. Composes the shipped kdeglobals from the session policy **and** `EkklesiaDark.colors`, so the room is dark because the package says so. One source of truth: it composes, it does not duplicate. |
|
||
| `scripts/classroom-desktop-reset` | asserts the cascade and repairs it for the shell about to start if it is wrong; removes another desktop's look and feel defaults and a foreign palette; keeps an Ekklesia one; clears a user power profile. |
|
||
| `scripts/classroom-migrate` | new. Stops, deactivates and masks the old shell's units, removes activation from all three directories it can live in, removes hand-written unit files that no Debian package owns, clears old autostart entries, removes the login shell that used to start sway, and migrates `device.conf`. Run from `postinst` on every install. |
|
||
| `config/dbus/fr.emersion.mako.service` | new. Shadows mako's D-Bus activation from `/usr/local/share`, where the session bus looks first. `Exec=/bin/false`, which is what a machine with no notification daemon does anyway. |
|
||
| `config/plasma/xdg/powermanagementprofilesrc` | profiles immutable at the profile level, not just on the button subgroups: PowerDevil loads an idle action when it finds a group for it, so a writable profile is one a leftover user file can add a suspend to. |
|
||
| `config/systemd/logind-ekklesia-classroom.conf` | `IdleAction=ignore` as well. |
|
||
| `config/sddm/ekklesia-classroom.conf` | uid range 1000–1999. The first migrated room's classroom account was uid 1001 and would not have appeared in the greeter at all. |
|
||
|
||
### What device.conf means now
|
||
|
||
`COMPOSITOR` and `HYPR_APPEARANCE` are removed on upgrade. There is one
|
||
desktop stack and nothing left for them to choose; a stale value is an
|
||
instruction this package cannot carry out.
|
||
|
||
`CHANNEL=hyprland` becomes `CHANNEL=classroom-v2`. Not `stable` and not
|
||
`canary`: both are the sway line, and a release published there would
|
||
convert a v2 machine back. **That channel is not published yet**, so a
|
||
migrated machine takes no updates until it is — which is the honest state
|
||
of an unpublished experiment, and better than being quietly reverted.
|
||
Everything else in the file, the broker password included, is rewritten
|
||
line by line and byte-for-byte.
|
||
|
||
### The tests that would have caught it
|
||
|
||
- `tools/lab/lab plasma-up` no longer sets `XDG_CONFIG_DIRS`. The package
|
||
has to put it there, the way it has to on a machine.
|
||
- `tools/lab/lab reboot` — a real restart of the machine under test: new
|
||
PID 1, new user manager, every drop-in and generator read again. **SDDM
|
||
autologin genuinely runs here**: it authenticates the classroom through
|
||
PAM and launches the Plasma session, and then has no display hardware to
|
||
draw on. Both scenarios now go through it.
|
||
- `tools/lab/converged-check` — 60 properties of a converged machine,
|
||
asked of the running system and never of the repository. Both scenarios
|
||
run the same list, because a fresh install and a migrated one have to be
|
||
indistinguishable from a teacher's chair.
|
||
- `tools/lab/dirty-upgrade` — the Hyprland package actually installed,
|
||
with sway, waybar and mako behind it; Waybar activated in all three
|
||
directories; an autostart entry; a device.conf asking for a compositor
|
||
that is gone; a Breeze-light profile in both places that outrank the
|
||
package; a stock panel already built; and a user power profile that
|
||
would suspend the room. Then the new package, a reboot, and the
|
||
questions.
|
||
- `tests/test-migration.sh` — 41 source-level checks, one per reason the
|
||
failure happened.
|
||
|
||
### Results
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| dirty upgrade → converged | 60 / 60 |
|
||
| installed a second time over itself | 60 / 60, device.conf unchanged |
|
||
| clean install → converged | 60 / 60 |
|
||
| panel killed eight times | back every time, 2.2–7.4s |
|
||
| compositor killed twice | 0.1–0.4s |
|
||
| audio policy killed three times | active every time |
|
||
| the room's light theme across a logout | survived |
|
||
| every test script | 11 / 11 |
|
||
|
||
`docs/screenshots/migration-after.png` and `v2-from-package.png` are the
|
||
migrated machine and the fresh one. They are the same desktop.
|
||
|
||
### One regression this pass made and caught
|
||
|
||
The first version of the reset deleted `~/.config/kdedefaults`
|
||
unconditionally. Plasma treats that directory as the record of which look
|
||
and feel is in force, so deleting it made Plasma re-apply the look and
|
||
feel at every login — which throws away the room's chosen palette.
|
||
Measured: `classroom-theme light`, log out, and the bar came back dark.
|
||
The rule is now whose the defaults are: the classroom's are kept, another
|
||
desktop's are removed.
|
||
|
||
## Unresolved risks
|
||
|
||
In the order I would worry about them.
|
||
|
||
1. **DVD playback has never run end to end on this desktop.** The port off
|
||
sway is done and nothing asks a compositor anything, but the lab has a
|
||
drive with no readable disc and no libdvdcss, so VLC exits before a
|
||
picture. A room that uses DVDs should not take this build until a real
|
||
disc has played, the cover has dropped at the right moment, and eject
|
||
has returned cleanly to the desktop.
|
||
2. **The loading cover is now timed, not observed.** It used to wait for
|
||
the window manager to confirm VLC was fullscreen. It waits for a floor
|
||
instead, because VLC's own fullscreen flag was measured to be the
|
||
option it was given rather than a report about its window. On real
|
||
hardware the disc is far slower than the floor, so this should not
|
||
show — but if the cover ever drops onto a black VLC window, this is
|
||
why, and the fix is to raise `MINIMUM_COVER_SECONDS`.
|
||
3. **Parent Call has never had a real key pressed into it.** Keyboard
|
||
injection cannot reach the nested session in this lab at all — the
|
||
same limit that stopped it reaching Chrome — and the one tool that
|
||
tries disturbs the session enough to close the card. The name was
|
||
typed through a lab-only `--child` argument. **Type a name on the
|
||
bench before a Sunday.**
|
||
4. **SDDM has never put a picture on a screen.** More is known than
|
||
before: in the lab it really does autologin, authenticating the
|
||
classroom through PAM and launching the Plasma session — that is now
|
||
one of the checks. What it cannot do in a container is find a display
|
||
to draw on, so everything from the compositor's first frame onwards is
|
||
still a bench test. It remains the first thing to check on a VM or a
|
||
bench machine.
|
||
5. **No real audio hardware has been through the policy.** The decision is
|
||
well tested; applying it is not. A wrong `pactl set-card-profile` on
|
||
real hardware is a silent room.
|
||
6. **Plasma is a much larger surface than sway.** More code, more services,
|
||
more that can regress in a Debian point release. The mitigations are
|
||
that all of it is supervised, that Debian 13 is frozen apart from
|
||
security updates, and that the OTA channel structure already lets a
|
||
build be held back. But it is a real trade and it should be named.
|
||
7. **Performance is unmeasured on classroom hardware.** Everything here ran
|
||
on llvmpipe, which says nothing useful about an old office machine with
|
||
Intel graphics. Plasma with compositing is heavier than sway.
|
||
8. **Chrome remains a third-party repository.** Unchanged from today, but
|
||
still the one part of the stack Debian does not update.
|
||
9. **The wallpaper is a stand-in.** The real illustration has not been seen
|
||
against this palette, and is to be supplied separately.
|
||
10. **The disc launcher tracks the drive, not the disc.** A room with a
|
||
drive shows the DVD button whether or not anything is in it.
|
||
11. **The power button has never been pressed.** Both layers are
|
||
configured, packaged, and read back correctly, but nothing in a
|
||
container has a power button.
|
||
12. **The maintenance chord has never been typed.** The shortcut is
|
||
registered from the packaged entry and its action opens a shell —
|
||
verified over D-Bus — but key injection cannot reach the nested
|
||
session, so `Meta+Ctrl+Alt+Shift+K` itself is unpressed.
|
||
13. **Idle behaviour is configured and unobserved.** No suspend, no dim,
|
||
no blank and no lock are all now in the package, immutable, and read
|
||
back correctly from a migrated session. Nothing in a container goes
|
||
idle, so none of it has been watched not happening. **Leave a bench
|
||
machine alone for an hour.**
|
||
14. **The v2 channel does not exist yet.** A migrated machine is moved to
|
||
`CHANNEL=classroom-v2` so that a stable or Hyprland release cannot
|
||
take it back, and nothing is published there. Until it is, a v2
|
||
machine takes no updates at all. That is deliberate; it is also a
|
||
thing to remember before leaving one in a room.
|
||
15. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it
|
||
and the registration survives being shadowed. It is inert, because
|
||
Konsole refuses to open a shell under the classroom's
|
||
`shell_access=false` — measured — but it rests on that one
|
||
restriction rather than on the chord not existing.
|
||
|
||
## Would I trust it on a classroom computer, unattended, for a Sunday?
|
||
|
||
**For a room that does not use DVDs: yes, after the checks below.** The
|
||
reasoning is not that Plasma is nicer. It is that the four failures the
|
||
church actually reported are all failures of code we wrote, three of them
|
||
are gone because the code is gone, and the fourth — the panel disappearing
|
||
— went from *reproducible on the third crash* to *survived eight kills* and
|
||
is now a supervised system service rather than a process started by a
|
||
compositor and forgotten.
|
||
|
||
**For a room that uses DVDs: not yet.** Finish the focus port and play a
|
||
real disc first.
|
||
|
||
Before any room:
|
||
|
||
1. Boot a bench machine from the package and confirm SDDM autologins, and
|
||
that killing the session brings it back rather than a shell.
|
||
2. Put a television on HDMI, switch it on after the computer, and confirm
|
||
sound arrives without touching anything, with the browser already
|
||
open. The full procedure is under *Audio: how to test it on the
|
||
hardware*.
|
||
3. Press Parent Call and **type a name with a real keyboard**. Then do it
|
||
with the coordinator's mode off, and with the broker stopped.
|
||
4. **Press the power button.** Nothing should happen. Then hold it, and
|
||
confirm the machine still dies — that is the recovery path and it
|
||
must stay.
|
||
5. **Type Meta+Ctrl+Alt+Shift+K.** A terminal should open. Then try
|
||
Ctrl+Alt+T and confirm nothing does.
|
||
6. Print to Elmer.
|
||
7. Play a DVD, if that room uses them: cover, picture, controls, eject.
|
||
8. Leave it running from Friday to Sunday.
|
||
|
||
## Alternate operating systems
|
||
|
||
None is required. The recommendation stays on Debian 13, which the classroom
|
||
already runs and already packages for.
|
||
|
||
If the church later wants Fedora KDE or Arch compared on real hardware, that
|
||
needs a VM this session could not create:
|
||
|
||
> **Recommended next candidate: none.**
|
||
> Debian 13 + Plasma 6.3 was actually tested, in a container, on this branch.
|
||
> A disposable VM is needed only for what a container cannot show:
|
||
> 4 vCPU, 8 GiB RAM, 60 GiB disk, a real GPU or working virtio-gpu,
|
||
> an HDMI-capable audio device, and an optical drive — to test DVD
|
||
> playback, audio switching against real hardware, SDDM autologin,
|
||
> display hotplug, and performance on classroom-class hardware.
|
||
|
||
The existing Debian classroom test VM should be kept.
|
||
|
||
## Checkpoint log
|
||
|
||
* Branch opened at `aadb1b8`; host recorded as unable to create a VM.
|
||
* Lab built; Plasma 6.3.6 running software-rendered at 1920×1080.
|
||
* Panel supervision measured, failure reproduced, drop-in written, re-measured.
|
||
* Classroom desktop written: colours, layout, window rules, Kiosk, launchers.
|
||
* Task states measured; minimise removed as a route.
|
||
* Secondary windows, print dialog, notifications, fullscreen, themes, lockdown
|
||
all verified on a running session.
|
||
* Audio policy written with the failing room as a test.
|
||
* Cold starts repeated: eleven, identical.
|
||
* Package built, and the file conflict and the stale `postinst` it exposed
|
||
both fixed.
|
||
* `tools/lab/from-package` run: fresh machine, `.deb`, session up, both
|
||
applications launched, one button each.
|
||
* **Parent Call built** against the coordinator's own frozen contract,
|
||
and proven end to end on a rig that rejects the church's networks
|
||
before it starts.
|
||
* **Parent Call's mark** replaced twice and judged on a real panel.
|
||
* **The last of sway removed**: the DVD player asks no compositor
|
||
anything, and `classroom-window`, `classroom_sway` and
|
||
`classroom_chrome` are gone.
|
||
* **DVD moved out of the status row** into the applications, and the
|
||
launcher now follows the machine's drive rather than `DVD_ENABLED`.
|
||
* **The panel is rebuilt from the package at every login**, so an update
|
||
can reach a room that has already run.
|
||
* **Recovery re-measured against the packaged build**, not a hand-edited
|
||
session.
|
||
* **The power button disabled at both layers**, packaged, and read back
|
||
from logind after a reload that did not disturb the session.
|
||
* **A maintenance terminal behind one obscure chord**, declared in the
|
||
package, hidden from every application list, granting no privilege.
|
||
* **Long-Escape investigated and recommended against**, with the two
|
||
clean alternatives written down.
|
||
* **The migration failure found on a real room and fixed in the package**:
|
||
the settings directory now reaches the session the way Plasma delivers
|
||
it, the look and feel cannot be answered for by a stale profile, the
|
||
palette is in the package rather than in somebody's home directory, and
|
||
the old shell is stopped, deactivated, masked and — the one that was
|
||
only found by killing the panel — unable to come back through D-Bus.
|
||
* **The lab stopped flattering the package**: it no longer sets the
|
||
session's configuration path by hand, it restarts the machine, and it
|
||
asks a fresh install and a machine upgraded from the Hyprland
|
||
experiment the same sixty questions. Both answer them identically.
|
||
|
||
**Next**, in order:
|
||
|
||
0. **The thirty-second Escape test**, before anything is built on the
|
||
gesture: bind a probe entry to `Escape`, press it in a fullscreen
|
||
video, and see whether the video leaves fullscreen. That settles the
|
||
one thing this pass could not measure.
|
||
1. **On the bench, tonight.** Type a name into Parent Call with a real
|
||
keyboard. Switch a television on after the computer and listen. Play a
|
||
disc. These three are the only things left that a container cannot
|
||
answer, and each of them is a Sunday-morning path. Add the power
|
||
button and the maintenance chord to that list.
|
||
2. Publish the v2 channel. `classroom-migrate` already moves a migrated
|
||
room onto `CHANNEL=classroom-v2`, and nothing serves that manifest
|
||
yet, so those machines are taking no updates at all. This is now the
|
||
first packaging job rather than a tidy-up.
|
||
3. Take the upgraded VM through the same list by hand:
|
||
`tools/lab/converged-check` is the shape of it, and the things a
|
||
container cannot answer are the lock screen never appearing after an
|
||
hour alone, and the machine still being there on Monday.
|
||
4. Bench-test SDDM on hardware: autologin all the way to a picture, and
|
||
that ending the session brings the classroom back rather than a shell.
|
||
5. When the CEC adapters have been exercised, replace the computer volume
|
||
applet with a television control, behind the service boundary in
|
||
*Television control, and where CEC will fit*.
|
||
6. Live View, when the rest is quiet.
|