940 lines
44 KiB
Markdown
940 lines
44 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.
|
||
|
||
|
||
## Television control, and where CEC will fit
|
||
|
||
The devices arrive today. **Nothing here guesses at their protocol** and
|
||
no CEC code has been written.
|
||
|
||
What the architecture needs to leave room for: the volume control in the
|
||
bar today is the *computer's* volume, and once the television is under
|
||
CEC control that is the wrong thing for a teacher to be adjusting. The
|
||
teacher should be turning the television up, not the computer.
|
||
|
||
The shape that keeps Plasma out of the hardware:
|
||
|
||
```
|
||
panel button / notification presentation only
|
||
|
|
||
v
|
||
ekklesia-classroom-tv.service a small supervised user service
|
||
| the same shape as the audio policy
|
||
+-- backend: CEC over the adapter
|
||
+-- interface: on, off, volume up, volume down, mute, input
|
||
```
|
||
|
||
Three properties to hold to:
|
||
|
||
* **Plasma is presentation.** The panel may show state and send an
|
||
action; it must not contain the protocol. That is why the audio policy
|
||
is a service and not a plasmoid, and this should follow it.
|
||
* **The interface is verbs, not device commands.** "Volume up" is the
|
||
contract; whether that is a CEC user-control-pressed opcode or
|
||
something else is the backend's business.
|
||
* **Supervised and failure-tolerant.** A television that is off, an
|
||
adapter that is unplugged, or a set that ignores CEC must degrade to
|
||
nothing happening, never to a stuck classroom.
|
||
|
||
When the hardware has been exercised, the computer volume applet comes
|
||
out of the bar and a television control goes in. Until then it stays,
|
||
because a room with no sound control at all is worse.
|
||
|
||
|
||
## 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.
|
||
|
||
## 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 is configured and untested.** It cannot be exercised in a
|
||
container, and it is the thing that starts the session. If the config is
|
||
wrong the room boots to nothing. This is the first thing to check on a
|
||
VM or a bench machine, and it is why the checklist below starts there.
|
||
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.
|
||
|
||
## 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. Print to Elmer.
|
||
5. Play a DVD, if that room uses them: cover, picture, controls, eject.
|
||
6. 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.
|
||
|
||
**Next**, in order:
|
||
|
||
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.
|
||
2. Publish `0.1.45~plasma1` to a channel of its own, the way the Hyprland
|
||
milestone did, so a stable or canary room can never be offered it.
|
||
3. Bench-test SDDM: autologin, and that ending the session brings the
|
||
classroom back rather than a shell.
|
||
4. 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*.
|
||
5. Live View, when the rest is quiet.
|