Parent Call told rooms they had never been set up. Nothing about the
rooms was wrong. Read from the church broker: …/coordinator and
…/pending are retained and …/mode is absent, and the coordinator's own
source says why — it publishes the retained mode only in answer to an
operator writing to …/mode/set, and nobody ever has. Every classroom
waited a second and a half for a message that will never exist.
A missing mode is now read as the coordinator's own default rather than
as an unconfigured room: the room offers the call and lets the sanctuary
end answer, because that is the end that knows. A mode that really is
off still refuses, in the coordinator's words, and a broker that is
missing or unreachable still says which of those it is. Proved against a
rig broker made to match the church's exactly, with a call landing on
the sanctuary mock.
The card is anchored to the bottom-left corner instead of centred, by
layer shell, which is what the disc player's cover has used under KWin
for months. Anchoring rather than positioning is what makes it right at
1080p and at 4K without a coordinate anywhere. It falls back to the
centred card if the library is missing.
Notifications are sized for a room. Every Plasma measurement is a grid
unit and a grid unit is the height of the general font, so the font is
the lever — 15 point instead of Breeze's 10, which takes the card from
about 350x110 to about 410x145 at 100% and doubles again at 200%. Text,
icon, padding and buttons move together. The chime stays off. No shell
package: that path cost the panel and is not coming back.
And the product name is gone from everything a teacher sees. The
notification card is from Classroom, the disc player's from DVD.
Co-Authored-By: Claude Opus 5 <[email protected]>
classroom-migrate knew how to remove a setting, move a channel and point
a room at the broker, and knew nothing about which settings the classroom
has gained since a room was installed. That list is in
classroom_config.py and it stays there: the migration now hands the file
to ensure_device_config when its own work is done, rather than growing a
second copy of it.
It matters for the command rather than the package. postinst already
called the shared helper before running the migration, so an install
converged; a technician running `sudo classroom-migrate` by hand on an
old machine got a file that was still short of LIVE_ENABLED,
AUDIO_OUTPUT and UI_SCALE. Both paths end in the same place now, and the
release gate runs the command by hand on an already-converged machine to
prove it.
The validator's own expectation was the release-blocking failure, and it
was not what it looked like: it stops at CHANNEL=canary, before it ever
reaches the LIVE_ENABLED count. canary is the sway line, and moving a
migrated room off it is deliberate — a Plasma room left there is handed
the old desktop back by the next release published to it. The fixture now
expects the move, checks that every setting this release has arrived
exactly once, and checks it again after a reinstall.
Co-Authored-By: Claude Opus 5 <[email protected]>
Little Waders took the Plasma build and reported what a container
cannot. A disc player on a machine with no optical drive, a clock nobody
at the back of the room could read, a browser at half the size of the
panel around it, and Parent Call with no broker to call.
The broker is the same for every classroom, so it is a default now and
not something to fill in room by room. classroom-migrate brings an
existing room to the same place without touching its name, its username
or its password, and Parent Call distinguishes a broker it cannot reach
from one that was never set up: those send somebody to look at
completely different things.
The disc player's detection was right and ran too late — alongside the
session rather than before it, and the panel is built from the layout the
moment a session starts. It runs before the display manager now, and
again from udev when a drive is plugged in.
UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a
clock pinned at 15 points, which is the same size at every scale; and a
browser that was never told the number at all. All three are fixed and
the room doubles together at 200%.
This also brings 0.1.47's work into the repository, where it had only
ever existed in a build directory under /tmp: the hidden display dialog,
its authenticated writer, the polkit action, and the church's own
wallpaper. One piece of it is deliberately left out. The custom volume
display needed a shell package of our own, and with one selected
plasmashell builds no panel at all — measured twice, with that build's
own files, on a classroom that deletes its panel configuration at every
login by design.
The lab connected to the real church broker the moment the package
defaulted to it. Nothing was left behind, which was checked, but the
guard that should have stopped it had never worked: it used iptables,
which the lab image does not have, and probed with /dev/tcp under dash,
where that always fails. Both reported success.
Co-Authored-By: Claude Opus 5 <[email protected]>
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]>
Two layers for the button, because one is not enough: while a session is
running PowerDevil holds logind's handle-power-key inhibitor and acts on
the button itself, so a logind drop-in alone would be overruled. logind
covers the machine before the session starts, after it ends, and if
PowerDevil is not running. Verified: after the drop-in is installed and
logind reloaded, it reports ignore for the power, suspend, hibernate and
lid keys, and the running session was not disturbed by the reload.
Holding the button still cuts the power. That is firmware, below any
operating system, and it is deliberately left alone as the only recovery
a person standing at a wedged machine has.
The maintenance terminal is Meta+Ctrl+Alt+Shift+K, declared in the
package rather than seeded into a teacher's configuration, where an
update could not reach it. KGlobalAccel read it and registered the
component; invoking it opened a working shell.
It needed a wrapper. The classroom's Kiosk policy sets shell_access=false
and Konsole honours it — measured: with the classroom configuration in
its cascade Konsole starts and says "Program to run not set." The
restriction is worth keeping everywhere else, so it is lifted for this
one process by leaving the classroom's configuration directory out of
its cascade. It grants no privilege: the classroom user has no sudo
rule, and that restriction was always a barrier in the interface rather
than a privilege boundary.
Konsole's own entry is shadowed and hidden from /usr/local/share, so
nothing is patched and Konsole's package stays maintainable. 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.
Co-Authored-By: Claude Opus 5 <[email protected]>
Three things in this window that a container cannot answer, and they are
now the top of the next-steps list: a real key pressed into Parent Call,
a television switched on after the computer, and a disc that plays.
Also brings the comments in the shared libraries up to date. The only
mention of sway left in the tree is the upgrade step that removes the
sway-era login shell from a room being upgraded.
Co-Authored-By: Claude Opus 5 <[email protected]>
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 in the same 250ms poll as playing, 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 and capped by the deadline that
was already there, and the compositor is not asked anything.
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.
classroom-window, classroom_sway and classroom_chrome are gone with it —
the last of them was reachable from nothing at all.
DVD also moves out of the status row, where it sat beside the clock as
though a disc were a machine reading. It is an application: it is in the
row with Wonder Ink and the browser, and classroom-provision removes it
from the layout on a machine with no optical drive, which retires
DVD_ENABLED. A computer that moves between rooms takes its drive with
it; nobody remembers to edit a file when it does.
The panel is rebuilt from the package at every login now. Plasma keeps
whatever panel it first built, so an update could change the bar in the
package and never change it in a room — measured, with an icon that
outlived the file it came from. Editing the live panel configuration was
tried instead and destroyed the panel, because the shell owns that file.
Removing it before the shell starts is safe and makes the desktop
stateless.
Co-Authored-By: Claude Opus 5 <[email protected]>
The coordinator already existed on the control computer and its contract
is frozen, so this is the classroom half of a contract that was read from
its README and source rather than guessed: four fields out, the status
topic subscribed before anything is published, and every word the teacher
reads taken from the coordinator's own message field.
It is a separate process, 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; if this dies the bar is untouched.
The panel mark is Breeze's call-outgoing. The old one was a bell, sitting
two inches from Plasma's notification bell in the same bar and meaning
something entirely different.
Two things it will not do: claim a call was displayed, and let a teacher
type the room. The machine already knows which room it is.
Sixty tests cover the contract without a broker. The live path was proven
against the real coordinator running in the lab, on a loopback broker,
with ProPresenter mocked and the church's networks rejected from inside
the container first.
Co-Authored-By: Claude Opus 5 <[email protected]>
Building the package showed what reading the tree did not: classroom-dvd
still calls classroom-window, classroom-mqtt still calls classroom-notify,
and the DVD overlay still imports the icon and stylesheet helpers the dock
shared. They go back in, and the write-up now counts what was actually
removed rather than what could be.
Co-Authored-By: Claude Opus 5 <[email protected]>
The install list loses the dock, the window helper, the Chrome window
arbiter, the notification wrapper, the compositor and bar configs and the
GTK icon and stylesheet machinery, and gains a look and feel package, a
colour scheme, window rules, Kiosk restrictions and two supervised user
services. Dependencies follow: Plasma and SDDM in, sway, waybar, mako and
gtk-layer-shell out.
Co-Authored-By: Claude Opus 5 <[email protected]>
The room that prompted this had a television on HDMI with working PCM
audio and nothing in the analog jack, and WirePlumber chose analog. A
teacher who corrected it by hand then had to restart the browser before
any sound came out.
The policy is a pure function so it can be tested against that room
rather than against a machine: an unavailable port is not a candidate,
an analog port merely existing is not evidence of a cable, and an
explicit AUDIO_OUTPUT overrides the lot. Twelve cases, the real room
among them.
The daemon follows PipeWire's own event stream rather than polling, and
moves streams that are already playing, which is the part that stops a
teacher having to close and reopen the browser.
Co-Authored-By: Claude Opus 5 <[email protected]>
The whole appliance UI is now configuration for a desktop Debian already
ships: a Plasma 6.3 session with a colour scheme, a look and feel package
that builds the bar, window rules, and KDE's own Kiosk restrictions.
Two things were measured rather than assumed, and both changed a decision.
Plasma restarts a crashed panel three times a minute and then gives up,
which is the failure a classroom has already had. Four lines of systemd
drop-in make it unconditional; the panel then survived eight SIGKILLs a
second apart, back each time in 2.4s.
Plasma draws a minimised task and a background task identically, so the
five states the room needs cannot all be shown. Rather than write a task
manager, the classroom removes minimise: clicking a launcher always means
show me this.
Co-Authored-By: Claude Opus 5 <[email protected]>
The render helper sat on top of the Print dialog, so the dialog could
not be reached and nothing could be printed.
Two causes. The daemon could not see window titles at all — the tree
walk never collected them — and both windows arrive close enough
together to be configured in the same pass, in whatever order the tree
lists them. Configuring a window focuses it, so whichever was done last
ended up in front, and that was not reliably the dialog.
The dialog is now recognised by its exact title and configured last, and
if a Chrome popup takes focus while it exists, focus is handed straight
back. Focus is only ever reclaimed from another popup: clicking the main
browser or the dock is the teacher's business and is left alone.
A title match, narrowly, and only for this one known dialog. Creation
order still decides which Chrome window is the browser; nothing else is
classified by what it happens to be called.
Nothing auto-closes the helper. After Print is cancelled it stays, and
the sticky dock X still closes it.
Proven by looking rather than by state. The render window is given a
saturated background in the fixture so a screenshot answers "which
window is in front" unambiguously: Print opens in front, the helper is
then deliberately raised over it and Print comes back in front,
cancelling Print leaves the helper visible with the dock X still
offering it, and that X closes exactly the helper and leaves the main
browser untouched.
Co-Authored-By: Claude Opus 5 <[email protected]>
The Close button followed focus, and moving the pointer down to the dock
moves focus off the popup. So the button disappeared somewhere on the
journey to it, and a secondary Chrome window could not be closed with a
mouse at all. Found on the endpoint; my fault.
The target is remembered instead. Once a secondary window has been
focused the dock keeps offering to close it, and losing focus — to the
dock, the main browser, a print dialog, any layer-shell surface —
changes nothing. It moves on only when that window closes or another
secondary takes over, and it falls back to another open secondary rather
than to nothing, so cancelling a print dialog leaves the render window
still closeable. It is chosen from the secondary list, which never
contains the first window, so it can never name the main browser.
Restoring the floating role to a window that already existed no longer
moves or focuses it. Moving it would undo wherever the teacher dragged
it; focusing it would raise the render window over the print dialog
sitting above it, which is one way the print flow was getting disturbed.
Verified by driving a real pointer through the exact path that was
broken: pointer over the popup, click to focus, then travel down to the
dock in steps, screenshotting throughout. The button is still there on
arrival, at 992,1041. Then the print flow: main browser, render window
and a separate Print dialog, pointer to the dock, button still there,
and killing the dialog leaves the render window as the new target with
the main browser untouched.
The button's own action could not be actuated in that harness, because
synthetic pointer input does not reach Wayland clients under headless
sway — the Browser button does not respond to it either. The action was
run directly instead, closing exactly the remembered container and
leaving main Chrome alive.
Co-Authored-By: Claude Opus 5 <[email protected]>
Three things, none of them redesigns.
The close affordance for a secondary Chrome window was a separate
layer-shell surface anchored to that window's corner, repositioned by a
five-per-second poll. Any position change tore the surface down and
built another, so on real hardware it chased the window and flickered.
Removed entirely: the GTK button process, the geometry maths that placed
it, the --close-button mode that spawned it, and the poll loop that
drove it. The window helper no longer imports GTK at all.
The behaviour underneath it was right and is kept. The first Chrome
top-level is the classroom browser, tiled and borderless; later ones
float centred at about three quarters of the output. Creation order is
the only thing that can tell them apart — confirmed on a real session,
where every Chrome top-level shares app_id, shell, PID and a null
transient_for, and sway exposes no window properties for Wayland
toplevels at all. Titles are still not used. It now follows sway's own
window events instead of polling, so dragging a popup costs nothing.
Close moved into the dock, which was already on screen and does not
move, so it cannot chase anything. It appears only while a secondary
window is focused and runs kill on that one container. The helper
publishes which window that is; the dock watches the file rather than
polling. A stock sway title bar has no close button, so restoring the
border would not have solved this either.
Two bugs found by looking rather than by testing state. Chrome
renegotiates the size of a popup opened with explicit dimensions after
the resize returns, which left windows centred for a width they no
longer had; placement now settles and repeats once, at creation only, so
dragging is still free. And the dock's Close button came up empty
because show_all does not recurse into a no-show-all widget.
The printer's Location is now blank. CUPS prints it under the queue name
in every dialog, so "Ekklesia Classroom" was a second line the teacher
had to read past; the identity is one word. The already-configured check
includes Location, so a machine set up before this is corrected rather
than left as close enough.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first piece of turning these machines into connected endpoints.
classroom-mqtt is one small service per room that subscribes, validates
what arrives, and hands the work to commands that already exist.
Nothing in the room depends on it. A broker that is down, a network that
is missing, a machine with no broker configured at all — each is a
service sitting quietly, not a classroom that fails. Nothing waits for
it at boot and the shipped default is off, so every existing classroom
upgrades into this release with messaging disabled until a broker
exists.
The room is the hostname, which is already how these machines are named.
Topics carry no product name because the broker is the church's and
these are just rooms: classroom/notify/all, classroom/notify/<room>, and
a retained classroom/status/<room> so anyone asking later can be told
which rooms are up without waiting for them to say so again.
Notifications are events and are never retained; status is state and is.
Stale messages cannot come back, which is the property that matters most
and has two halves. The session is clean, so a broker never hands over a
backlog from while the room was off. And retained notifications are
refused on arrival, because otherwise one would replay into every room
at every reboot forever. The honest limit is written down: MQTT only
flags a retained message when it is replayed to a new subscription, so a
retained publish delivered live is indistinguishable from an ordinary
one and is shown once — which is right, since it is live at that moment,
and the replay is the actual hazard.
Delivery reuses the session-aware sender rather than a second copy of
it. classroom-notify-test grew into the real thing, so it is now
classroom-notify with the sample sender a wrapper over it, and its 36
tests came along.
The broker password lives in device.conf, so that file stops being world
readable: 0640 root:classroom, which everything that reads it already
satisfies.
Transport logic is kept free of any broker library so it can be tested
without one — 40 checks on room identity, the topic contract and which
payloads become notifications. Separately verified against a real
mosquitto: connect, subscribe, retained status, per-room and all-room
delivery, urgency, duplicate and expired and malformed messages ignored,
the broker killed with the daemon surviving, reconnect, and a stale
retained notification left on the broker not reaching the screen when a
room boots.
Co-Authored-By: Claude Opus 5 <[email protected]>
Icons went soft under UI_SCALE while the clock text beside them stayed
sharp, which is visible on a 24" monitor even if it hides on a TV.
new_from_file_at_scale rasterises an SVG at its *logical* size. The
artwork is vector, but by the time sway scaled the session it was
already a fixed raster, and the compositor enlarged that. Text never had
the problem because GTK draws it at the real density.
GTK 3 has no fractional scaling, so sway advertises the ceiling integer
scale and scales the finished buffer down. Measured under sway: scale
factor 1 at output scale 1.0, and 2 at 1.25, 1.5 and 2.0. So each icon
is now rasterised at size x scale_factor physical pixels and handed to
GTK as a cairo surface carrying that device scale. The icon lands at the
real density; its logical size, and the whole dock layout, is unchanged.
Icons redraw on notify::scale-factor as well as at realize, since the
scale factor is not known until a widget is on a display and changes if
UI_SCALE is changed under a running session.
Browser, DVD, LIVE and all six DVD control icons go through it, in a
shared module so the dock and the DVD session cannot diverge. The
throbber is drawn rather than loaded and had the same flaw, so it draws
on a device-scaled surface too rather than being the one soft thing left
in a sharp dock.
The artwork is untouched. Verified by rendering the dock under sway at
1.0, 1.25, 1.5 and 2.0 against the previous build: apparent icon size is
identical at every scale (28, 35, 42, 56 px), 1.0 is unchanged
pixel-for-pixel, and peak edge contrast on the Chrome icon goes from 60
to 99 at 1.25, 49 to 83 at 1.5, and 37 to 82 at 2.0 out of 255. The DVD
control bar was checked the same way.
Also records that Wonder Ink session-only cookies are confirmed working
on the endpoint, which settles the question 0.1.34 shipped as unverified
and tells us the session is in cookies rather than localStorage.
Co-Authored-By: Claude Opus 5 <[email protected]>
Clicking Browser and watching nothing happen for several seconds is the
one place the classroom felt broken rather than quiet. The button now
becomes a throbber the instant it is pressed, and stays one until the
window actually appears in the sway tree.
Only a real launch spins. The dock asks sway whether Chrome is already
open first, so focusing or restoring an existing window stays immediate,
which is how it should feel. DVD is left alone: it has its own
full-screen loading surface and does not need two answers to one click.
A launch that never arrives gives up after thirty seconds, puts the icon
back and says so, so a broken start cannot spin all morning.
The throbber is drawn rather than a GtkSpinner: the stock one is capped
well below the size of the icon it replaces, so it read as a hesitation
rather than as work in progress. Pre-rendered frames swapped into the
same Gtk.Image keep the button an ordinary image with ordinary clicks,
and it falls back to GtkSpinner if cairo is ever missing.
The pointer also turns busy while it is over the dock. Wayland gives the
cursor to whichever client the pointer is over and sway offers no way to
set one globally, so this only holds on our own surface — which is
exactly where the pointer is at the moment a dock button is pressed.
Co-Authored-By: Claude Opus 5 <[email protected]>
Three things a volunteer should never have to see.
Sway was drawing its own title bar above Chrome's tab strip, which
already carries the window controls. Verified on a real session: Chrome
is an xdg_shell client with app_id "google-chrome" and sway was giving
it border "normal". A scoped rule removes the compositor decoration
only — Chrome keeps its tabs and address bar, stays a normal window, and
still maximises and minimises. A class rule covers an XWayland fallback.
Nothing else changes: the only other window in this classroom is VLC,
which is already borderless, so no broader rule is warranted.
"Restore pages? Chrome didn't shut down correctly" is suppressed with
--hide-crash-restore-bubble. Note the name. The switch this was going to
use, --disable-session-crashed-bubble, is not present in Chrome 152 at
all — checked against the strings of the installed binary — so it would
have silently done nothing.
That hides the prompt; the profile was still being marked as crashed,
which is the untidy half. Chrome's browser process shuts down cleanly on
SIGTERM, so a shutdown unit now gives it one and waits up to eight
seconds before the session is torn out from under it. Best effort by
construction: a shutdown that will not finish is far worse than a crash
flag.
Password prompts go off with the PasswordManagerEnabled policy, on the
path the classroom already manages. Existing profile and session data
are untouched, and website logins are unaffected: Wonder Ink and YouTube
stay signed in.
Co-Authored-By: Claude Opus 5 <[email protected]>
UI_SCALE only ever reached the surfaces we draw ourselves. Chrome stayed
tiny — its tabs, omnibox and menus are Chrome's own pixels, and Ctrl+
only scales page content, which is no use to a volunteer reading a
toolbar from across the room.
UI_SCALE is a property of the television, not of one program, so it now
sets sway's output scale, which every client is measured against.
classroom-scale runs first in the session and applies it. The display
mode is untouched: the panel stays at 1920x1080 and only the logical
coordinate space shrinks, which is what makes everything in it larger.
Our own surfaces stop scaling themselves. They are measured in the same
logical pixels sway is already scaling, so applying UI_SCALE again would
double it. SHELL_SCALE is that decision in one named place, and setting
it back to ui_scale(config) would restore per-surface scaling without
touching a template.
Measured at 1920x1080 with Chrome 152. Chrome's tab strip 40 -> 49 ->
59px against 1.0, 1.25, 1.5; sway's title bar 25 -> 31 -> 38px; the
classroom dock 74 -> 92 -> 110px. Everything scales once, together, and
1.0 is identical to no scaling at all.
Two tradeoffs, both measured rather than assumed. GTK 3 has no
fractional scaling, so at 1.5 it renders at 2x and sway scales down —
very slightly softer, invisible at classroom distance. XWayland is
softer too: terminal text at 1.5 showed about 50% more intermediate-tone
pixels than at 1.0. VLC is the only XWayland application here, and in
DVD mode none of its interface is on screen, only video from a 720x480
disc that is being upscaled regardless. Chrome is a native Wayland
client and stays sharp, which is what matters for reading.
Co-Authored-By: Claude Opus 5 <[email protected]>
The readiness condition was playback state alone, and on a real disc
VLC reports playing well before its window has taken the screen. The
cover came off onto VLC's ordinary tiled startup window — black, sized
to everything except the dock's reserved strip — and the disc menu only
arrived afterwards. Exactly the state the cover exists to hide.
Ask sway instead. VLC only takes the display when its window goes
fullscreen, and the tree says when that happens. The cover now comes off
when VLC reports playing or paused, its window is fullscreen, and 800ms
have passed for the menu to draw its first frames. The playback check
runs first because it is local and cheap; the tree is only walked once
VLC claims to be playing something.
The twenty second deadline is unchanged: a cover with nothing behind it
is still the one outcome DVD mode must never produce.
Co-Authored-By: Claude Opus 5 <[email protected]>
classroom-dvd-session needs to ask sway whether VLC has taken the
screen, which is the same tree walking classroom-window already does.
One copy, in a module both import; classroom-window loses a third of its
length.
The state machine is worth protecting through a move like that, so its
regression test comes with it: the exact swaymsg commands for launch,
focus, minimise, restore, Super+M, Super+Shift+M, the scratchpad rescue
and sway not running at all.
Co-Authored-By: Claude Opus 5 <[email protected]>
The loading surface was invisible. GtkWindow only renders its own CSS
background when the application is not painting the window itself, and
this window was created app-paintable with no draw handler and no child
— so it drew nothing at all. The black area during loading was never it:
it was VLC's own window, tiled by sway into the usable area, which
excludes the strip the dock reserves. That is why the dock and the
wallpaper stayed on screen underneath.
An earlier headless check missed this because that sway had no wallpaper
configured, so an invisible surface and a black one looked identical.
Rechecked with the real wallpaper and the real dock underneath.
The surface now paints, and carries a spinner and "Loading DVD..." on
the same translucent rounded card as the dock. It moves from the top
layer to the overlay layer, because the dock is a top-layer surface and
DVD mode has to cover it rather than depend on which surface sway
created first. Its exclusive zone of -1 keeps sway from moving it out of
the way of the dock's reserved strip.
It goes up before VLC is started rather than alongside it, so no part of
VLC's window is ever seen against the wallpaper, and comes down only
once VLC reports playback on screen. The deadline that takes it down
regardless is now twenty seconds, since an optical drive can be slow,
and a failure to start VLC at all ends the session instead of leaving a
cover with nothing behind it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Pressing DVD now enters DVD mode rather than opening VLC. VLC is still
the backend, because its dvdnav module is what decodes disc menus and
nothing else on Debian does that reliably, but nothing a person can see
or press belongs to it any more.
classroom-dvd-session owns the whole thing. It puts a full-screen black
layer-shell surface up the moment the button is pressed, starts VLC
behind it, and drops it once playback is actually on screen — so the
harsh cut from wallpaper to VLC's black window is gone, and no partial
VLC window or reserved dock strip is ever visible. A deadline drops the
surface regardless, because DVD mode must never leave a black screen
with nothing behind it.
The controls are a classroom bar on the overlay layer, the one layer
sway draws above a fullscreen window. It slides up when the pointer
reaches the bottom of the screen and hides again shortly after it
leaves. It is drawn from the dock's own stylesheet at the room's UI
scale, so it is the same surface, the same radius and the same white
icons. While hidden it only accepts the pointer in a thin strip along
the bottom edge, so a click on a disc menu goes to the disc.
The bar reaches VLC through an HTTP interface on loopback behind a
password generated per session, sending hotkey *actions* by name. That
is what makes the next part possible.
All 108 hotkeys VLC 3.0.23 defines are unset, then Space, Esc and the
arrow keys plus Enter are bound. Crop, aspect ratio, subtitles, audio
tracks and deinterlacing are no longer reachable by pressing a key at
random, and the arrow keys no longer change VLC's volume — the
television owns volume, and CEC will own it properly later.
Esc quits instead of leaving fullscreen. There is nothing to leave, and
leaving it was what stranded people on a black VLC window. Close, Eject
and Esc all ask VLC to quit and escalate to SIGTERM then SIGKILL if it
does not; when VLC exits by any route, including a crash, the session
ends and the classroom desktop comes back.
Verified against VLC 3.0.23 on Trixie: every argument accepted, every
control action accepted over the control interface, a wrong password
refused, and quit exiting cleanly. The surfaces were rendered under a
headless sway and screenshotted to check the bar against the dock.
Co-Authored-By: Claude Opus 5 <[email protected]>
The DVD control bar has to look like the dock, which is best guaranteed
by drawing it from the same template with the same measurements rather
than by keeping two stylesheets in step by hand.
Co-Authored-By: Claude Opus 5 <[email protected]>
A fresh classroom needed two things installed by hand on every box:
Google Chrome, which is not in Debian, and the DVD codecs, which Debian
cannot ship. Both now happen on their own.
ekklesia-classroom-provision.service runs after every boot and does the
work that needs the network and the apt lock, neither of which a package
postinst can have. Each step returns immediately if it is already done,
so a provisioned machine costs nothing and a machine that was offline
the first time gets another go at the next boot. Nothing there is
allowed to fail the boot: a classroom with no DVD codecs is still a
working classroom, and it says so in the log rather than silently.
The postinst enables the service but deliberately does not start it.
Every OTA update reboots the moment the install finishes, and an apt run
killed by that reboot would leave dpkg half-configured.
Chrome comes from Google's own repository rather than a one-off .deb, so
it keeps getting security updates. Google's package migrates whatever we
write into a deb822 file of its own, so the repository is looked for
across every source file instead of only the one we would write;
otherwise a machine that had Chrome removed would end up with two copies
of the same repository.
The DVD codec helper previously assumed deb822 sources and did nothing
on a machine using a classic /etc/apt/sources.list, which is what the
hardware actually runs. It now handles both formats, and only ever adds
a missing component to an existing Debian entry — so it cannot produce a
duplicate repository or a duplicate component however many times it
runs, and third-party repositories like Chrome's are never touched. It
also answers libdvd-pkg's debconf questions, so nothing blocks on a
prompt in a service with no terminal.
Tested against fixture APT trees for classic sources.list, deb822
.sources, contrib already enabled, repeated runs, third-party
repositories and the awkward real lines: bracketed options, indentation,
trailing comments and flat repositories.
Co-Authored-By: Claude Opus 5 <[email protected]>
Stock VLC looks like a desktop media player, which is wrong for a room
where the TV is the whole interface. VLC stays the backend because its
dvdnav module is what provides the disc menus, and menus are the part
that matters on a commercial disc.
It is dressed down with documented VLC 3.x options, all verified against
the 3.0.23 build in Debian Trixie: minimal view, dark palette, no cone,
no tray, no now-playing popups, no resume prompt, no recent-disc
history, no title banner. sway draws no border on the window either, so
there is nothing above the video.
The auto-hiding fullscreen controller is deliberately kept: it is not
persistent chrome, and it is the only way a volunteer with a mouse can
pause or change chapter.
No skin is involved. Skins replace the interface wholesale and take the
keyboard and mouse handling that DVD menu navigation depends on with
them, as would --intf dummy.
Also drops --no-qt-updates-notif, which was not an option in Debian's
VLC at all. It made VLC exit before it started, so the DVD button could
never have launched a disc.
Co-Authored-By: Claude Opus 5 <[email protected]>
A 32" panel a few feet from the children and a 65" set across a hall
want the same design at different sizes. UI_SCALE in device.conf now
sets that, and nobody has to edit CSS or source to tune a room.
The stylesheet and the notification config become templates carrying
measurement tokens. classroom-dock resolves the stylesheet in memory at
startup; classroom-notifications resolves the notification config into
the session's runtime directory and execs mako on it, so the process is
still just mako and everything that looks for it by name still finds it.
Neither template is rewritten on disk, so an update cannot lose a room's
setting and a room cannot drift from the packaged design.
Dock height and the reserved strip, corner radii, padding, button size,
spacing, every icon, the clock and its inset, and the notification
geometry all follow the scale. Colours, translucency and the hairline
borders do not: those are the design, not the size.
At UI_SCALE=1.0 the rendered stylesheet is byte-identical to the one the
shell used before any of this existed, which is the property worth
keeping. Values outside 0.8 to 1.5 are clamped and anything that is not
a number falls back to 1.0, so a typo in device.conf cannot produce an
unusable classroom.
Device configuration moves into a shared module, since the dock and the
notification daemon now have to agree about what a setting means.
Co-Authored-By: Claude Opus 5 <[email protected]>
The classroom desktop belongs to the classroom user and mako sits on
that user's D-Bus session. An administrator over SSH is in a different
session where nothing owns org.freedesktop.Notifications, so notify-send
asked D-Bus to start a second mako, which had no display to open and
timed out with NoReply.
Find the running sway process for the classroom user and deliver into
its session instead. XDG_RUNTIME_DIR and the bus address are read from
that process, falling back to /run/user/<uid> and its bus socket when
the environment cannot be read — root has ptrace access on the endpoint,
but that is not somewhere to be brittle. The Wayland display is read the
same way, and failing that from the wayland-N.lock sway holds open, so
no socket name is ever assumed. notify-send only needs the bus, so a
display that cannot be resolved is not worth refusing to send over.
Refuses to send when mako is not already running, rather than letting
D-Bus start one, and says plainly what is wrong when there is no
session, no privileges, or no bus socket. --dry-run reports what it
resolved without sending.
Behaviour inside the session is unchanged, and the sample notifications
and styling are untouched.
The tests stand a real background process in for sway so /proc/<pid>/environ
is genuine rather than mocked, and cover both invocation paths plus the
fallbacks and every refusal. The build runs them.
Co-Authored-By: Claude Opus 5 <[email protected]>
Nothing puts windows there any more, but a window parked by an older
classroom build — the exact case this work exists to fix — would
otherwise be invisible to restore and stranded until the next reboot.
Counting the scratchpad as hidden means the Browser button and
Super+Shift+M both pull it back, tiled.
Co-Authored-By: Claude Opus 5 <[email protected]>
DVD support stays optional per room: DVD_ENABLED decides whether the
button is built at all, and rooms without it poll nothing. Upgrades
append a commented DVD_ENABLED=false so the option is discoverable
without changing what those rooms already do.
The dock polls the drive with CDROM_DRIVE_STATUS, which is a status
query rather than a read, so it does not spin the disc up. A disc that
becomes ready announces itself once and brings the DVD logo up to full
strength; an empty or unplugged drive holds it back, the same gray
versus colour language the LIVE icon already uses. Nothing autoplays —
playback always needs a deliberate press.
Playback is VLC rather than mpv because mpv dropped DVD menu support,
and menus are the part that matters for a commercial disc. It goes
through classroom-window like everything else, so the player minimizes
and restores from the dock exactly as the browser does.
Pressing the button with no drive, no disc, or a disc still spinning up
gives a plain-language notification instead of failing silently.
Reading the drive needs group cdrom, which the postinst grants and the
OTA reboot activates. Commercial CSS discs additionally need libdvdcss,
which Debian cannot ship; classroom-dvd-css-setup bootstraps it from
contrib. That is deliberately not run from the postinst, which executes
while dpkg already holds the apt lock.
The optical helpers live in a shared module both the dock and
classroom-dvd import, and the dock imports it defensively: if it ever
goes missing the shell loses disc detection rather than failing to
start.
Co-Authored-By: Claude Opus 5 <[email protected]>
Chrome stays a full browser. The managed policy only removes the things
a volunteer should never be asked about — the first-run tour, the
default-browser prompt, sync and sign-in promos, the profile picker —
and pins the Home button to Wonder Ink so there is always one obvious
way back. Startup URL is a recommended policy rather than a managed one,
so a teacher can still change it. Nothing restricts where they browse,
and the profile is untouched, so YouTube and other logins survive
reboots and updates.
Notifications were still stock mako, which looked nothing like the rest
of the shell. Give them the dock's palette: the same dark slate, the
same hairline white border, rounded, white text, top right. Kept a
little more opaque than the dock so text stays readable over video, and
on the overlay layer so a parent-call alert can still reach someone
watching a fullscreen DVD.
mako was only ever reaching the session through D-Bus activation. Start
it from the sway config so it is running before anything needs it.
classroom-notify-test sends a sample so the styling can be checked
without waiting for a real event.
Co-Authored-By: Claude Opus 5 <[email protected]>
The scratchpad technically minimized Chrome, but sway hands scratchpad
windows back floating, so restoring gave a draggable window instead of
the maximized one the user put away. Park windows on a hidden workspace
as ordinary tiled containers instead, and move them back to the visible
workspace on restore.
classroom-window carries the whole state machine, so the dock buttons
and Super+M share one implementation:
not running -> launch
unfocused -> focus
focused -> minimize
minimized -> restore, tiled and maximized
Fullscreen is dropped on the way out, since a container parked while
still fullscreen keeps drawing over everything else. floating disable on
the way back also rescues windows left floating by the old scratchpad
path.
Super+Shift+M restores the newest minimized window, so nothing can get
stranded on the hidden workspace.
Co-Authored-By: Claude Opus 5 <[email protected]>